diff --git a/.gitignore b/.gitignore
index 4c910ec89690225dbb8d54806f6547e9e805fc90..9a265372d7702acbb17a2f9f32769e5b7a4885c0 100755
--- a/.gitignore
+++ b/.gitignore
@@ -83,3 +83,4 @@ web/locale.*
 web/secondlife.com.*
 /Pipfile.lock
 .env
+.vscode
diff --git a/doc/contributions.txt b/doc/contributions.txt
index 2fbe8ab7cc49d1cd2451219f9c3375c292c0d8b1..a097aad7f63da59771d358f7da0365671ed02dc1 100755
--- a/doc/contributions.txt
+++ b/doc/contributions.txt
@@ -240,6 +240,7 @@ Ansariel Hiller
 	SL-18432
 	SL-19140
 	SL-4126
+	SL-20224
 Aralara Rajal
 Arare Chantilly
 	CHUIBUG-191
@@ -929,6 +930,8 @@ LSL Scientist
 Lamorna Proctor
 Lares Carter
 Larry Pixel
+Lars Næsbye Christensen
+	SL-20054
 Laurent Bechir
 Leal Choche
 Lenae Munz
diff --git a/indra/CMakeLists.txt b/indra/CMakeLists.txt
index 48cfb6e5917efd6fb188fb83d98822a979b3ba1e..f8099ca3e0e0cc9a5420202a58d4d4d540143155 100644
--- a/indra/CMakeLists.txt
+++ b/indra/CMakeLists.txt
@@ -60,7 +60,6 @@ else()
 endif()
 
 set_property(GLOBAL PROPERTY USE_FOLDERS ON)
-
 if (NOT CMAKE_BUILD_TYPE)
   set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING
       "Build type.  One of: Debug Release RelWithDebInfo" FORCE)
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index c7e34b83f05e805c460d0d4c7f8aa8611729af88..9490a342e0d4ab76c46624f51c87d17887c50d0d 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -19,8 +19,10 @@ include(XXHash)
 
 
 set(llcommon_SOURCE_FILES
+    apply.cpp
     commoncontrol.cpp
     indra_constants.cpp
+    lazyeventapi.cpp
     llapp.cpp
     llapr.cpp
     llassettype.cpp
@@ -117,12 +119,16 @@ set(llcommon_SOURCE_FILES
 set(llcommon_HEADER_FILES
     CMakeLists.txt
 
+    always_return.h
+    apply.h
     chrono.h
     classic_callback.h
     commoncontrol.h
     ctype_workaround.h
     fix_macros.h
+    function_types.h
     indra_constants.h
+    lazyeventapi.h
     linden_common.h
     llalignedarray.h
     llapp.h
@@ -329,9 +335,11 @@ if (LL_TESTS)
 
   #set(TEST_DEBUG on)
   set(test_libs llcommon)
+  LL_ADD_INTEGRATION_TEST(apply "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(bitpack "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(classic_callback "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(commonmisc "" "${test_libs}")
+  LL_ADD_INTEGRATION_TEST(lazyeventapi "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(llbase64 "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(llcond "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(lldate "" "${test_libs}")
diff --git a/indra/llcommon/always_return.h b/indra/llcommon/always_return.h
new file mode 100644
index 0000000000000000000000000000000000000000..6b9f1fdeafabcaf7119b4d461134a701aa6c2de4
--- /dev/null
+++ b/indra/llcommon/always_return.h
@@ -0,0 +1,124 @@
+/**
+ * @file   always_return.h
+ * @author Nat Goodspeed
+ * @date   2023-01-20
+ * @brief  Call specified callable with arbitrary arguments, but always return
+ *         specified type.
+ * 
+ * $LicenseInfo:firstyear=2023&license=viewerlgpl$
+ * Copyright (c) 2023, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_ALWAYS_RETURN_H)
+#define LL_ALWAYS_RETURN_H
+
+#include <type_traits>              // std::enable_if, std::is_convertible
+
+namespace LL
+{
+
+#if __cpp_lib_is_invocable >= 201703L // C++17
+    template <typename CALLABLE, typename... ARGS>
+    using invoke_result = std::invoke_result<CALLABLE, ARGS...>;
+#else  // C++14
+    template <typename CALLABLE, typename... ARGS>
+    using invoke_result = std::result_of<CALLABLE(ARGS...)>;
+#endif // C++14
+
+    /**
+     * AlwaysReturn<T>()(some_function, some_args...) calls
+     * some_function(some_args...). It is guaranteed to return a value of type
+     * T, regardless of the return type of some_function(). If some_function()
+     * returns a type convertible to T, it will convert and return that value.
+     * Otherwise (notably if some_function() is void), AlwaysReturn returns
+     * T().
+     *
+     * When some_function() returns a type not convertible to T, if
+     * you want AlwaysReturn to return some T value other than
+     * default-constructed T(), pass that value to AlwaysReturn's constructor.
+     */
+    template <typename DESIRED>
+    class AlwaysReturn
+    {
+    public:
+        /// pass explicit default value if other than default-constructed type
+        AlwaysReturn(const DESIRED& dft=DESIRED()): mDefault(dft) {}
+
+        // callable returns a type not convertible to DESIRED, return default
+        template <typename CALLABLE, typename... ARGS,
+                  typename std::enable_if<
+                      ! std::is_convertible<
+                          typename invoke_result<CALLABLE, ARGS...>::type,
+                          DESIRED
+                      >::value,
+                      bool
+                  >::type=true>
+        DESIRED operator()(CALLABLE&& callable, ARGS&&... args)
+        {
+            // discard whatever callable(args) returns
+            std::forward<CALLABLE>(callable)(std::forward<ARGS>(args)...);
+            return mDefault;
+        }
+
+        // callable returns a type convertible to DESIRED
+        template <typename CALLABLE, typename... ARGS,
+                  typename std::enable_if<
+                      std::is_convertible<
+                          typename invoke_result<CALLABLE, ARGS...>::type,
+                          DESIRED
+                      >::value,
+                      bool
+                  >::type=true>
+        DESIRED operator()(CALLABLE&& callable, ARGS&&... args)
+        {
+            return { std::forward<CALLABLE>(callable)(std::forward<ARGS>(args)...) };
+        }
+
+    private:
+        DESIRED mDefault;
+    };
+
+    /**
+     * always_return<T>(some_function, some_args...) calls
+     * some_function(some_args...). It is guaranteed to return a value of type
+     * T, regardless of the return type of some_function(). If some_function()
+     * returns a type convertible to T, it will convert and return that value.
+     * Otherwise (notably if some_function() is void), always_return() returns
+     * T().
+     */
+    template <typename DESIRED, typename CALLABLE, typename... ARGS>
+    DESIRED always_return(CALLABLE&& callable, ARGS&&... args)
+    {
+        return AlwaysReturn<DESIRED>()(std::forward<CALLABLE>(callable),
+                                       std::forward<ARGS>(args)...);
+    }
+
+    /**
+     * make_always_return<T>(some_function) returns a callable which, when
+     * called with appropriate some_function() arguments, always returns a
+     * value of type T, regardless of the return type of some_function(). If
+     * some_function() returns a type convertible to T, the returned callable
+     * will convert and return that value. Otherwise (notably if
+     * some_function() is void), the returned callable returns T().
+     *
+     * When some_function() returns a type not convertible to T, if
+     * you want the returned callable to return some T value other than
+     * default-constructed T(), pass that value to make_always_return() as its
+     * optional second argument.
+     */
+    template <typename DESIRED, typename CALLABLE>
+    auto make_always_return(CALLABLE&& callable, const DESIRED& dft=DESIRED())
+    {
+        return
+            [dft, callable = std::forward<CALLABLE>(callable)]
+            (auto&&... args)
+            {
+                return AlwaysReturn<DESIRED>(dft)(callable,
+                                                  std::forward<decltype(args)>(args)...);
+            };
+    }
+
+} // namespace LL
+
+#endif /* ! defined(LL_ALWAYS_RETURN_H) */
diff --git a/indra/llcommon/apply.cpp b/indra/llcommon/apply.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..417e23d3b480442eb64e1650cbf59f7a7bdb665c
--- /dev/null
+++ b/indra/llcommon/apply.cpp
@@ -0,0 +1,29 @@
+/**
+ * @file   apply.cpp
+ * @author Nat Goodspeed
+ * @date   2022-12-21
+ * @brief  Implementation for apply.
+ * 
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Copyright (c) 2022, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "apply.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "stringize.h"
+
+void LL::apply_validate_size(size_t size, size_t arity)
+{
+    if (size != arity)
+    {
+        LLTHROW(apply_error(stringize("LL::apply(func(", arity, " args), "
+                                      "std::vector(", size, " elements))")));
+    }
+}
diff --git a/indra/llcommon/apply.h b/indra/llcommon/apply.h
index 7c58d63bc03f4a45aac4db3fe1b20177ade5b652..cf6161ed5012b8001bf07821711dfac02f6b7cf6 100644
--- a/indra/llcommon/apply.h
+++ b/indra/llcommon/apply.h
@@ -12,8 +12,11 @@
 #if ! defined(LL_APPLY_H)
 #define LL_APPLY_H
 
+#include "llexception.h"
 #include <boost/type_traits/function_traits.hpp>
+#include <functional>               // std::mem_fn()
 #include <tuple>
+#include <type_traits>              // std::is_member_pointer
 
 namespace LL
 {
@@ -54,20 +57,67 @@ namespace LL
         },                                                          \
         (ARGS))
 
-#if __cplusplus >= 201703L
+/*****************************************************************************
+*   invoke()
+*****************************************************************************/
+#if __cpp_lib_invoke >= 201411L
 
 // C++17 implementation
-using std::apply;
+using std::invoke;
+
+#else  // no std::invoke
+
+// Use invoke() to handle pointer-to-method:
+// derived from https://stackoverflow.com/a/38288251
+template<typename Fn, typename... Args, 
+         typename std::enable_if<std::is_member_pointer<typename std::decay<Fn>::type>::value,
+                                 int>::type = 0 >
+auto invoke(Fn&& f, Args&&... args)
+{
+    return std::mem_fn(std::forward<Fn>(f))(std::forward<Args>(args)...);
+}
+
+template<typename Fn, typename... Args, 
+         typename std::enable_if<!std::is_member_pointer<typename std::decay<Fn>::type>::value,
+                                 int>::type = 0 >
+auto invoke(Fn&& f, Args&&... args)
+{
+    return std::forward<Fn>(f)(std::forward<Args>(args)...);
+}
+
+#endif // no std::invoke
+
+/*****************************************************************************
+*   apply(function, tuple); apply(function, array)
+*****************************************************************************/
+#if __cpp_lib_apply >= 201603L
+
+// C++17 implementation
+// We don't just say 'using std::apply;' because that template is too general:
+// it also picks up the apply(function, vector) case, which we want to handle
+// below.
+template <typename CALLABLE, typename... ARGS>
+auto apply(CALLABLE&& func, const std::tuple<ARGS...>& args)
+{
+    return std::apply(std::forward<CALLABLE>(func), args);
+}
 
 #else // C++14
 
 // Derived from https://stackoverflow.com/a/20441189
 // and https://en.cppreference.com/w/cpp/utility/apply
-template <typename CALLABLE, typename TUPLE, std::size_t... I>
-auto apply_impl(CALLABLE&& func, TUPLE&& args, std::index_sequence<I...>)
+template <typename CALLABLE, typename... ARGS, std::size_t... I>
+auto apply_impl(CALLABLE&& func, const std::tuple<ARGS...>& args, std::index_sequence<I...>)
 {
+    // We accept const std::tuple& so a caller can construct an tuple on the
+    // fly. But std::get<I>(const tuple) adds a const qualifier to everything
+    // it extracts. Get a non-const ref to this tuple so we can extract
+    // without the extraneous const.
+    auto& non_const_args{ const_cast<std::tuple<ARGS...>&>(args) };
+
     // call func(unpacked args)
-    return std::forward<CALLABLE>(func)(std::move(std::get<I>(args))...);
+    return invoke(std::forward<CALLABLE>(func),
+                  std::forward<ARGS>(std::get<I>(non_const_args))...);
 }
 
 template <typename CALLABLE, typename... ARGS>
@@ -81,6 +131,8 @@ auto apply(CALLABLE&& func, const std::tuple<ARGS...>& args)
                       std::index_sequence_for<ARGS...>{});
 }
 
+#endif // C++14
+
 // per https://stackoverflow.com/a/57510428/5533635
 template <typename CALLABLE, typename T, size_t SIZE>
 auto apply(CALLABLE&& func, const std::array<T, SIZE>& args)
@@ -88,28 +140,92 @@ auto apply(CALLABLE&& func, const std::array<T, SIZE>& args)
     return apply(std::forward<CALLABLE>(func), std::tuple_cat(args));
 }
 
+/*****************************************************************************
+*   bind_front()
+*****************************************************************************/
+// To invoke a non-static member function with a tuple, you need a callable
+// that binds your member function with an instance pointer or reference.
+// std::bind_front() is perfect: std::bind_front(&cls::method, instance).
+// Unfortunately bind_front() only enters the standard library in C++20.
+#if __cpp_lib_bind_front >= 201907L
+
+// C++20 implementation
+using std::bind_front;
+
+#else  // no std::bind_front()
+
+template<typename Fn, typename... Args, 
+         typename std::enable_if<!std::is_member_pointer<typename std::decay<Fn>::type>::value,
+                                 int>::type = 0 >
+auto bind_front(Fn&& f, Args&&... args)
+{
+    // Don't use perfect forwarding for f or args: we must bind them for later.
+    return [f, pfx_args=std::make_tuple(args...)]
+        (auto&&... sfx_args)
+    {
+        // Use perfect forwarding for sfx_args because we use them as soon as
+        // we receive them.
+        return apply(
+            f,
+            std::tuple_cat(pfx_args,
+                           std::make_tuple(std::forward<decltype(sfx_args)>(sfx_args)...)));
+    };
+}
+
+template<typename Fn, typename... Args, 
+         typename std::enable_if<std::is_member_pointer<typename std::decay<Fn>::type>::value,
+                                 int>::type = 0 >
+auto bind_front(Fn&& f, Args&&... args)
+{
+    return bind_front(std::mem_fn(std::forward<Fn>(f)), std::forward<Args>(args)...);
+}
+
+#endif // C++20 with std::bind_front()
+
+/*****************************************************************************
+*   apply(function, std::vector)
+*****************************************************************************/
 // per https://stackoverflow.com/a/28411055/5533635
 template <typename CALLABLE, typename T, std::size_t... I>
 auto apply_impl(CALLABLE&& func, const std::vector<T>& args, std::index_sequence<I...>)
 {
+    return apply(std::forward<CALLABLE>(func),
+                 std::make_tuple(args[I]...));
+}
+
+// produce suitable error if apply(func, vector) is the wrong size for func()
+void apply_validate_size(size_t size, size_t arity);
+
+/// possible exception from apply() validation
+struct apply_error: public LLException
+{
+    apply_error(const std::string& what): LLException(what) {}
+};
+
+template <size_t ARITY, typename CALLABLE, typename T>
+auto apply_n(CALLABLE&& func, const std::vector<T>& args)
+{
+    apply_validate_size(args.size(), ARITY);
     return apply_impl(std::forward<CALLABLE>(func),
-                      std::make_tuple(std::forward<T>(args[I])...),
-                      I...);
+                      args,
+                      std::make_index_sequence<ARITY>());
 }
 
-// this goes beyond C++17 std::apply()
+/**
+ * apply(function, std::vector) goes beyond C++17 std::apply(). For this case
+ * @a function @emph cannot be variadic: the compiler must know at compile
+ * time how many arguments to pass. This isn't Python. (But see apply_n() to
+ * pass a specific number of args to a variadic function.)
+ */
 template <typename CALLABLE, typename T>
 auto apply(CALLABLE&& func, const std::vector<T>& args)
 {
+    // infer arity from the definition of func
     constexpr auto arity = boost::function_traits<CALLABLE>::arity;
-    assert(args.size() == arity);
-    return apply_impl(std::forward<CALLABLE>(func),
-                      args,
-                      std::make_index_sequence<arity>());
+    // now that we have a compile-time arity, apply_n() works
+    return apply_n<arity>(std::forward<CALLABLE>(func), args);
 }
 
-#endif // C++14
-
 } // namespace LL
 
 #endif /* ! defined(LL_APPLY_H) */
diff --git a/indra/llcommon/function_types.h b/indra/llcommon/function_types.h
new file mode 100644
index 0000000000000000000000000000000000000000..3f42f6d64046e218599802172314d3a7426445a3
--- /dev/null
+++ b/indra/llcommon/function_types.h
@@ -0,0 +1,49 @@
+/**
+ * @file   function_types.h
+ * @author Nat Goodspeed
+ * @date   2023-01-20
+ * @brief  Extend boost::function_types to examine boost::function and
+ *         std::function
+ * 
+ * $LicenseInfo:firstyear=2023&license=viewerlgpl$
+ * Copyright (c) 2023, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_FUNCTION_TYPES_H)
+#define LL_FUNCTION_TYPES_H
+
+#include <boost/function.hpp>
+#include <boost/function_types/function_arity.hpp>
+#include <functional>
+
+namespace LL
+{
+
+    template <typename F>
+    struct function_arity_impl
+    {
+        static constexpr auto value = boost::function_types::function_arity<F>::value;
+    };
+
+    template <typename F>
+    struct function_arity_impl<std::function<F>>
+    {
+        static constexpr auto value = function_arity_impl<F>::value;
+    };
+
+    template <typename F>
+    struct function_arity_impl<boost::function<F>>
+    {
+        static constexpr auto value = function_arity_impl<F>::value;
+    };
+
+    template <typename F>
+    struct function_arity
+    {
+        static constexpr auto value = function_arity_impl<typename std::decay<F>::type>::value;
+    };
+
+} // namespace LL
+
+#endif /* ! defined(LL_FUNCTION_TYPES_H) */
diff --git a/indra/llcommon/lazyeventapi.cpp b/indra/llcommon/lazyeventapi.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..028af9f33f3a9f9dc3619af6bcb667e594e5eb7a
--- /dev/null
+++ b/indra/llcommon/lazyeventapi.cpp
@@ -0,0 +1,72 @@
+/**
+ * @file   lazyeventapi.cpp
+ * @author Nat Goodspeed
+ * @date   2022-06-17
+ * @brief  Implementation for lazyeventapi.
+ * 
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Copyright (c) 2022, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lazyeventapi.h"
+// STL headers
+// std headers
+#include <algorithm>                // std::find_if
+// external library headers
+// other Linden headers
+#include "llevents.h"
+#include "llsdutil.h"
+
+LL::LazyEventAPIBase::LazyEventAPIBase(
+    const std::string& name, const std::string& desc, const std::string& field)
+{
+    // populate embedded LazyEventAPIParams instance
+    mParams.name = name;
+    mParams.desc = desc;
+    mParams.field = field;
+    // mParams.init and mOperations are populated by subsequent add() calls.
+
+    // Our raison d'etre: register as an LLEventPumps::PumpFactory
+    // so obtain() will notice any request for this name and call us.
+    // Of course, our subclass constructor must finish running (making add()
+    // calls) before mParams will be fully populated, but we expect that to
+    // happen well before the first LLEventPumps::obtain(name) call.
+    mRegistered = LLEventPumps::instance().registerPumpFactory(
+        name,
+        [this](const std::string& name){ return construct(name); });
+}
+
+LL::LazyEventAPIBase::~LazyEventAPIBase()
+{
+    // If our constructor's registerPumpFactory() call was unsuccessful, that
+    // probably means somebody else claimed the name first. If that's the
+    // case, do NOT unregister their name out from under them!
+    // If this is a static instance being destroyed at process shutdown,
+    // LLEventPumps will probably have been cleaned up already.
+    if (mRegistered && ! LLEventPumps::wasDeleted())
+    {
+        // unregister the callback to this doomed instance
+        LLEventPumps::instance().unregisterPumpFactory(mParams.name);
+    }
+}
+
+LLSD LL::LazyEventAPIBase::getMetadata(const std::string& name) const
+{
+    // Since mOperations is a vector rather than a map, just search.
+    auto found = std::find_if(mOperations.begin(), mOperations.end(),
+                              [&name](const auto& namedesc)
+                              { return (namedesc.first == name); });
+    if (found == mOperations.end())
+        return {};
+
+    // LLEventDispatcher() supplements the returned metadata in different
+    // ways, depending on metadata provided to the specific add() method.
+    // Don't try to emulate all that. At some point we might consider more
+    // closely unifying LLEventDispatcher machinery with LazyEventAPI, but for
+    // now this will have to do.
+    return llsd::map("name", found->first, "desc", found->second);
+}
diff --git a/indra/llcommon/lazyeventapi.h b/indra/llcommon/lazyeventapi.h
new file mode 100644
index 0000000000000000000000000000000000000000..e36831270b0071a55bcc696a5b4944cedac1c4f6
--- /dev/null
+++ b/indra/llcommon/lazyeventapi.h
@@ -0,0 +1,205 @@
+/**
+ * @file   lazyeventapi.h
+ * @author Nat Goodspeed
+ * @date   2022-06-16
+ * @brief  Declaring a static module-scope LazyEventAPI registers a specific
+ *         LLEventAPI for future on-demand instantiation.
+ * 
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Copyright (c) 2022, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LAZYEVENTAPI_H)
+#define LL_LAZYEVENTAPI_H
+
+#include "apply.h"
+#include "lleventapi.h"
+#include "llinstancetracker.h"
+#include <boost/signals2/signal.hpp>
+#include <string>
+#include <tuple>
+#include <utility>                  // std::pair
+#include <vector>
+
+namespace LL
+{
+    /**
+     * Bundle params we want to pass to LLEventAPI's protected constructor. We
+     * package them this way so a subclass constructor can simply forward an
+     * opaque reference to the LLEventAPI constructor.
+     */
+    // This is a class instead of a plain struct mostly so when we forward-
+    // declare it we don't have to remember the distinction.
+    class LazyEventAPIParams
+    {
+    public:
+        // package the parameters used by the normal LLEventAPI constructor
+        std::string name, desc, field;
+        // bundle LLEventAPI::add() calls collected by LazyEventAPI::add(), so
+        // the special LLEventAPI constructor we engage can "play back" those
+        // add() calls
+        boost::signals2::signal<void(LLEventAPI*)> init;
+    };
+
+    /**
+     * LazyEventAPIBase implements most of the functionality of LazyEventAPI
+     * (q.v.), but we need the LazyEventAPI template subclass so we can accept
+     * the specific LLEventAPI subclass type.
+     */
+    // No LLInstanceTracker key: we don't need to find a specific instance,
+    // LLLeapListener just needs to be able to enumerate all instances.
+    class LazyEventAPIBase: public LLInstanceTracker<LazyEventAPIBase>
+    {
+    public:
+        LazyEventAPIBase(const std::string& name, const std::string& desc,
+                         const std::string& field);
+        virtual ~LazyEventAPIBase();
+
+        // Do not copy or move: once constructed, LazyEventAPIBase must stay
+        // put: we bind its instance pointer into a callback.
+        LazyEventAPIBase(const LazyEventAPIBase&) = delete;
+        LazyEventAPIBase(LazyEventAPIBase&&) = delete;
+        LazyEventAPIBase& operator=(const LazyEventAPIBase&) = delete;
+        LazyEventAPIBase& operator=(LazyEventAPIBase&&) = delete;
+
+        // capture add() calls we want to play back on LLEventAPI construction
+        template <typename... ARGS>
+        void add(const std::string& name, const std::string& desc, ARGS&&... rest)
+        {
+            // capture the metadata separately
+            mOperations.push_back(std::make_pair(name, desc));
+            // Use connect_extended() so the lambda is passed its own
+            // connection.
+
+            // apply() can't accept a template per se; it needs a particular
+            // specialization. Specialize out here to work around a clang bug:
+            // https://github.com/llvm/llvm-project/issues/41999
+            auto func{ &LazyEventAPIBase::add_trampoline
+                       <const std::string&, const std::string&, ARGS...> };
+            // We can't bind an unexpanded parameter pack into a lambda --
+            // shame really. Instead, capture all our args as a std::tuple and
+            // then, in the lambda, use apply() to pass to add_trampoline().
+            auto args{ std::make_tuple(name, desc, std::forward<ARGS>(rest)...) };
+
+            mParams.init.connect_extended(
+                [func, args]
+                (const boost::signals2::connection& conn, LLEventAPI* instance)
+                {
+                    // we only need this connection once
+                    conn.disconnect();
+                    // apply() expects a tuple specifying ALL the arguments,
+                    // so prepend instance.
+                    apply(func, std::tuple_cat(std::make_tuple(instance), args));
+                });
+        }
+
+        // The following queries mimic the LLEventAPI / LLEventDispatcher
+        // query API.
+
+        // Get the string name of the subject LLEventAPI
+        std::string getName() const { return mParams.name; }
+        // Get the documentation string
+        std::string getDesc() const { return mParams.desc; }
+        // Retrieve the LLSD key we use for dispatching
+        std::string getDispatchKey() const { return mParams.field; }
+
+        // operations
+        using NameDesc = std::pair<std::string, std::string>;
+
+    private:
+        // metadata that might be queried by LLLeapListener
+        std::vector<NameDesc> mOperations;
+
+    public:
+        using const_iterator = decltype(mOperations)::const_iterator;
+        const_iterator begin() const { return mOperations.begin(); }
+        const_iterator end()   const { return mOperations.end(); }
+        LLSD getMetadata(const std::string& name) const;
+
+    protected:
+        // Params with which to instantiate the companion LLEventAPI subclass
+        LazyEventAPIParams mParams;
+
+    private:
+        // true if we successfully registered our LLEventAPI on construction
+        bool mRegistered;
+
+        // actually instantiate the companion LLEventAPI subclass
+        virtual LLEventPump* construct(const std::string& name) = 0;
+
+        // Passing an overloaded function to any function that accepts an
+        // arbitrary callable is a PITB because you have to specify the
+        // correct overload. What we want is for the compiler to select the
+        // correct overload, based on the carefully-wrought enable_ifs in
+        // LLEventDispatcher. This (one and only) add_trampoline() method
+        // exists solely to pass to LL::apply(). Once add_trampoline() is
+        // called with the expanded arguments, we hope the compiler will Do
+        // The Right Thing in selecting the correct LLEventAPI::add()
+        // overload.
+        template <typename... ARGS>
+        static
+        void add_trampoline(LLEventAPI* instance, ARGS&&... args)
+        {
+            instance->add(std::forward<ARGS>(args)...);
+        }
+    };
+
+    /**
+     * LazyEventAPI provides a way to register a particular LLEventAPI to be
+     * instantiated on demand, that is, when its name is passed to
+     * LLEventPumps::obtain().
+     *
+     * Derive your listener from LLEventAPI as usual, with its various
+     * operation methods, but code your constructor to accept
+     * <tt>(const LL::LazyEventAPIParams& params)</tt>
+     * and forward that reference to (the protected)
+     * <tt>LLEventAPI(const LL::LazyEventAPIParams&)</tt> constructor.
+     *
+     * Then derive your listener registrar from
+     * <tt>LazyEventAPI<your LLEventAPI subclass></tt>. The constructor should
+     * look very like a traditional LLEventAPI constructor:
+     *
+     * * pass (name, desc [, field]) to LazyEventAPI's constructor
+     * * in the body, make a series of add() calls referencing your LLEventAPI
+     *   subclass methods.
+     *
+     * You may use any LLEventAPI::add() methods, that is, any
+     * LLEventDispatcher::add() methods. But the target methods you pass to
+     * add() must belong to your LLEventAPI subclass, not the LazyEventAPI
+     * subclass.
+     *
+     * Declare a static instance of your LazyEventAPI listener registrar
+     * class. When it's constructed at static initialization time, it will
+     * register your LLEventAPI subclass with LLEventPumps. It will also
+     * collect metadata for the LLEventAPI and its operations to provide to
+     * LLLeapListener's introspection queries.
+     *
+     * When someone later calls LLEventPumps::obtain() to post an event to
+     * your LLEventAPI subclass, obtain() will instantiate it using
+     * LazyEventAPI's name, desc, field and add() calls.
+     */
+    template <class EVENTAPI>
+    class LazyEventAPI: public LazyEventAPIBase
+    {
+    public:
+        // for subclass constructor to reference handler methods
+        using listener = EVENTAPI;
+
+        LazyEventAPI(const std::string& name, const std::string& desc,
+                     const std::string& field="op"):
+            // Forward ctor params to LazyEventAPIBase
+            LazyEventAPIBase(name, desc, field)
+        {}
+
+    private:
+        LLEventPump* construct(const std::string& /*name*/) override
+        {
+            // base class has carefully assembled LazyEventAPIParams embedded
+            // in this instance, just pass to LLEventAPI subclass constructor
+            return new EVENTAPI(mParams);
+        }
+    };
+} // namespace LL
+
+#endif /* ! defined(LL_LAZYEVENTAPI_H) */
diff --git a/indra/llcommon/llapr.cpp b/indra/llcommon/llapr.cpp
index db4d8d5df381e8005b1476c5c1355e03ad73d9d0..383b36a6841cb1186ea82faa94f886049ab3e5f1 100644
--- a/indra/llcommon/llapr.cpp
+++ b/indra/llcommon/llapr.cpp
@@ -38,6 +38,12 @@ const S32 FULL_VOLATILE_APR_POOL = 1024 ; //number of references to LLVolatileAP
 
 bool gAPRInitialized = false;
 
+int abortfunc(int retcode)
+{
+    LL_WARNS("APR") << "Allocation failure in apr pool with code " << (S32)retcode << LL_ENDL;
+    return 0;
+}
+
 void ll_init_apr()
 {
 	// Initialize APR and create the global pool
@@ -45,7 +51,7 @@ void ll_init_apr()
 	
 	if (!gAPRPoolp)
 	{
-		apr_pool_create(&gAPRPoolp, NULL);
+		apr_pool_create_ex(&gAPRPoolp, NULL, abortfunc, NULL);
 	}
 
 	if(!LLAPRFile::sAPRFilePoolp)
diff --git a/indra/llcommon/llcoros.cpp b/indra/llcommon/llcoros.cpp
index e936646a956446654ebe7376a3d252c2a42bb396..0b651ae7e5d304d668a8e05ae00e4346876c5440 100644
--- a/indra/llcommon/llcoros.cpp
+++ b/indra/llcommon/llcoros.cpp
@@ -274,6 +274,7 @@ std::string LLCoros::launch(const std::string& prefix, const callable_t& callabl
     catch (std::bad_alloc&)
     {
         // Out of memory on stack allocation?
+        printActiveCoroutines();
         LL_ERRS("LLCoros") << "Bad memory allocation in LLCoros::launch(" << prefix << ")!" << LL_ENDL;
     }
 
diff --git a/indra/llcommon/lleventapi.cpp b/indra/llcommon/lleventapi.cpp
index eeaf9977c17a96282c95f7f8539b11bf50520110..0da73bdd5da6e69e4dd3c03dfdefb0b73daca67e 100644
--- a/indra/llcommon/lleventapi.cpp
+++ b/indra/llcommon/lleventapi.cpp
@@ -35,6 +35,7 @@
 // external library headers
 // other Linden headers
 #include "llerror.h"
+#include "lazyeventapi.h"
 
 LLEventAPI::LLEventAPI(const std::string& name, const std::string& desc, const std::string& field):
     lbase(name, field),
@@ -43,6 +44,13 @@ LLEventAPI::LLEventAPI(const std::string& name, const std::string& desc, const s
 {
 }
 
+LLEventAPI::LLEventAPI(const LL::LazyEventAPIParams& params):
+    LLEventAPI(params.name, params.desc, params.field)
+{
+    // call initialization functions with our brand-new instance pointer
+    params.init(this);
+}
+
 LLEventAPI::Response::Response(const LLSD& seed, const LLSD& request, const LLSD::String& replyKey):
     mResp(seed),
     mReq(request),
diff --git a/indra/llcommon/lleventapi.h b/indra/llcommon/lleventapi.h
index 08e3626091fa490c17d26455acca947c6ea07858..7019824546c26925f487c1935cbb1185737bb229 100644
--- a/indra/llcommon/lleventapi.h
+++ b/indra/llcommon/lleventapi.h
@@ -35,6 +35,11 @@
 #include "llinstancetracker.h"
 #include <string>
 
+namespace LL
+{
+    class LazyEventAPIParams;
+}
+
 /**
  * LLEventAPI not only provides operation dispatch functionality, inherited
  * from LLDispatchListener -- it also gives us event API introspection.
@@ -64,19 +69,6 @@ class LL_COMMON_API LLEventAPI: public LLDispatchListener,
     /// Get the documentation string
     std::string getDesc() const { return mDesc; }
 
-    /**
-     * Publish only selected add() methods from LLEventDispatcher.
-     * Every LLEventAPI add() @em must have a description string.
-     */
-    template <typename CALLABLE>
-    void add(const std::string& name,
-             const std::string& desc,
-             CALLABLE callable,
-             const LLSD& required=LLSD())
-    {
-        LLEventDispatcher::add(name, desc, callable, required);
-    }
-
     /**
      * Instantiate a Response object in any LLEventAPI subclass method that
      * wants to guarantee a reply (if requested) will be sent on exit from the
@@ -150,16 +142,20 @@ class LL_COMMON_API LLEventAPI: public LLDispatchListener,
          * @endcode
          */
         LLSD& operator[](const LLSD::String& key) { return mResp[key]; }
-		
-		 /**
-		 * set the response to the given data
-		 */
-		void setResponse(LLSD const & response){ mResp = response; }
+
+         /**
+         * set the response to the given data
+         */
+        void setResponse(LLSD const & response){ mResp = response; }
 
         LLSD mResp, mReq;
         LLSD::String mKey;
     };
 
+protected:
+    // constructor used only by subclasses registered by LazyEventAPI
+    LLEventAPI(const LL::LazyEventAPIParams&);
+
 private:
     std::string mDesc;
 };
diff --git a/indra/llcommon/lleventdispatcher.cpp b/indra/llcommon/lleventdispatcher.cpp
index 66989fae5fe0b7987d1855af48ca58ecc405e20c..40c344b504a7907de20655a8c09c58a70ad02146 100644
--- a/indra/llcommon/lleventdispatcher.cpp
+++ b/indra/llcommon/lleventdispatcher.cpp
@@ -40,70 +40,12 @@
 // other Linden headers
 #include "llevents.h"
 #include "llerror.h"
+#include "llexception.h"
 #include "llsdutil.h"
 #include "stringize.h"
+#include <iomanip>                  // std::quoted()
 #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
 *****************************************************************************/
@@ -156,19 +98,26 @@ void LLSDArgsSource::done() const
  * - Holes are filled with the default values.
  * - Any remaining holes constitute an error.
  */
-class LL_COMMON_API LLSDArgsMapper
+class LL_COMMON_API LLEventDispatcher::LLSDArgsMapper
 {
 public:
     /// Accept description of function: function name, param names, param
     /// default values
-    LLSDArgsMapper(const std::string& function, const LLSD& names, const LLSD& defaults);
+    LLSDArgsMapper(LLEventDispatcher* parent, const std::string& function,
+                   const LLSD& names, const LLSD& defaults);
 
-    /// Given arguments map, return LLSD::Array of parameter values, or LL_ERRS.
+    /// Given arguments map, return LLSD::Array of parameter values, or
+    /// trigger error.
     LLSD map(const LLSD& argsmap) const;
 
 private:
     static std::string formatlist(const LLSD&);
+    template <typename... ARGS>
+    [[noreturn]] void callFail(ARGS&&... args) const;
 
+    // store a plain dumb back-pointer because we don't have to manage the
+    // parent LLEventDispatcher's lifespan
+    LLEventDispatcher* _parent;
     // 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;
@@ -187,15 +136,18 @@ class LL_COMMON_API LLSDArgsMapper
     FilledVector _has_dft;
 };
 
-LLSDArgsMapper::LLSDArgsMapper(const std::string& function,
-                               const LLSD& names, const LLSD& defaults):
+LLEventDispatcher::LLSDArgsMapper::LLSDArgsMapper(LLEventDispatcher* parent,
+                                                  const std::string& function,
+                                                  const LLSD& names,
+                                                  const LLSD& defaults):
+    _parent(parent),
     _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;
+        callFail(" names must be an array, not ", names);
     }
     auto nparams(_names.size());
     // From _names generate _indexes.
@@ -218,8 +170,7 @@ LLSDArgsMapper::LLSDArgsMapper(const std::string& function,
         // 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;
+            callFail(" names array ", names, " shorter than defaults array ", defaults);
         }
 
         // Offset by which we slide defaults array right to right-align with
@@ -256,23 +207,20 @@ LLSDArgsMapper::LLSDArgsMapper(const std::string& function,
         }
         if (bogus.size())
         {
-            LL_ERRS("LLSDArgsMapper") << function << " defaults specified for nonexistent params "
-                                      << formatlist(bogus) << LL_ENDL;
+            callFail(" defaults specified for nonexistent params ", formatlist(bogus));
         }
     }
     else
     {
-        LL_ERRS("LLSDArgsMapper") << function << " defaults must be a map or an array, not "
-                                  << defaults << LL_ENDL;
+        callFail(" defaults must be a map or an array, not ", defaults);
     }
 }
 
-LLSD LLSDArgsMapper::map(const LLSD& argsmap) const
+LLSD LLEventDispatcher::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;
+        callFail(" map() needs a map or array, not ", argsmap);
     }
     // 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
@@ -369,15 +317,14 @@ LLSD LLSDArgsMapper::map(const LLSD& argsmap) const
     // by argsmap, that's a problem.
     if (unfilled.size())
     {
-        LL_ERRS("LLSDArgsMapper") << _function << " missing required arguments "
-                                  << formatlist(unfilled) << " from " << argsmap << LL_ENDL;
+        callFail(" missing required arguments ", formatlist(unfilled), " from ", argsmap);
     }
 
     // done
     return args;
 }
 
-std::string LLSDArgsMapper::formatlist(const LLSD& list)
+std::string LLEventDispatcher::LLSDArgsMapper::formatlist(const LLSD& list)
 {
     std::ostringstream out;
     const char* delim = "";
@@ -390,19 +337,40 @@ std::string LLSDArgsMapper::formatlist(const LLSD& list)
     return out.str();
 }
 
-LLEventDispatcher::LLEventDispatcher(const std::string& desc, const std::string& key):
-    mDesc(desc),
-    mKey(key)
+template <typename... ARGS>
+[[noreturn]] void LLEventDispatcher::LLSDArgsMapper::callFail(ARGS&&... args) const
 {
+    _parent->callFail<LLEventDispatcher::DispatchError>
+        (_function, std::forward<ARGS>(args)...);
 }
 
+/*****************************************************************************
+*   LLEventDispatcher
+*****************************************************************************/
+LLEventDispatcher::LLEventDispatcher(const std::string& desc, const std::string& key):
+    LLEventDispatcher(desc, key, "args")
+{}
+
+LLEventDispatcher::LLEventDispatcher(const std::string& desc, const std::string& key,
+                                     const std::string& argskey):
+    mDesc(desc),
+    mKey(key),
+    mArgskey(argskey)
+{}
+
+LLEventDispatcher::DispatchEntry::DispatchEntry(LLEventDispatcher* parent, const std::string& desc):
+    mParent(parent),
+    mDesc(desc)
+{}
+
 /**
  * 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),
+    LLSDDispatchEntry(LLEventDispatcher* parent, const std::string& desc,
+                      const Callable& func, const LLSD& required):
+        DispatchEntry(parent, desc),
         mFunc(func),
         mRequired(required)
     {}
@@ -410,22 +378,21 @@ struct LLEventDispatcher::LLSDDispatchEntry: public LLEventDispatcher::DispatchE
     Callable mFunc;
     LLSD mRequired;
 
-    virtual void call(const std::string& desc, const LLSD& event) const
+    LLSD call(const std::string& desc, const LLSD& event, bool, const std::string&) const override
     {
         // 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;
+            callFail(desc, ": bad request: ", mismatch);
         }
         // Event syntax looks good, go for it!
-        mFunc(event);
+        return mFunc(event);
     }
 
-    virtual LLSD addMetadata(LLSD meta) const
+    LLSD getMetadata() const override
     {
-        meta["required"] = mRequired;
-        return meta;
+        return llsd::map("required", mRequired);
     }
 };
 
@@ -435,17 +402,27 @@ struct LLEventDispatcher::LLSDDispatchEntry: public LLEventDispatcher::DispatchE
  */
 struct LLEventDispatcher::ParamsDispatchEntry: public LLEventDispatcher::DispatchEntry
 {
-    ParamsDispatchEntry(const std::string& desc, const invoker_function& func):
-        DispatchEntry(desc),
+    ParamsDispatchEntry(LLEventDispatcher* parent, const std::string& name,
+                        const std::string& desc, const invoker_function& func):
+        DispatchEntry(parent, desc),
+        mName(name),
         mInvoker(func)
     {}
 
+    std::string mName;
     invoker_function mInvoker;
 
-    virtual void call(const std::string& desc, const LLSD& event) const
+    LLSD call(const std::string&, const LLSD& event, bool, const std::string&) const override
     {
-        LLSDArgsSource src(desc, event);
-        mInvoker(boost::bind(&LLSDArgsSource::next, boost::ref(src)));
+        try
+        {
+            return mInvoker(event);
+        }
+        catch (const LL::apply_error& err)
+        {
+            // could hit runtime errors with LL::apply()
+            callFail(err.what());
+        }
     }
 };
 
@@ -455,23 +432,62 @@ struct LLEventDispatcher::ParamsDispatchEntry: public LLEventDispatcher::Dispatc
  */
 struct LLEventDispatcher::ArrayParamsDispatchEntry: public LLEventDispatcher::ParamsDispatchEntry
 {
-    ArrayParamsDispatchEntry(const std::string& desc, const invoker_function& func,
+    ArrayParamsDispatchEntry(LLEventDispatcher* parent, const std::string& name,
+                             const std::string& desc, const invoker_function& func,
                              LLSD::Integer arity):
-        ParamsDispatchEntry(desc, func),
+        ParamsDispatchEntry(parent, name, desc, func),
         mArity(arity)
     {}
 
     LLSD::Integer mArity;
 
-    virtual LLSD addMetadata(LLSD meta) const
+    LLSD call(const std::string& desc, const LLSD& event, bool fromMap, const std::string& argskey) const override
+    {
+//      std::string context { stringize(desc, "(", event, ") with argskey ", std::quoted(argskey), ": ") };
+        // Whether we try to extract arguments from 'event' depends on whether
+        // the LLEventDispatcher consumer called one of the (name, event)
+        // methods (! fromMap) or one of the (event) methods (fromMap). If we
+        // were called with (name, event), the passed event must itself be
+        // suitable to pass to the registered callable, no args extraction
+        // required or even attempted. Only if called with plain (event) do we
+        // consider extracting args from that event. Initially assume 'event'
+        // itself contains the arguments.
+        LLSD args{ event };
+        if (fromMap)
+        {
+            if (! mArity)
+            {
+                // When the target function is nullary, and we're called from
+                // an (event) method, just ignore the rest of the map entries.
+                args.clear();
+            }
+            else
+            {
+                // We only require/retrieve argskey if the target function
+                // isn't nullary. For all others, since we require an LLSD
+                // array, we must have an argskey.
+                if (argskey.empty())
+                {
+                    callFail("LLEventDispatcher has no args key");
+                }
+                if ((! event.has(argskey)))
+                {
+                    callFail("missing required key ", std::quoted(argskey));
+                }
+                args = event[argskey];
+            }
+        }
+        return ParamsDispatchEntry::call(desc, args, fromMap, argskey);
+    }
+
+    LLSD getMetadata() const override
     {
         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;
+        return llsd::map("required", array);
     }
 };
 
@@ -481,11 +497,11 @@ struct LLEventDispatcher::ArrayParamsDispatchEntry: public LLEventDispatcher::Pa
  */
 struct LLEventDispatcher::MapParamsDispatchEntry: public LLEventDispatcher::ParamsDispatchEntry
 {
-    MapParamsDispatchEntry(const std::string& name, const std::string& desc,
-                           const invoker_function& func,
+    MapParamsDispatchEntry(LLEventDispatcher* parent, 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),
+        ParamsDispatchEntry(parent, name, desc, func),
+        mMapper(parent, name, params, defaults),
         mRequired(LLSD::emptyMap())
     {
         // Build the set of all param keys, then delete the ones that are
@@ -528,18 +544,27 @@ struct LLEventDispatcher::MapParamsDispatchEntry: public LLEventDispatcher::Para
     LLSD mRequired;
     LLSD mOptional;
 
-    virtual void call(const std::string& desc, const LLSD& event) const
+    LLSD call(const std::string& desc, const LLSD& event, bool fromMap, const std::string& argskey) const override
     {
-        // Just convert from LLSD::Map to LLSD::Array using mMapper, then pass
-        // to base-class call() method.
-        ParamsDispatchEntry::call(desc, mMapper.map(event));
+        // by default, pass the whole event as the arguments map
+        LLSD args{ event };
+        // Were we called by one of the (event) methods (instead of the (name,
+        // event) methods), do we have an argskey, and does the incoming event
+        // have that key?
+        if (fromMap && (! argskey.empty()) && event.has(argskey))
+        {
+            // if so, extract the value of argskey from the incoming event,
+            // and use that as the arguments map
+            args = event[argskey];
+        }
+        // Now convert args from LLSD map to LLSD array using mMapper, then
+        // pass to base-class call() method.
+        return ParamsDispatchEntry::call(desc, mMapper.map(args), fromMap, argskey);
     }
 
-    virtual LLSD addMetadata(LLSD meta) const
+    LLSD getMetadata() const override
     {
-        meta["required"] = mRequired;
-        meta["optional"] = mOptional;
-        return meta;
+        return llsd::map("required", mRequired, "optional", mOptional);
     }
 };
 
@@ -548,9 +573,9 @@ void LLEventDispatcher::addArrayParamsDispatchEntry(const std::string& name,
                                                     const invoker_function& invoker,
                                                     LLSD::Integer arity)
 {
-    mDispatch.insert(
-        DispatchMap::value_type(name, std::static_pointer_cast<DispatchEntry>(
-                                    std::make_shared<ArrayParamsDispatchEntry>(desc, invoker, arity))));
+    mDispatch.emplace(
+        name,
+        new ArrayParamsDispatchEntry(this, "", desc, invoker, arity));
 }
 
 void LLEventDispatcher::addMapParamsDispatchEntry(const std::string& name,
@@ -559,25 +584,25 @@ void LLEventDispatcher::addMapParamsDispatchEntry(const std::string& name,
                                                   const LLSD& params,
                                                   const LLSD& defaults)
 {
-    mDispatch.insert(
-        DispatchMap::value_type(name, std::static_pointer_cast<DispatchEntry>(
-                                    std::make_shared<MapParamsDispatchEntry>(name, desc, invoker, params, defaults))));
+    // Pass instance info as well as this entry name for error messages.
+    mDispatch.emplace(
+        name,
+        new MapParamsDispatchEntry(this, "", 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)
+void LLEventDispatcher::addLLSD(const std::string& name, const std::string& desc,
+                                const Callable& callable, const LLSD& required)
 {
-    mDispatch.insert(
-        DispatchMap::value_type(name, std::static_pointer_cast<
-                                    DispatchEntry>(std::make_shared<LLSDDispatchEntry>(desc, callable, required))));
+    mDispatch.emplace(name, new LLSDDispatchEntry(this, desc, callable, required));
 }
 
-void LLEventDispatcher::addFail(const std::string& name, const std::string& classname) const
+void LLEventDispatcher::addFail(const std::string& name, const char* classname) const
 {
     LL_ERRS("LLEventDispatcher") << "LLEventDispatcher(" << mDesc << ")::add(" << name
-                                 << "): " << classname << " is not a subclass "
-                                 << "of LLEventDispatcher" << LL_ENDL;
+                                 << "): " << LLError::Log::demangle(classname)
+                                 << " is not a subclass of LLEventDispatcher"
+                                 << LL_ENDL;
 }
 
 /// Unregister a callable
@@ -592,48 +617,105 @@ bool LLEventDispatcher::remove(const std::string& name)
     return true;
 }
 
-/// Call a registered callable with an explicitly-specified name. If no
-/// such callable exists, die with LL_ERRS.
-void LLEventDispatcher::operator()(const std::string& name, const LLSD& event) const
+/// Call a registered callable with an explicitly-specified name. It is an
+/// error if no such callable exists.
+LLSD LLEventDispatcher::operator()(const std::string& name, const LLSD& event) const
 {
-    if (! try_call(name, event))
-    {
-        LL_ERRS("LLEventDispatcher") << "LLEventDispatcher(" << mDesc << "): '" << name
-                                     << "' not found" << LL_ENDL;
-    }
+    return try_call(std::string(), name, event);
 }
 
-/// 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.
-void LLEventDispatcher::operator()(const LLSD& event) const
+bool LLEventDispatcher::try_call(const std::string& name, 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 (! try_call(name, event))
+    try
     {
-        LL_ERRS("LLEventDispatcher") << "LLEventDispatcher(" << mDesc << "): bad " << mKey
-                                     << " value '" << name << "'" << LL_ENDL;
+        try_call(std::string(), name, event);
+        return true;
+    }
+    // Note that we don't catch the generic DispatchError, only the specific
+    // DispatchMissing. try_call() only promises to return false if the
+    // specified callable name isn't found -- not for general errors.
+    catch (const DispatchMissing&)
+    {
+        return false;
     }
 }
 
+/// Extract the @a key value from the incoming @a event, and call the callable
+/// whose name is specified by that map @a key. It is an error if no such
+/// callable exists.
+LLSD LLEventDispatcher::operator()(const LLSD& event) const
+{
+    return try_call(mKey, event[mKey], event);
+}
+
 bool LLEventDispatcher::try_call(const LLSD& event) const
 {
-    return try_call(event[mKey], event);
+    try
+    {
+        try_call(mKey, event[mKey], event);
+        return true;
+    }
+    catch (const DispatchMissing&)
+    {
+        return false;
+    }
 }
 
-bool LLEventDispatcher::try_call(const std::string& name, const LLSD& event) const
+LLSD LLEventDispatcher::try_call(const std::string& key, const std::string& name,
+                                 const LLSD& event) const
 {
+    if (name.empty())
+    {
+        if (key.empty())
+        {
+            callFail<DispatchError>("attempting to call with no name");
+        }
+        else
+        {
+            callFail<DispatchError>("no ", key);
+        }
+    }
+
     DispatchMap::const_iterator found = mDispatch.find(name);
     if (found == mDispatch.end())
     {
-        return false;
+        // Here we were passed a non-empty name, but there's no registered
+        // callable with that name. This is the one case in which we throw
+        // DispatchMissing instead of the generic DispatchError.
+        // Distinguish the public method by which our caller reached here:
+        // key.empty() means the name was passed explicitly, non-empty means
+        // we extracted the name from the incoming event using that key.
+        if (key.empty())
+        {
+            callFail<DispatchMissing>(std::quoted(name), " not found");
+        }
+        else
+        {
+            callFail<DispatchMissing>("bad ", key, " value ", std::quoted(name));
+        }
     }
+
     // 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
+    const char* delim = (key.empty()? "" : "=");
+    // append either "[key=name]" or just "[name]"
+    SetState transient(this, '[', key, delim, name, ']');
+    return found->second->call("", event, (! key.empty()), mArgskey);
+}
+
+template <typename EXCEPTION, typename... ARGS>
+//static
+[[noreturn]] void LLEventDispatcher::sCallFail(ARGS&&... args)
+{
+    auto error = stringize(std::forward<ARGS>(args)...);
+    LL_WARNS("LLEventDispatcher") << error << LL_ENDL;
+    LLTHROW(EXCEPTION(error));
+}
+
+template <typename EXCEPTION, typename... ARGS>
+[[noreturn]] void LLEventDispatcher::callFail(ARGS&&... args) const
+{
+    // Describe this instance in addition to the error itself.
+    sCallFail<EXCEPTION>(*this, ": ", std::forward<ARGS>(args)...);
 }
 
 LLSD LLEventDispatcher::getMetadata(const std::string& name) const
@@ -643,26 +725,243 @@ LLSD LLEventDispatcher::getMetadata(const std::string& name) const
     {
         return LLSD();
     }
-    LLSD meta;
+    LLSD meta{ found->second->getMetadata() };
     meta["name"] = name;
     meta["desc"] = found->second->mDesc;
-    return found->second->addMetadata(meta);
+    return meta;
+}
+
+std::ostream& operator<<(std::ostream& out, const LLEventDispatcher& self)
+{
+    // If we're a subclass of LLEventDispatcher, e.g. LLEventAPI, report that.
+    // Also report whatever transient state is active.
+    return out << LLError::Log::classname(self) << '(' << self.mDesc << ')'
+               << self.getState();
 }
 
-LLDispatchListener::LLDispatchListener(const std::string& pumpname, const std::string& key):
-    LLEventDispatcher(pumpname, key),
-    mPump(pumpname, true),          // allow tweaking for uniqueness
-    mBoundListener(mPump.listen("self", boost::bind(&LLDispatchListener::process, this, _1)))
+std::string LLEventDispatcher::getState() const
 {
+    // default value of fiber_specific_ptr is nullptr, and ~SetState() reverts
+    // to that; infer empty string
+    if (! mState.get())
+        return {};
+    else
+        return *mState;
 }
 
-bool LLDispatchListener::process(const LLSD& event)
+bool LLEventDispatcher::setState(SetState&, const std::string& state) const
 {
-    (*this)(event);
+    // If SetState is instantiated at multiple levels of function call, ignore 
+    // the lower-level call because the outer call presumably provides more
+    // context.
+    if (mState.get())
+        return false;
+
+    // Pass us empty string (a la ~SetState()) to reset to nullptr, else take
+    // a heap copy of the passed state string so we can delete it on
+    // subsequent reset().
+    mState.reset(state.empty()? nullptr : new std::string(state));
+    return true;
+}
+
+/*****************************************************************************
+*   LLDispatchListener
+*****************************************************************************/
+std::string LLDispatchListener::mReplyKey{ "reply" };
+
+bool LLDispatchListener::process(const LLSD& event) const
+{
+    // Decide what to do based on the incoming value of the specified dispatch
+    // key.
+    LLSD name{ event[getDispatchKey()] };
+    if (name.isMap())
+    {
+        call_map(name, event);
+    }
+    else if (name.isArray())
+    {
+        call_array(name, event);
+    }
+    else
+    {
+        call_one(name, event);
+    }
     return false;
 }
 
-LLEventDispatcher::DispatchEntry::DispatchEntry(const std::string& desc):
-    mDesc(desc)
-{}
+void LLDispatchListener::call_one(const LLSD& name, const LLSD& event) const
+{
+    LLSD result;
+    try
+    {
+        result = (*this)(event);
+    }
+    catch (const DispatchError& err)
+    {
+        if (! event.has(mReplyKey))
+        {
+            // Without a reply key, let the exception propagate.
+            throw;
+        }
+
+        // Here there was an error and the incoming event has mReplyKey. Reply
+        // with a map containing an "error" key explaining the problem.
+        return reply(llsd::map("error", err.what()), event);
+    }
 
+    // We seem to have gotten a valid result. But we don't know whether the
+    // registered callable is void or non-void. If it's void,
+    // LLEventDispatcher returned isUndefined(). Otherwise, try to send it
+    // back to our invoker.
+    if (result.isDefined())
+    {
+        if (! result.isMap())
+        {
+            // wrap the result in a map as the "data" key
+            result = llsd::map("data", result);
+        }
+        reply(result, event);
+    }
+}
+
+void LLDispatchListener::call_map(const LLSD& reqmap, const LLSD& event) const
+{
+    // LLSD map containing returned values
+    LLSD result;
+    // cache dispatch key
+    std::string key{ getDispatchKey() };
+    // collect any error messages here
+    std::ostringstream errors;
+    const char* delim = "";
+
+    for (const auto& pair : llsd::inMap(reqmap))
+    {
+        const LLSD::String& name{ pair.first };
+        const LLSD& args{ pair.second };
+        try
+        {
+            // in case of errors, tell user the dispatch key, the fact that
+            // we're processing a request map and the current key in that map
+            SetState(this, '[', key, '[', name, "]]");
+            // With this form, capture return value even if undefined:
+            // presence of the key in the response map can be used to detect
+            // which request keys succeeded.
+            result[name] = (*this)(name, args);
+        }
+        catch (const std::exception& err)
+        {
+            // Catch not only DispatchError, but any C++ exception thrown by
+            // the target callable. Collect exception name and message in
+            // 'errors'.
+            errors << delim << LLError::Log::classname(err) << ": " << err.what();
+            delim = "\n";
+        }
+    }
+
+    // so, were there any errors?
+    std::string error = errors.str();
+    if (! error.empty())
+    {
+        if (! event.has(mReplyKey))
+        {
+            // can't send reply, throw
+            sCallFail<DispatchError>(error);
+        }
+        else
+        {
+            // reply key present
+            result["error"] = error;
+        }
+    }
+
+    reply(result, event);
+}
+
+void LLDispatchListener::call_array(const LLSD& reqarray, const LLSD& event) const
+{
+    // LLSD array containing returned values
+    LLSD results;
+    // cache the dispatch key
+    std::string key{ getDispatchKey() };
+    // arguments array, if present -- const because, if it's shorter than
+    // reqarray, we don't want to grow it
+    const LLSD argsarray{ event[getArgsKey()] };
+    // error message, if any
+    std::string error;
+
+    // classic index loop because we need the index
+    for (size_t i = 0, size = reqarray.size(); i < size; ++i)
+    {
+        const auto& reqentry{ reqarray[i] };
+        std::string name;
+        LLSD args;
+        if (reqentry.isString())
+        {
+            name = reqentry.asString();
+            args = argsarray[i];
+        }
+        else if (reqentry.isArray() && reqentry.size() == 2 && reqentry[0].isString())
+        {
+            name = reqentry[0].asString();
+            args = reqentry[1];
+        }
+        else
+        {
+            // reqentry isn't in either of the documented forms
+            error = stringize(*this, ": ", getDispatchKey(), '[', i, "] ",
+                              reqentry, " unsupported");
+            break;
+        }
+
+        // reqentry is one of the valid forms, got name and args
+        try
+        {
+            // in case of errors, tell user the dispatch key, the fact that
+            // we're processing a request array, the current entry in that
+            // array and the corresponding callable name
+            SetState(this, '[', key, '[', i, "]=", name, ']');
+            // With this form, capture return value even if undefined
+            results.append((*this)(name, args));
+        }
+        catch (const std::exception& err)
+        {
+            // Catch not only DispatchError, but any C++ exception thrown by
+            // the target callable. Report the exception class as well as the
+            // error string.
+            error = stringize(LLError::Log::classname(err), ": ", err.what());
+            break;
+        }
+    }
+
+    LLSD result;
+    // was there an error?
+    if (! error.empty())
+    {
+        if (! event.has(mReplyKey))
+        {
+            // can't send reply, throw
+            sCallFail<DispatchError>(error);
+        }
+        else
+        {
+            // reply key present
+            result["error"] = error;
+        }
+    }
+
+    // wrap the results array as response map "data" key, as promised
+    if (results.isDefined())
+    {
+        result["data"] = results;
+    }
+
+    reply(result, event);
+}
+
+void LLDispatchListener::reply(const LLSD& reply, const LLSD& request) const
+{
+    // Call sendReply() unconditionally: sendReply() itself tests whether the
+    // specified reply key is present in the incoming request, and does
+    // nothing if there's no such key.
+    sendReply(reply, request, mReplyKey);
+}
diff --git a/indra/llcommon/lleventdispatcher.h b/indra/llcommon/lleventdispatcher.h
index b8d21245d7c2d0f28809699b7367df4587724182..041c12f981ec28e90dabeb60622cc0ee718cf937 100644
--- a/indra/llcommon/lleventdispatcher.h
+++ b/indra/llcommon/lleventdispatcher.h
@@ -27,55 +27,26 @@
  * 
  * 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 auto 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 auto& nil(nil_);
-#endif
-
-#include <string>
-#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/fiber/fss.hpp>
+#include <boost/function_types/is_member_function_pointer.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 <boost/hof/is_invocable.hpp> // until C++17, when we get std::is_invocable
+#include <boost/iterator/transform_iterator.hpp>
+#include <functional>               // std::function
+#include <memory>                   // std::unique_ptr
+#include <string>
 #include <typeinfo>
+#include <type_traits>
+#include <utility>                  // std::pair
+#include "always_return.h"
+#include "function_types.h"         // LL::function_arity
 #include "llevents.h"
+#include "llptrto.h"
 #include "llsdutil.h"
 
 class LLSD;
@@ -89,15 +60,27 @@ class LLSD;
 class LL_COMMON_API LLEventDispatcher
 {
 public:
+    /**
+     * Pass description and the LLSD key used by try_call(const LLSD&) and
+     * operator()(const LLSD&) to extract the name of the registered callable
+     * to invoke.
+     */
     LLEventDispatcher(const std::string& desc, const std::string& key);
+    /**
+     * Pass description, the LLSD key used by try_call(const LLSD&) and
+     * operator()(const LLSD&) to extract the name of the registered callable
+     * to invoke, and the LLSD key used by try_call(const LLSD&) and
+     * operator()(const LLSD&) to extract arguments LLSD.
+     */
+    LLEventDispatcher(const std::string& desc, const std::string& key,
+                      const std::string& argskey);
     virtual ~LLEventDispatcher() = default;
 
     /// @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;
+    /// Accept any C++ callable with the right signature
+    typedef std::function<LLSD(const LLSD&)> Callable;
 
     /**
      * Register a @a callable by @a name. The passed @a callable accepts a
@@ -109,27 +92,54 @@ class LL_COMMON_API LLEventDispatcher
     void add(const std::string& name,
              const std::string& desc,
              const Callable& callable,
-             const LLSD& required=LLSD());
+             const LLSD& required=LLSD())
+    {
+        addLLSD(name, desc, callable, required);
+    }
 
-    /**
-     * 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.
-     */
+    template <typename CALLABLE,
+              typename=typename std::enable_if<
+                  boost::hof::is_invocable<CALLABLE, LLSD>::value
+             >::type>
     void add(const std::string& name,
              const std::string& desc,
-             void (*f)(const LLSD&),
+             CALLABLE&& callable,
              const LLSD& required=LLSD())
     {
-        add(name, desc, Callable(f), required);
+        addLLSD(
+            name,
+            desc,
+            Callable(LL::make_always_return<LLSD>(std::forward<CALLABLE>(callable))),
+            required);
     }
 
     /**
      * Special case: a subclass of this class can pass an unbound member
      * function pointer (of an LLEventDispatcher subclass) without explicitly
-     * specifying the <tt>boost::bind()</tt> expression. The passed @a method
+     * specifying a <tt>std::bind()</tt> expression. The passed @a method
      * accepts a single LLSD value, presumably containing other parameters.
      */
+    template <typename R, class CLASS>
+    void add(const std::string& name,
+             const std::string& desc,
+             R (CLASS::*method)(const LLSD&),
+             const LLSD& required=LLSD())
+    {
+        addMethod<CLASS>(name, desc, method, required);
+    }
+
+    /// Overload for both const and non-const methods. The passed @a method
+    /// accepts a single LLSD value, presumably containing other parameters.
+    template <typename R, class CLASS>
+    void add(const std::string& name,
+             const std::string& desc,
+             R (CLASS::*method)(const LLSD&) const,
+             const LLSD& required=LLSD())
+    {
+        addMethod<CLASS>(name, desc, method, required);
+    }
+
+    // because the compiler can't match a method returning void to the above
     template <class CLASS>
     void add(const std::string& name,
              const std::string& desc,
@@ -150,6 +160,128 @@ class LL_COMMON_API LLEventDispatcher
         addMethod<CLASS>(name, desc, method, required);
     }
 
+    // non-const nullary method
+    template <typename R, class CLASS>
+    void add(const std::string& name,
+             const std::string& desc,
+             R (CLASS::*method)())
+    {
+        addVMethod<CLASS>(name, desc, method);
+    }
+
+    // const nullary method
+    template <typename R, class CLASS>
+    void add(const std::string& name,
+             const std::string& desc,
+             R (CLASS::*method)() const)
+    {
+        addVMethod<CLASS>(name, desc, method);
+    }
+
+    // non-const nullary method returning void
+    template <class CLASS>
+    void add(const std::string& name,
+             const std::string& desc,
+             void (CLASS::*method)())
+    {
+        addVMethod<CLASS>(name, desc, method);
+    }
+
+    // const nullary method returning void
+    template <class CLASS>
+    void add(const std::string& name,
+             const std::string& desc,
+             void (CLASS::*method)() const)
+    {
+        addVMethod<CLASS>(name, desc, method);
+    }
+
+    // non-const unary method (but method accepting LLSD should use the other add())
+    // enable_if usage per https://stackoverflow.com/a/39913395/5533635
+    template <typename R, class CLASS, typename ARG,
+              typename = typename std::enable_if<
+                  ! std::is_same<typename std::decay<ARG>::type, LLSD>::value
+             >::type>    
+    void add(const std::string& name,
+             const std::string& desc,
+             R (CLASS::*method)(ARG))
+    {
+        addVMethod<CLASS>(name, desc, method);
+    }
+
+    // const unary method (but method accepting LLSD should use the other add())
+    template <typename R, class CLASS, typename ARG,
+              typename = typename std::enable_if<
+                  ! std::is_same<typename std::decay<ARG>::type, LLSD>::value
+             >::type>    
+    void add(const std::string& name,
+             const std::string& desc,
+             R (CLASS::*method)(ARG) const)
+    {
+        addVMethod<CLASS>(name, desc, method);
+    }
+
+    // non-const unary method returning void
+    // enable_if usage per https://stackoverflow.com/a/39913395/5533635
+    template <class CLASS, typename ARG,
+              typename = typename std::enable_if<
+                  ! std::is_same<typename std::decay<ARG>::type, LLSD>::value
+             >::type>    
+    void add(const std::string& name,
+             const std::string& desc,
+             void (CLASS::*method)(ARG))
+    {
+        addVMethod<CLASS>(name, desc, method);
+    }
+
+    // const unary method returning void
+    template <class CLASS, typename ARG,
+              typename = typename std::enable_if<
+                  ! std::is_same<typename std::decay<ARG>::type, LLSD>::value
+             >::type>    
+    void add(const std::string& name,
+             const std::string& desc,
+             void (CLASS::*method)(ARG) const)
+    {
+        addVMethod<CLASS>(name, desc, method);
+    }
+
+    // non-const binary (or more) method
+    template <typename R, class CLASS, typename ARG0, typename ARG1, typename... ARGS>    
+    void add(const std::string& name,
+             const std::string& desc,
+             R (CLASS::*method)(ARG0, ARG1, ARGS...))
+    {
+        addVMethod<CLASS>(name, desc, method);
+    }
+
+    // const binary (or more) method
+    template <typename R, class CLASS, typename ARG0, typename ARG1, typename... ARGS>    
+    void add(const std::string& name,
+             const std::string& desc,
+             R (CLASS::*method)(ARG0, ARG1, ARGS...) const)
+    {
+        addVMethod<CLASS>(name, desc, method);
+    }
+
+    // non-const binary (or more) method returning void
+    template <class CLASS, typename ARG0, typename ARG1, typename... ARGS>    
+    void add(const std::string& name,
+             const std::string& desc,
+             void (CLASS::*method)(ARG0, ARG1, ARGS...))
+    {
+        addVMethod<CLASS>(name, desc, method);
+    }
+
+    // const binary (or more) method returning void
+    template <class CLASS, typename ARG0, typename ARG1, typename... ARGS>    
+    void add(const std::string& name,
+             const std::string& desc,
+             void (CLASS::*method)(ARG0, ARG1, ARGS...) const)
+    {
+        addVMethod<CLASS>(name, desc, method);
+    }
+
     //@}
 
     /// @name Register functions with arbitrary param lists
@@ -159,51 +291,43 @@ class LL_COMMON_API LLEventDispatcher
      * 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);
+    template <typename CALLABLE,
+              typename=typename std::enable_if<
+                  ! boost::hof::is_invocable<CALLABLE, LLSD>()
+             >::type>
+    void add(const std::string& name,
+             const std::string& desc,
+             CALLABLE&& f)
+    {
+        addV(name, desc, 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.
+     * instance.
      *
      * 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);
+    template<typename Method, typename InstanceGetter,
+             typename = typename std::enable_if<
+                 boost::function_types::is_member_function_pointer<Method>::value &&
+                 ! std::is_convertible<InstanceGetter, LLSD>::value
+             >::type>
+    void 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.
      *
@@ -211,21 +335,17 @@ class LL_COMMON_API LLEventDispatcher
      * 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());
+    template<typename Function,
+             typename = typename std::enable_if<
+                 boost::function_types::is_nonmember_callable_builtin<Function>::value &&
+                 ! boost::hof::is_invocable<Function, LLSD>::value
+             >::type>
+    void 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
@@ -233,6 +353,8 @@ class LL_COMMON_API LLEventDispatcher
      * boost::lambda::var(instance) or boost::lambda::constant(instance_ptr)
      * produce suitable callables.
      *
+     * TODO: variant accepting a method of the containing class, no getter.
+     *
      * Pass an LLSD::Array of parameter names, and optionally another
      * LLSD::Array of default parameter values, a la LLSDArgsMapper.
      *
@@ -240,42 +362,96 @@ class LL_COMMON_API LLEventDispatcher
      * 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());
+    template<typename Method, typename InstanceGetter,
+             typename = typename std::enable_if<
+                 boost::function_types::is_member_function_pointer<Method>::value &&
+                 ! std::is_convertible<InstanceGetter, LLSD>::value
+             >::type>
+    void 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);
 
-    /// Call a registered callable with an explicitly-specified name. 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 std::string& name, const LLSD& event) const;
+    /// Exception if an attempted call fails for any reason
+    struct DispatchError: public LLException
+    {
+        DispatchError(const std::string& what): LLException(what) {}
+    };
+
+    /// Specific exception for an attempt to call a nonexistent name
+    struct DispatchMissing: public DispatchError
+    {
+        DispatchMissing(const std::string& what): DispatchError(what) {}
+    };
+
+    /**
+     * Call a registered callable with an explicitly-specified name,
+     * converting its return value to LLSD (undefined for a void callable).
+     * It is an error if no such callable exists. It is an error if the @a
+     * event fails to match the @a required prototype specified at add()
+     * time.
+     *
+     * @a event must be an LLSD array for a callable registered to accept its
+     * arguments from such an array. It must be an LLSD map for a callable
+     * registered to accept its arguments from such a map.
+     */
+    LLSD 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.
+    /**
+     * Call a registered callable with an explicitly-specified name and
+     * return <tt>true</tt>. If no such callable exists, return
+     * <tt>false</tt>. It is an error if the @a event fails to match the @a
+     * required prototype specified at add() time.
+     *
+     * @a event must be an LLSD array for a callable registered to accept its
+     * arguments from such an array. It must be an LLSD map for a callable
+     * registered to accept its arguments from such a map.
+     */
     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.
+    /**
+     * Extract the @a key specified to our constructor from the incoming LLSD
+     * map @a event, and call the callable whose name is specified by that @a
+     * key's value, converting its return value to LLSD (undefined for a void
+     * callable). It is an error if no such callable exists. It is an error if
+     * the @a event fails to match the @a required prototype specified at
+     * add() time.
+     *
+     * For a (non-nullary) callable registered to accept its arguments from an
+     * LLSD array, the @a event map must contain the key @a argskey specified to
+     * our constructor. The value of the @a argskey key must be an LLSD array
+     * containing the arguments to pass to the callable named by @a key.
+     *
+     * For a callable registered to accept its arguments from an LLSD map, if
+     * the @a event map contains the key @a argskey specified our constructor,
+     * extract the value of the @a argskey key and use it as the arguments map.
+     * If @a event contains no @a argskey key, use the whole @a event as the
+     * arguments map.
+     */
+    LLSD operator()(const LLSD& event) const;
+
+    /**
+     * Extract the @a key specified to our constructor from the incoming LLSD
+     * map @a event, call the callable whose name is specified by that @a
+     * key's value and return <tt>true</tt>. If no such callable exists,
+     * return <tt>false</tt>. It is an error if the @a event fails to match
+     * the @a required prototype specified at add() time.
+     *
+     * For a (non-nullary) callable registered to accept its arguments from an
+     * LLSD array, the @a event map must contain the key @a argskey specified to
+     * our constructor. The value of the @a argskey key must be an LLSD array
+     * containing the arguments to pass to the callable named by @a key.
+     *
+     * For a callable registered to accept its arguments from an LLSD map, if
+     * the @a event map contains the key @a argskey specified our constructor,
+     * extract the value of the @a argskey key and use it as the arguments map.
+     * If @a event contains no @a argskey key, use the whole @a event as the
+     * arguments map.
+     */
     bool try_call(const LLSD& event) const;
 
     /// @name Iterate over defined names
@@ -285,22 +461,26 @@ class LL_COMMON_API LLEventDispatcher
 private:
     struct DispatchEntry
     {
-        DispatchEntry(const std::string& desc);
+        DispatchEntry(LLEventDispatcher* parent, const std::string& desc);
         virtual ~DispatchEntry()  = default; // suppress MSVC warning, sigh
 
+        // store a plain dumb back-pointer because the parent
+        // LLEventDispatcher manages the lifespan of each DispatchEntry
+        // subclass instance -- not the other way around
+        LLEventDispatcher* mParent;
         std::string mDesc;
 
-        virtual void call(const std::string& desc, const LLSD& event) const = 0;
-        virtual LLSD addMetadata(LLSD) const = 0;
+        virtual LLSD call(const std::string& desc, const LLSD& event,
+                          bool fromMap, const std::string& argskey) const = 0;
+        virtual LLSD getMetadata() const = 0;
+
+        template <typename... ARGS>
+        [[noreturn]] void callFail(ARGS&&... args) const
+        {
+            mParent->callFail<LLEventDispatcher::DispatchError>(std::forward<ARGS>(args)...);
+        }
     };
-    // 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, std::shared_ptr<DispatchEntry> > DispatchMap;
+    typedef std::map<std::string, std::unique_ptr<DispatchEntry> > DispatchMap;
 
 public:
     /// We want the flexibility to redefine what data we store per name,
@@ -323,11 +503,57 @@ class LL_COMMON_API LLEventDispatcher
 
     /// Retrieve the LLSD key we use for one-arg <tt>operator()</tt> method
     std::string getDispatchKey() const { return mKey; }
+    /// Retrieve the LLSD key we use for non-map arguments
+    std::string getArgsKey() const { return mArgskey; }
+
+    /// description of this instance's leaf class and description
+    friend std::ostream& operator<<(std::ostream&, const LLEventDispatcher&);
 
 private:
-    template <class CLASS, typename METHOD>
+    void addLLSD(const std::string& name,
+                 const std::string& desc,
+                 const Callable& callable,
+                 const LLSD& required);
+
+    template <class CLASS, typename METHOD,
+              typename std::enable_if<
+                  std::is_base_of<LLEventDispatcher, CLASS>::value,
+                  bool
+              >::type=true>
     void addMethod(const std::string& name, const std::string& desc,
                    const METHOD& method, const LLSD& required)
+    {
+        // Why two overloaded addMethod() methods, discriminated with
+        // std::is_base_of? It might seem simpler to use dynamic_cast and test
+        // for nullptr. The trouble is that it doesn't work for LazyEventAPI
+        // deferred registration: we get nullptr even for a method of an
+        // LLEventAPI subclass.
+        CLASS* downcast = static_cast<CLASS*>(this);
+        add(name,
+            desc,
+            Callable(LL::make_always_return<LLSD>(
+                         [downcast, method]
+                         (const LLSD& args)
+                         {
+                             return (downcast->*method)(args);
+                         })),
+            required);
+    }
+
+    template <class CLASS, typename METHOD,
+              typename std::enable_if<
+                  ! std::is_base_of<LLEventDispatcher, CLASS>::value,
+                  bool
+              >::type=true>
+    void addMethod(const std::string& name, const std::string& desc,
+                   const METHOD&, const LLSD&)
+    {
+        addFail(name, typeid(CLASS).name());
+    }
+
+    template <class CLASS, typename METHOD>
+    void addVMethod(const std::string& name, const std::string& desc,
+                    const METHOD& method)
     {
         CLASS* downcast = dynamic_cast<CLASS*>(this);
         if (! downcast)
@@ -336,38 +562,85 @@ class LL_COMMON_API LLEventDispatcher
         }
         else
         {
-            add(name, desc, boost::bind(method, downcast, _1), required);
+            // add() arbitrary method plus InstanceGetter, where the
+            // InstanceGetter in this case returns 'this'. We don't need to
+            // worry about binding 'this' because, once this LLEventDispatcher
+            // is destroyed, the DispatchEntry goes away too.
+            add(name, desc, method, [downcast](){ return downcast; });
         }
     }
-    void addFail(const std::string& name, const std::string& classname) const;
 
-    std::string mDesc, mKey;
+    template <typename Function>
+    void addV(const std::string& name, const std::string& desc, Function f);
+
+    void addFail(const std::string& name, const char* classname) const;
+    LLSD try_call(const std::string& key, const std::string& name,
+                  const LLSD& event) const;
+
+protected:
+    // raise specified EXCEPTION with specified stringize(ARGS)
+    template <typename EXCEPTION, typename... ARGS>
+    [[noreturn]] void callFail(ARGS&&... args) const;
+    template <typename EXCEPTION, typename... ARGS>
+    [[noreturn]] static
+    void sCallFail(ARGS&&... args);
+
+    // Manage transient state, e.g. which registered callable we're attempting
+    // to call, for error reporting
+    class SetState
+    {
+    public:
+        template <typename... ARGS>
+        SetState(const LLEventDispatcher* self, ARGS&&... args):
+            mSelf(self)
+        {
+            mSet = mSelf->setState(*this, stringize(std::forward<ARGS>(args)...));
+        }
+        // RAII class: forbid both copy and move
+        SetState(const SetState&) = delete;
+        SetState(SetState&&) = delete;
+        SetState& operator=(const SetState&) = delete;
+        SetState& operator=(SetState&&) = delete;
+        virtual ~SetState()
+        {
+            // if we're the ones who succeeded in setting state, clear it
+            if (mSet)
+            {
+                mSelf->setState(*this, {});
+            }
+        }
+
+    private:
+        const LLEventDispatcher* mSelf;
+        bool mSet;
+    };
+
+private:
+    std::string mDesc, mKey, mArgskey;
     DispatchMap mDispatch;
+    // transient state: must be fiber_specific since multiple threads and/or
+    // multiple fibers may be calling concurrently. Make it mutable so we can
+    // use SetState even within const methods.
+    mutable boost::fibers::fiber_specific_ptr<std::string> mState;
+
+    std::string getState() const;
+    // setState() requires SetState& because only the SetState class should
+    // call it. Make it const so we can use SetState even within const methods.
+    bool setState(SetState&, const std::string& state) const;
 
     static NameDesc makeNameDesc(const DispatchMap::value_type& item)
     {
         return NameDesc(item.first, item.second->mDesc);
     }
 
+    class LLSDArgsMapper;
     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;
+    // call target function with args from LLSD array
+    typedef std::function<LLSD(const LLSD&)> invoker_function;
 
     template <typename Function>
     invoker_function make_invoker(Function f);
@@ -387,101 +660,38 @@ class LL_COMMON_API LLEventDispatcher
 /*****************************************************************************
 *   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)
+template <typename Function>
+void LLEventDispatcher::addV(const std::string& name, const std::string& desc, Function f)
 {
-    // Construct an invoker_function, a callable accepting const args_source&.
+    // Construct an invoker_function, a callable accepting const LLSD&.
     // 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);
+                                LL::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)
+template<typename Method, typename InstanceGetter, typename>
+void 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);
+                                LL::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)
+template<typename Function, typename>
+void 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)
+template<typename Method, typename InstanceGetter, typename>
+void 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);
 }
@@ -490,29 +700,45 @@ 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());
+    // Return an invoker_function that accepts (const LLSD& args).
+    return [f](const LLSD& args)
+    {
+        // When called, call always_return<LLSD>, directing it to call
+        // f(expanded args). always_return<LLSD> guarantees we'll get an LLSD
+        // value back, even if it's undefined because 'f' doesn't return a
+        // type convertible to LLSD.
+        return LL::always_return<LLSD>(
+            [f, args]
+            ()
+            {
+                return LL::apply(f, args);
+            });
+    };
 }
 
 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);
+    return [f, getter](const LLSD& args)
+    {
+        // always_return<LLSD>() immediately calls the lambda we pass, and
+        // returns LLSD whether our passed lambda returns void or non-void.
+        return LL::always_return<LLSD>(
+            [f, getter, args]
+            ()
+            {
+                // function_arity<member function> includes its implicit 'this' pointer
+                constexpr auto arity = LL::function_arity<
+                    typename std::remove_reference<Method>::type>::value - 1;
+
+                // Use bind_front() to bind the method to (a pointer to) the object
+                // returned by getter(). It's okay to capture and bind a pointer
+                // because this bind_front() object will last only as long as this
+                // lambda call.
+                return LL::apply_n<arity>(LL::bind_front(f, LL::get_ptr(getter())), args);
+            });
+    };
 }
 
 /*****************************************************************************
@@ -521,21 +747,138 @@ LLEventDispatcher::make_invoker(Method f, const InstanceGetter& getter)
 /**
  * Bundle an LLEventPump and a listener with an LLEventDispatcher. A class
  * that contains (or derives from) LLDispatchListener need only specify the
- * LLEventPump name and dispatch key, and add() its methods. Incoming events
- * will automatically be dispatched.
+ * LLEventPump name and dispatch key, and add() its methods. Each incoming
+ * event ("request") will automatically be dispatched.
+ *
+ * If the request contains a "reply" key specifying the LLSD::String name of
+ * an LLEventPump to which to respond, LLDispatchListener will attempt to send
+ * a response to that LLEventPump.
+ *
+ * If some error occurs (e.g. nonexistent callable name, wrong params) and
+ * "reply" is present, LLDispatchListener will send a response map to the
+ * specified LLEventPump containing an "error" key whose value is the relevant
+ * error message. If "reply" is not present, the DispatchError exception will
+ * propagate. Since LLDispatchListener bundles an LLEventStream, which
+ * attempts the call immediately on receiving the post() call, there's a
+ * reasonable chance that the exception will highlight the post() call that
+ * triggered the error.
+ *
+ * If LLDispatchListener successfully calls the target callable, but no
+ * "reply" key is present, any value returned by that callable is discarded.
+ * If a "reply" key is present, but the target callable is void -- or it
+ * returns LLSD::isUndefined() -- no response is sent. If a void callable
+ * wants to send a response, it must do so explicitly.
+ *
+ * If the target callable returns a type convertible to LLSD (and, if it
+ * directly returns LLSD, the return value isDefined()), and if a "reply" key
+ * is present in the request, LLDispatchListener will post the returned value
+ * to the "reply" LLEventPump. If the returned value is an LLSD map, it will
+ * merge the echoed "reqid" key into the map and send that. Otherwise, it will
+ * send an LLSD map containing "reqid" and a "data" key whose value is the
+ * value returned by the target callable.
+ *
+ * (It is inadvisable for a target callable to return an LLSD map containing
+ * keys "data", "reqid" or "error", as that will confuse the invoker.)
+ *
+ * Normally the request will specify the value of the dispatch key as an
+ * LLSD::String naming the target callable. Alternatively, several such calls
+ * may be "batched" as described below.
+ *
+ * If the value of the dispatch key is itself an LLSD map (a "request map"),
+ * each map key must name a target callable, and the value of that key must
+ * contain the parameters to pass to that callable. If a "reply" key is
+ * present in the request, the response map will contain a key for each of the
+ * keys in the request map. The value of every such key is the value returned
+ * by the target callable.
+ *
+ * (Avoid naming any target callable in the LLDispatchListener "data", "reqid"
+ * or "error" to avoid confusion.)
+ *
+ * Since LLDispatchListener calls the target callables specified by a request
+ * map in arbitrary order, this form assumes that the batched operations are
+ * independent of each other. LLDispatchListener will attempt every call, even
+ * if some attempts produce errors. If any keys in the request map produce
+ * errors, LLDispatchListener builds a composite error message string
+ * collecting the relevant messages. The corresponding keys will be missing
+ * from the response map. As in the single-callable case, absent a "reply" key
+ * in the request, this error message will be thrown as a DispatchError. With
+ * a "reply" key, it will be returned as the value of the "error" key. This
+ * form can indicate partial success: some request keys might have
+ * return-value keys in the response, others might have message text in the
+ * "error" key.
+ *
+ * If a specific call sequence is required, the value of the dispatch key may
+ * instead be an LLSD array (a "request array"). Each entry in the request
+ * array ("request entry") names a target callable, to be called in
+ * array-index sequence. Arguments for that callable may be specified in
+ * either of two ways.
+ *
+ * The request entry may itself be a two-element array, whose [0] is an
+ * LLSD::String naming the target callable and whose [1] contains the
+ * arguments to pass to that callable.
+ *
+ * Alternatively, the request entry may be an LLSD::String naming the target
+ * callable, in which case the request must contain an arguments key (optional
+ * third constructor argument) whose value is an array matching the request
+ * array. The arguments for the request entry's target callable are found at
+ * the same index in the arguments key array.
+ *
+ * If a "reply" key is present in the request, the response map will contain a
+ * "data" key whose value is an array. Each entry in that response array will
+ * contain the result from the corresponding request entry.
+ *
+ * This form assumes that any of the batched operations might depend on the
+ * success of a previous operation in the same batch. The @emph first error
+ * encountered will terminate the sequence. The error message might either be
+ * thrown as DispatchError or, given a "reply" key, returned as the "error"
+ * key in the response map. This form can indicate partial success: the first
+ * few request entries might have return-value entries in the "data" response
+ * array, along with an "error" key whose value is the error message that
+ * stopped the sequence.
  */
-class LL_COMMON_API LLDispatchListener: public LLEventDispatcher
+// Instead of containing an LLEventStream, LLDispatchListener derives from it.
+// This allows an LLEventPumps::PumpFactory to return a pointer to an
+// LLDispatchListener (subclass) instance, and still have ~LLEventPumps()
+// properly clean it up.
+class LL_COMMON_API LLDispatchListener:
+    public LLEventDispatcher,
+    public LLEventStream
 {
 public:
-    LLDispatchListener(const std::string& pumpname, const std::string& key);
-
-    std::string getPumpName() const { return mPump.getName(); }
+    /// LLEventPump name, dispatch key [, arguments key (see LLEventDispatcher)]
+    template <typename... ARGS>
+    LLDispatchListener(const std::string& pumpname, const std::string& key,
+                       ARGS&&... args);
+    virtual ~LLDispatchListener() {}
 
 private:
-    bool process(const LLSD& event);
+    bool process(const LLSD& event) const;
+    void call_one(const LLSD& name, const LLSD& event) const;
+    void call_map(const LLSD& reqmap, const LLSD& event) const;
+    void call_array(const LLSD& reqarray, const LLSD& event) const;
+    void reply(const LLSD& reply, const LLSD& request) const;
 
-    LLEventStream mPump;
     LLTempBoundListener mBoundListener;
+    static std::string mReplyKey;
 };
 
+template <typename... ARGS>
+LLDispatchListener::LLDispatchListener(const std::string& pumpname, const std::string& key,
+                                       ARGS&&... args):
+    // pass through any additional arguments to LLEventDispatcher ctor
+    LLEventDispatcher(pumpname, key, std::forward<ARGS>(args)...),
+    // Do NOT tweak the passed pumpname. In practice, when someone
+    // instantiates a subclass of our LLEventAPI subclass, they intend to
+    // claim that LLEventPump name in the global LLEventPumps namespace. It
+    // would be mysterious and distressing if we allowed name tweaking, and
+    // someone else claimed pumpname first for a completely unrelated
+    // LLEventPump. Posted events would never reach our subclass listener
+    // because we would have silently changed its name; meanwhile listeners
+    // (if any) on that other LLEventPump would be confused by the events
+    // intended for our subclass.
+    LLEventStream(pumpname, false),
+    mBoundListener(listen("self", [this](const LLSD& event){ return process(event); }))
+{
+}
+
 #endif /* ! defined(LL_LLEVENTDISPATCHER_H) */
diff --git a/indra/llcommon/lleventfilter.h b/indra/llcommon/lleventfilter.h
index 9dd2cbb684ed151d378dbd3de9016f062b6318de..db4dbeda560aca6a95144ac0a0dabbbfbc274610 100644
--- a/indra/llcommon/lleventfilter.h
+++ b/indra/llcommon/lleventfilter.h
@@ -435,16 +435,61 @@ class LLStoreListener: public LLEventFilter
         // generic type-appropriate store through mTarget, construct an
         // LLSDParam<T> and store that, thus engaging LLSDParam's custom
         // conversions.
-        mTarget = LLSDParam<T>(llsd::drill(event, mPath));
+        storeTarget(LLSDParam<T>(llsd::drill(event, mPath)));
         return mConsume;
     }
 
 private:
+    // This method disambiguates LLStoreListener<LLSD>. Directly assigning
+    // some_LLSD_var = LLSDParam<LLSD>(some_LLSD_value);
+    // is problematic because the compiler has too many choices: LLSD has
+    // multiple assignment operator overloads, and LLSDParam<LLSD> has a
+    // templated conversion operator. But LLSDParam<LLSD> can convert to a
+    // (const LLSD&) parameter, and LLSD::operator=(const LLSD&) works.
+    void storeTarget(const T& value)
+    {
+        mTarget = value;
+    }
+
     T& mTarget;
     const LLSD mPath;
     const bool mConsume;
 };
 
+/**
+ * LLVarHolder bundles a target variable of the specified type. We use it as a
+ * base class so the target variable will be fully constructed by the time a
+ * subclass constructor tries to pass a reference to some other base class.
+ */
+template <typename T>
+struct LLVarHolder
+{
+    T mVar;
+};
+
+/**
+ * LLCaptureListener isa LLStoreListener that bundles the target variable of
+ * interest.
+ */
+template <typename T>
+class LLCaptureListener: public LLVarHolder<T>,
+                         public LLStoreListener<T>
+{
+private:
+    using holder = LLVarHolder<T>;
+    using super = LLStoreListener<T>;
+
+public:
+    LLCaptureListener(const LLSD& path=LLSD(), bool consume=false):
+        super(*this, holder::mVar, path, consume)
+    {}
+
+    void set(T&& newval=T()) { holder::mVar = std::forward<T>(newval); }
+
+    const T& get() const { return holder::mVar; }
+    operator const T&() { return holder::mVar; }
+};
+
 /*****************************************************************************
 *   LLEventLogProxy
 *****************************************************************************/
diff --git a/indra/llcommon/llevents.cpp b/indra/llcommon/llevents.cpp
index 0a213bddef3da7f7e56554bc2419284db70f2355..1a305ec3dc453de445f2d00dbf9ac9c1eb5635a9 100644
--- a/indra/llcommon/llevents.cpp
+++ b/indra/llcommon/llevents.cpp
@@ -68,19 +68,78 @@
 LLEventPumps::LLEventPumps():
     mFactories
     {
-        { "LLEventStream",   [](const std::string& name, bool tweak)
+        { "LLEventStream",   [](const std::string& name, bool tweak, const std::string& /*type*/)
                              { return new LLEventStream(name, tweak); } },
-        { "LLEventMailDrop", [](const std::string& name, bool tweak)
+        { "LLEventMailDrop", [](const std::string& name, bool tweak, const std::string& /*type*/)
                              { return new LLEventMailDrop(name, tweak); } }
     },
     mTypes
     {
-        // LLEventStream is the default for obtain(), so even if somebody DOES
-        // call obtain("placeholder"), this sample entry won't break anything.
-        { "placeholder", "LLEventStream" }
+//      { "placeholder", "LLEventStream" }
     }
 {}
 
+bool LLEventPumps::registerTypeFactory(const std::string& type, const TypeFactory& factory)
+{
+    auto found = mFactories.find(type);
+    // can't re-register a TypeFactory for a type name that's already registered
+    if (found != mFactories.end())
+        return false;
+    // doesn't already exist, go ahead and register
+    mFactories[type] = factory;
+    return true;
+}
+
+void LLEventPumps::unregisterTypeFactory(const std::string& type)
+{
+    auto found = mFactories.find(type);
+    if (found != mFactories.end())
+        mFactories.erase(found);
+}
+
+bool LLEventPumps::registerPumpFactory(const std::string& name, const PumpFactory& factory)
+{
+    // Do we already have a pump by this name?
+    if (mPumpMap.find(name) != mPumpMap.end())
+        return false;
+    // Do we already have an override for this pump name?
+    if (mTypes.find(name) != mTypes.end())
+        return false;
+    // Leverage the two-level lookup implemented by mTypes (pump name -> type
+    // name) and mFactories (type name -> factory). We could instead create a
+    // whole separate (pump name -> factory) map, and look in both; or we
+    // could change mTypes to (pump name -> factory) and, for typical type-
+    // based lookups, use a "factory" that looks up the real factory in
+    // mFactories. But this works, and we don't expect many calls to make() -
+    // either explicit or implicit via obtain().
+    // Create a bogus type name extremely unlikely to collide with an actual type.
+    static std::string nul(1, '\0');
+    std::string type_name{ nul + name };
+    mTypes[name] = type_name;
+    // TypeFactory is called with (name, tweak, type), whereas PumpFactory
+    // accepts only name. We could adapt with std::bind(), but this lambda
+    // does the trick.
+    mFactories[type_name] =
+        [factory]
+        (const std::string& name, bool /*tweak*/, const std::string& /*type*/)
+        { return factory(name); };
+    return true;
+}
+
+void LLEventPumps::unregisterPumpFactory(const std::string& name)
+{
+    auto tfound = mTypes.find(name);
+    if (tfound != mTypes.end())
+    {
+        auto ffound = mFactories.find(tfound->second);
+        if (ffound != mFactories.end())
+        {
+            mFactories.erase(ffound);
+        }
+        mTypes.erase(tfound);
+    }
+}
+
 LLEventPump& LLEventPumps::obtain(const std::string& name)
 {
     PumpMap::iterator found = mPumpMap.find(name);
@@ -114,7 +173,7 @@ LLEventPump& LLEventPumps::make(const std::string& name, bool tweak,
         // Passing an unrecognized type name is a no-no
         LLTHROW(BadType(type));
     }
-    auto newInstance = (found->second)(name, tweak);
+    auto newInstance = (found->second)(name, tweak, type);
     // LLEventPump's constructor implicitly registers each new instance in
     // mPumpMap. But remember that we instantiated it (in mOurPumps) so we'll
     // delete it later.
diff --git a/indra/llcommon/llevents.h b/indra/llcommon/llevents.h
index 6968ef52ce35f984827d8dfdbdf6803f72c1b0e0..6a1e6d158b9b091cfeeac4860478226122180f8b 100644
--- a/indra/llcommon/llevents.h
+++ b/indra/llcommon/llevents.h
@@ -260,6 +260,45 @@ class LL_COMMON_API LLEventPumps final : public LLSingleton<LLEventPumps>,
     LLEventPump& make(const std::string& name, bool tweak=false,
                       const std::string& type=std::string());
 
+    /// function passed to registerTypeFactory()
+    typedef std::function<LLEventPump*(const std::string& name, bool tweak, const std::string& type)> TypeFactory;
+
+    /**
+     * Register a TypeFactory for use with make(). When make() is called with
+     * the specified @a type string, call @a factory(name, tweak, type) to
+     * instantiate it.
+     *
+     * Returns true if successfully registered, false if there already exists
+     * a TypeFactory for the specified @a type name.
+     */
+    bool registerTypeFactory(const std::string& type, const TypeFactory& factory);
+    void unregisterTypeFactory(const std::string& type);
+
+    /// function passed to registerPumpFactory()
+    typedef std::function<LLEventPump*(const std::string&)> PumpFactory;
+
+    /**
+     * Register a PumpFactory for use with obtain(). When obtain() is called
+     * with the specified @a name string, if an LLEventPump with the specified
+     * @a name doesn't already exist, call @a factory(name) to instantiate it.
+     *
+     * Returns true if successfully registered, false if there already exists
+     * a factory override for the specified @a name.
+     *
+     * PumpFactory does not support @a tweak because it's only called when
+     * <i>that particular</i> @a name is passed to obtain(). Bear in mind that
+     * <tt>obtain(name)</tt> might still bypass the caller's PumpFactory for a
+     * couple different reasons:
+     *
+     * * registerPumpFactory() returns false because there's already a factory
+     *   override for the specified @name
+     * * between a successful <tt>registerPumpFactory(name)</tt> call (returns
+     *   true) and a call to <tt>obtain(name)</tt>, someone explicitly
+     *   instantiated an LLEventPump(name), so obtain(name) returned that.
+     */
+    bool registerPumpFactory(const std::string& name, const PumpFactory& factory);
+    void unregisterPumpFactory(const std::string& name);
+
     /**
      * Find the named LLEventPump instance. If it exists post the message to it.
      * If the pump does not exist, do nothing.
@@ -317,13 +356,13 @@ class LL_COMMON_API LLEventPumps final : public LLSingleton<LLEventPumps>,
     typedef std::set<LLEventPump*> PumpSet;
     PumpSet mOurPumps;
     // for make(), map string type name to LLEventPump subclass factory function
-    typedef std::map<std::string, std::function<LLEventPump*(const std::string&, bool)>> PumpFactories;
+    typedef std::map<std::string, TypeFactory> TypeFactories;
     // Data used by make().
     // One might think mFactories and mTypes could reasonably be static. So
     // they could -- if not for the fact that make() or obtain() might be
     // called before this module's static variables have been initialized.
     // This is why we use singletons in the first place.
-    PumpFactories mFactories;
+    TypeFactories mFactories;
 
     // for obtain(), map desired string instance name to string type when
     // obtain() must create the instance
diff --git a/indra/llcommon/llleap.cpp b/indra/llcommon/llleap.cpp
index 051384f342919801d3f9910b9cf63cc0e3f1c0f2..fdb25fbe759e264e64201051fcf10f654feef253 100644
--- a/indra/llcommon/llleap.cpp
+++ b/indra/llcommon/llleap.cpp
@@ -332,11 +332,28 @@ class LLLeapImpl: public LLLeap
             }
             else
             {
-                // The LLSD object we got from our stream contains the keys we
-                // need.
-                LLEventPumps::instance().obtain(data["pump"]).post(data["data"]);
-                // Block calls to this method; resetting mBlocker unblocks calls
-                // to the other method.
+                try
+                {
+                    // The LLSD object we got from our stream contains the
+                    // keys we need.
+                    LLEventPumps::instance().obtain(data["pump"]).post(data["data"]);
+                }
+                catch (const std::exception& err)
+                {
+                    // No plugin should be allowed to crash the viewer by
+                    // driving an exception -- intentionally or not.
+                    LOG_UNHANDLED_EXCEPTION(stringize("handling request ", data));
+                    // Whether or not the plugin added a "reply" key to the
+                    // request, send a reply. We happen to know who originated
+                    // this request, and the reply LLEventPump of interest.
+                    // Not our problem if the plugin ignores the reply event.
+                    data["reply"] = mReplyPump.getName();
+                    sendReply(llsd::map("error",
+                                        stringize(LLError::Log::classname(err), ": ", err.what())),
+                              data);
+                }
+                // Block calls to this method; resetting mBlocker unblocks
+                // calls to the other method.
                 mBlocker.reset(new LLEventPump::Blocker(mStdoutDataConnection));
                 // Go check for any more pending events in the buffer.
                 if (childout.size())
diff --git a/indra/llcommon/llleaplistener.cpp b/indra/llcommon/llleaplistener.cpp
index 5a9bf75b0a3b1d0dc2ea43029352c25a772b3684..471f52e91c841a18a32b0bb65ea59c1c136ec3c1 100644
--- a/indra/llcommon/llleaplistener.cpp
+++ b/indra/llcommon/llleaplistener.cpp
@@ -14,13 +14,16 @@
 // associated header
 #include "llleaplistener.h"
 // STL headers
-#include <map>
+#include <algorithm>                // std::find_if
 #include <functional>
+#include <map>
+#include <set>
 // std headers
 // external library headers
 // other Linden headers
-#include "lluuid.h"
+#include "lazyeventapi.h"
 #include "llsdutil.h"
+#include "lluuid.h"
 #include "stringize.h"
 
 /*****************************************************************************
@@ -109,7 +112,7 @@ LLLeapListener::~LLLeapListener()
     // value_type, and Bad Things would happen if you copied an
     // LLTempBoundListener. (Destruction of the original would disconnect the
     // listener, invalidating every stored connection.)
-    for(ListenersMap::value_type& pair : mListeners)
+    for (ListenersMap::value_type& pair : mListeners)
     {
         pair.second.disconnect();
     }
@@ -207,31 +210,65 @@ void LLLeapListener::getAPIs(const LLSD& request) const
 {
     Response reply(LLSD(), request);
 
+    // first, traverse existing LLEventAPI instances
+    std::set<std::string> instances;
     for (auto& ea : LLEventAPI::instance_snapshot())
     {
-        LLSD info;
-        info["desc"] = ea.getDesc();
-        reply[ea.getName()] = info;
+        // remember which APIs are actually instantiated
+        instances.insert(ea.getName());
+        reply[ea.getName()] = llsd::map("desc", ea.getDesc());
+    }
+    // supplement that with *potential* instances: that is, instances of
+    // LazyEventAPI that can each instantiate an LLEventAPI on demand
+    for (const auto& lea : LL::LazyEventAPIBase::instance_snapshot())
+    {
+        // skip any LazyEventAPI that's already instantiated its LLEventAPI
+        if (instances.find(lea.getName()) == instances.end())
+        {
+            reply[lea.getName()] = llsd::map("desc", lea.getDesc());
+        }
     }
 }
 
+// Because LazyEventAPI deliberately mimics LLEventAPI's query API, this
+// function can be passed either -- even though they're unrelated types.
+template <typename API>
+void reportAPI(LLEventAPI::Response& reply, const API& api)
+{
+    reply["name"] = api.getName();
+    reply["desc"] = api.getDesc();
+    reply["key"]  = api.getDispatchKey();
+    LLSD ops;
+    for (const auto& namedesc : api)
+    {
+        ops.append(api.getMetadata(namedesc.first));
+    }
+    reply["ops"] = ops;
+}
+
 void LLLeapListener::getAPI(const LLSD& request) const
 {
     Response reply(LLSD(), request);
 
-    auto found = LLEventAPI::getInstance(request["api"]);
-    if (found)
+    // check first among existing LLEventAPI instances
+    auto foundea = LLEventAPI::getInstance(request["api"]);
+    if (foundea)
+    {
+        reportAPI(reply, *foundea);
+    }
+    else
     {
-        reply["name"] = found->getName();
-        reply["desc"] = found->getDesc();
-        reply["key"] = found->getDispatchKey();
-        LLSD ops;
-        for (LLEventAPI::const_iterator oi(found->begin()), oend(found->end());
-             oi != oend; ++oi)
+        // Here the requested LLEventAPI doesn't yet exist, but do we have a
+        // registered LazyEventAPI for it?
+        LL::LazyEventAPIBase::instance_snapshot snap;
+        auto foundlea = std::find_if(snap.begin(), snap.end(),
+                                     [api = request["api"].asString()]
+                                     (const auto& lea)
+                                     { return (lea.getName() == api); });
+        if (foundlea != snap.end())
         {
-            ops.append(found->getMetadata(oi->first));
+            reportAPI(reply, *foundlea);
         }
-        reply["ops"] = ops;
     }
 }
 
diff --git a/indra/llcommon/llprocess.cpp b/indra/llcommon/llprocess.cpp
index c14d7bca21def7dd0f0cd51d1999b7ea04acb3a8..eb70da1be600b283233c02179039f69d19f99287 100644
--- a/indra/llcommon/llprocess.cpp
+++ b/indra/llcommon/llprocess.cpp
@@ -535,6 +535,7 @@ LLProcess::LLProcess(const LLSDOrParams& params):
 	// preserve existing semantics, we promise that mAttached defaults to the
 	// same setting as mAutokill.
 	mAttached(params.attached.isProvided()? params.attached : params.autokill),
+    mPool(NULL),
 	mPipes(NSLOTS)
 {
 	// Hmm, when you construct a ptr_vector with a size, it merely reserves
@@ -555,8 +556,14 @@ LLProcess::LLProcess(const LLSDOrParams& params):
 
 	mPostend = params.postend;
 
+    apr_pool_create(&mPool, gAPRPoolp);
+    if (!mPool)
+    {
+        LLTHROW(LLProcessError(STRINGIZE("failed to create apr pool")));
+    }
+
 	apr_procattr_t *procattr = NULL;
-	chkapr(apr_procattr_create(&procattr, gAPRPoolp));
+	chkapr(apr_procattr_create(&procattr, mPool));
 
 	// IQA-490, CHOP-900: On Windows, ask APR to jump through hoops to
 	// constrain the set of handles passed to the child process. Before we
@@ -695,14 +702,14 @@ LLProcess::LLProcess(const LLSDOrParams& params):
 	// one. Hand-expand chkapr() macro so we can fill in the actual command
 	// string instead of the variable names.
 	if (ll_apr_warn_status(apr_proc_create(&mProcess, argv[0], &argv[0], NULL, procattr,
-										   gAPRPoolp)))
+										   mPool)))
 	{
 		LLTHROW(LLProcessError(STRINGIZE(params << " failed")));
 	}
 
 	// arrange to call status_callback()
 	apr_proc_other_child_register(&mProcess, &LLProcess::status_callback, this, mProcess.in,
-								  gAPRPoolp);
+                                  mPool);
 	// and make sure we poll it once per "mainloop" tick
 	sProcessListener.addPoll(*this);
 	mStatus.mState = RUNNING;
@@ -821,6 +828,12 @@ LLProcess::~LLProcess()
 	{
 		kill("destructor");
 	}
+
+    if (mPool)
+    {
+        apr_pool_destroy(mPool);
+        mPool = NULL;
+    }
 }
 
 bool LLProcess::kill(const std::string& who)
diff --git a/indra/llcommon/llprocess.h b/indra/llcommon/llprocess.h
index 98021450e1c7736e68fd6ad38b0ef3b4481a9fe6..8e23cc945c7120170f335956db0dea0efb7ceabf 100644
--- a/indra/llcommon/llprocess.h
+++ b/indra/llcommon/llprocess.h
@@ -566,6 +566,7 @@ class LL_COMMON_API LLProcess: public boost::noncopyable
 	// explicitly want this ptr_vector to be able to store NULLs
 	typedef boost::ptr_vector< boost::nullable<BasePipe> > PipeVector;
 	PipeVector mPipes;
+    apr_pool_t* mPool;
 };
 
 /// for logging
diff --git a/indra/llcommon/llptrto.h b/indra/llcommon/llptrto.h
index 4082e30de670bebdb422074b5b6f45f72847c7c8..9ef279fdbfd973f93b778b29dfe31df91734b163 100644
--- a/indra/llcommon/llptrto.h
+++ b/indra/llcommon/llptrto.h
@@ -33,9 +33,12 @@
 
 #include "llpointer.h"
 #include "llrefcount.h"             // LLRefCount
+#include <boost/intrusive_ptr.hpp>
+#include <boost/shared_ptr.hpp>
 #include <boost/type_traits/is_base_of.hpp>
 #include <boost/type_traits/remove_pointer.hpp>
-#include <boost/utility/enable_if.hpp>
+#include <memory>                   // std::shared_ptr, std::unique_ptr
+#include <type_traits>
 
 /**
  * LLPtrTo<TARGET>::type is either of two things:
@@ -55,14 +58,14 @@ struct LLPtrTo
 
 /// specialize for subclasses of LLRefCount
 template <class T>
-struct LLPtrTo<T, typename boost::enable_if< boost::is_base_of<LLRefCount, T> >::type>
+struct LLPtrTo<T, typename std::enable_if< boost::is_base_of<LLRefCount, T>::value >::type>
 {
     typedef LLPointer<T> type;
 };
 
 /// specialize for subclasses of LLThreadSafeRefCount
 template <class T>
-struct LLPtrTo<T, typename boost::enable_if< boost::is_base_of<LLThreadSafeRefCount, T> >::type>
+struct LLPtrTo<T, typename std::enable_if< boost::is_base_of<LLThreadSafeRefCount, T>::value >::type>
 {
     typedef LLPointer<T> type;
 };
@@ -83,4 +86,83 @@ struct LLRemovePointer< LLPointer<SOMECLASS> >
     typedef SOMECLASS type;
 };
 
+namespace LL
+{
+
+/*****************************************************************************
+*   get_ref()
+*****************************************************************************/
+    template <typename T>
+    struct GetRef
+    {
+        // return const ref or non-const ref, depending on whether we can bind
+        // a non-const lvalue ref to the argument
+        const auto& operator()(const T& obj) const { return obj; }
+        auto& operator()(T& obj) const { return obj; }
+    };
+
+    template <typename T>
+    struct GetRef<const T*>
+    {
+        const auto& operator()(const T* ptr) const { return *ptr; }
+    };
+
+    template <typename T>
+    struct GetRef<T*>
+    {
+        auto& operator()(T* ptr) const { return *ptr; }
+    };
+
+    template <typename T>
+    struct GetRef< LLPointer<T> >
+    {
+        auto& operator()(LLPointer<T> ptr) const { return *ptr; }
+    };
+
+    /// whether we're passed a pointer or a reference, return a reference
+    template <typename T>
+    auto& get_ref(T& ptr_or_ref)
+    {
+        return GetRef<typename std::decay<T>::type>()(ptr_or_ref);
+    }
+
+    template <typename T>
+    const auto& get_ref(const T& ptr_or_ref)
+    {
+        return GetRef<typename std::decay<T>::type>()(ptr_or_ref);
+    }
+
+/*****************************************************************************
+*   get_ptr()
+*****************************************************************************/
+    // if T is any pointer type we recognize, return it unchanged
+    template <typename T>
+    const T* get_ptr(const T* ptr) { return ptr; }
+
+    template <typename T>
+    T* get_ptr(T* ptr) { return ptr; }
+
+    template <typename T>
+    const std::shared_ptr<T>& get_ptr(const std::shared_ptr<T>& ptr) { return ptr; }
+
+    template <typename T>
+    const std::unique_ptr<T>& get_ptr(const std::unique_ptr<T>& ptr) { return ptr; }
+
+    template <typename T>
+    const boost::shared_ptr<T>& get_ptr(const boost::shared_ptr<T>& ptr) { return ptr; }
+
+    template <typename T>
+    const boost::intrusive_ptr<T>& get_ptr(const boost::intrusive_ptr<T>& ptr) { return ptr; }
+
+    template <typename T>
+    const LLPointer<T>& get_ptr(const LLPointer<T>& ptr) { return ptr; }
+
+    // T is not any pointer type we recognize, take a pointer to the parameter
+    template <typename T>
+    const T* get_ptr(const T& obj) { return &obj; }
+
+    template <typename T>
+    T* get_ptr(T& obj) { return &obj; }
+} // namespace LL
+
 #endif /* ! defined(LL_LLPTRTO_H) */
diff --git a/indra/llcommon/llsdutil.cpp b/indra/llcommon/llsdutil.cpp
index dd2ecda802bda1f032c0d1796f700b7a6bb8563d..a8c3f2dea2e8dd7ab24d989ff1ff192c994b553c 100644
--- a/indra/llcommon/llsdutil.cpp
+++ b/indra/llcommon/llsdutil.cpp
@@ -1045,3 +1045,38 @@ LLSD llsd_shallow(LLSD value, LLSD filter)
 
     return shallow;
 }
+
+LLSD LL::apply_llsd_fix(size_t arity, const LLSD& args)
+{
+    // LLSD supports a number of types, two of which are aggregates: Map and
+    // Array. We don't try to support Map: supporting Map would seem to
+    // promise that we could somehow match the string key to 'func's parameter
+    // names. Uh sorry, maybe in some future version of C++ with reflection.
+    if (args.isMap())
+    {
+        LLTHROW(LL::apply_error("LL::apply(function, Map LLSD) unsupported"));
+    }
+    // We expect an LLSD array, but what the heck, treat isUndefined() as a
+    // zero-length array for calling a nullary 'func'.
+    if (args.isUndefined() || args.isArray())
+    {
+        // this works because LLSD().size() == 0
+        if (args.size() != arity)
+        {
+            LLTHROW(LL::apply_error(stringize("LL::apply(function(", arity, " args), ",
+                                              args.size(), "-entry LLSD array)")));
+        }
+        return args;
+    }
+
+    // args is one of the scalar types
+    // scalar_LLSD.size() == 0, so don't test that here.
+    // You can pass a scalar LLSD only to a unary 'func'.
+    if (arity != 1)
+    {
+        LLTHROW(LL::apply_error(stringize("LL::apply(function(", arity, " args), "
+                                          "LLSD ", LLSD::typeString(args.type()), ")")));
+    }
+    // make an array of it
+    return llsd::array(args);
+}
diff --git a/indra/llcommon/llsdutil.h b/indra/llcommon/llsdutil.h
index c730393906df1d57709cb66ed30967217bb3a685..e6f404d59f009b1b35cb53f905fc9db33954a707 100644
--- a/indra/llcommon/llsdutil.h
+++ b/indra/llcommon/llsdutil.h
@@ -29,8 +29,14 @@
 #ifndef LL_LLSDUTIL_H
 #define LL_LLSDUTIL_H
 
+#include "apply.h"                  // LL::invoke()
+#include "function_types.h"         // LL::function_arity
 #include "llsd.h"
 #include <boost/functional/hash.hpp>
+#include <cassert>
+#include <memory>                   // std::shared_ptr
+#include <type_traits>
+#include <vector>
 
 // U32
 LL_COMMON_API LLSD ll_sd_from_U32(const U32);
@@ -298,6 +304,11 @@ LLSD map(Ts&&... vs)
 /*****************************************************************************
 *   LLSDParam
 *****************************************************************************/
+struct LLSDParamBase
+{
+    virtual ~LLSDParamBase() {}
+};
+
 /**
  * LLSDParam is a customization point for passing LLSD values to function
  * parameters of more or less arbitrary type. LLSD provides a small set of
@@ -315,7 +326,7 @@ LLSD map(Ts&&... vs)
  * @endcode
  */
 template <typename T>
-class LLSDParam
+class LLSDParam: public LLSDParamBase
 {
 public:
     /**
@@ -323,13 +334,66 @@ class LLSDParam
      * value for later retrieval
      */
     LLSDParam(const LLSD& value):
-        _value(value)
+        value_(value)
     {}
 
-    operator T() const { return _value; }
+    operator T() const { return value_; }
 
 private:
-    T _value;
+    T value_;
+};
+
+/**
+ * LLSDParam<LLSD> is for when you don't already have the target parameter
+ * type in hand. Instantiate LLSDParam<LLSD>(your LLSD object), and the
+ * templated conversion operator will try to select a more specific LLSDParam
+ * specialization.
+ */
+template <>
+class LLSDParam<LLSD>: public LLSDParamBase
+{
+private:
+    LLSD value_;
+    // LLSDParam<LLSD>::operator T() works by instantiating an LLSDParam<T> on
+    // demand. Returning that engages LLSDParam<T>::operator T(), producing
+    // the desired result. But LLSDParam<const char*> owns a std::string whose
+    // c_str() is returned by its operator const char*(). If we return a temp
+    // LLSDParam<const char*>, the compiler can destroy it right away, as soon
+    // as we've called operator const char*(). That's a problem! That
+    // invalidates the const char* we've just passed to the subject function.
+    // This LLSDParam<LLSD> is presumably guaranteed to survive until the
+    // subject function has returned, so we must ensure that any constructed
+    // LLSDParam<T> lives just as long as this LLSDParam<LLSD> does. Putting
+    // each LLSDParam<T> on the heap and capturing a smart pointer in a vector
+    // works. We would have liked to use std::unique_ptr, but vector entries
+    // must be copyable.
+    // (Alternatively we could assume that every instance of LLSDParam<LLSD>
+    // will be asked for at most ONE conversion. We could store a scalar
+    // std::unique_ptr and, when constructing an new LLSDParam<T>, assert that
+    // the unique_ptr is empty. But some future change in usage patterns, and
+    // consequent failure of that assertion, would be very mysterious. Instead
+    // of explaining how to fix it, just fix it now.)
+    mutable std::vector<std::shared_ptr<LLSDParamBase>> converters_;
+
+public:
+    LLSDParam(const LLSD& value): value_(value) {}
+
+    /// if we're literally being asked for an LLSD parameter, avoid infinite
+    /// recursion
+    operator LLSD() const { return value_; }
+
+    /// otherwise, instantiate a more specific LLSDParam<T> to convert; that
+    /// preserves the existing customization mechanism
+    template <typename T>
+    operator T() const
+    {
+        // capture 'ptr' with the specific subclass type because converters_
+        // only stores LLSDParamBase pointers
+        auto ptr{ std::make_shared<LLSDParam<std::decay_t<T>>>(value_) };
+        // keep the new converter alive until we ourselves are destroyed
+        converters_.push_back(ptr);
+        return *ptr;
+    }
 };
 
 /**
@@ -346,17 +410,17 @@ class LLSDParam
  */
 #define LLSDParam_for(T, AS)                    \
 template <>                                     \
-class LLSDParam<T>                              \
+class LLSDParam<T>: public LLSDParamBase        \
 {                                               \
 public:                                         \
     LLSDParam(const LLSD& value):               \
-        _value((T)value.AS())                      \
+        value_((T)value.AS())                   \
     {}                                          \
                                                 \
-    operator T() const { return _value; }       \
+    operator T() const { return value_; }       \
                                                 \
 private:                                        \
-    T _value;                                   \
+    T value_;                                   \
 }
 
 LLSDParam_for(float,        asReal);
@@ -372,31 +436,31 @@ LLSDParam_for(LLSD::Binary, asBinary);
  * safely pass an LLSDParam<const char*>(yourLLSD).
  */
 template <>
-class LLSDParam<const char*>
+class LLSDParam<const char*>: public LLSDParamBase
 {
 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;
+    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
+    // distinguish whether value_ is an empty string because the LLSD object
     // contains an empty string or because it's isUndefined().
-    bool _undefined;
+    bool undefined_;
 
 public:
     LLSDParam(const LLSD& value):
-        _value(value),
-        _undefined(value.isUndefined())
+        value_(value),
+        undefined_(value.isUndefined())
     {}
 
-    // The const char* we retrieve is for storage owned by our _value member.
+    // 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)
+        if (undefined_)
         {
             // By default, an isUndefined() LLSD object's asString() method
             // will produce an empty string. But for a function accepting
@@ -406,7 +470,7 @@ class LLSDParam<const char*>
             // case, though, no LLSD value could pass NULL.
             return NULL;
         }
-        return _value.c_str();
+        return value_.c_str();
     }
 };
 
@@ -555,4 +619,56 @@ struct hash<LLSD>
     }
 };
 }
+
+namespace LL
+{
+
+/*****************************************************************************
+*   apply(function, LLSD array)
+*****************************************************************************/
+// validate incoming LLSD blob, and return an LLSD array suitable to pass to
+// the function of interest
+LLSD apply_llsd_fix(size_t arity, const LLSD& args);
+
+// Derived from https://stackoverflow.com/a/20441189
+// and https://en.cppreference.com/w/cpp/utility/apply .
+// We can't simply make a tuple from the LLSD array and then apply() that
+// tuple to the function -- how would make_tuple() deduce the correct
+// parameter type for each entry? We must go directly to the target function.
+template <typename CALLABLE, std::size_t... I>
+auto apply_impl(CALLABLE&& func, const LLSD& array, std::index_sequence<I...>)
+{
+    // call func(unpacked args), using generic LLSDParam<LLSD> to convert each
+    // entry in 'array' to the target parameter type
+    return std::forward<CALLABLE>(func)(LLSDParam<LLSD>(array[I])...);
+}
+
+// use apply_n<ARITY>(function, LLSD) to call a specific arity of a variadic
+// function with (that many) items from the passed LLSD array
+template <size_t ARITY, typename CALLABLE>
+auto apply_n(CALLABLE&& func, const LLSD& args)
+{
+    return apply_impl(std::forward<CALLABLE>(func),
+                      apply_llsd_fix(ARITY, args),
+                      std::make_index_sequence<ARITY>());
+}
+
+/**
+ * apply(function, LLSD) goes beyond C++17 std::apply(). For this case
+ * @a function @emph cannot be variadic: the compiler must know at compile
+ * time how many arguments to pass. This isn't Python. (But see apply_n() to
+ * pass a specific number of args to a variadic function.)
+ */
+template <typename CALLABLE>
+auto apply(CALLABLE&& func, const LLSD& args)
+{
+    // infer arity from the definition of func
+    constexpr auto arity = function_arity<
+        typename std::remove_reference<CALLABLE>::type>::value;
+    // now that we have a compile-time arity, apply_n() works
+    return apply_n<arity>(std::forward<CALLABLE>(func), args);
+}
+
+} // namespace LL
+
 #endif // LL_LLSDUTIL_H
diff --git a/indra/llcommon/tests/apply_test.cpp b/indra/llcommon/tests/apply_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..56b497e0c84951e3ac77219aba8965c72b133486
--- /dev/null
+++ b/indra/llcommon/tests/apply_test.cpp
@@ -0,0 +1,240 @@
+/**
+ * @file   apply_test.cpp
+ * @author Nat Goodspeed
+ * @date   2022-12-19
+ * @brief  Test for apply.
+ * 
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Copyright (c) 2022, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "apply.h"
+// STL headers
+// std headers
+#include <iomanip>
+// external library headers
+// other Linden headers
+#include "llsd.h"
+#include "llsdutil.h"
+#include <array>
+#include <string>
+#include <vector>
+
+// for ensure_equals
+std::ostream& operator<<(std::ostream& out, const std::vector<std::string>& stringvec)
+{
+    const char* delim = "[";
+    for (const auto& str : stringvec)
+    {
+        out << delim << std::quoted(str);
+        delim = ", ";
+    }
+    return out << ']';
+}
+
+// the above must be declared BEFORE ensure_equals(std::vector<std::string>)
+#include "../test/lltut.h"
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+namespace tut
+{
+    namespace statics
+    {
+        /*------------------------------ data ------------------------------*/
+        // Although we're using types from the LLSD namespace, we're not
+        // constructing LLSD values, but rather instances of the C++ types
+        // supported by LLSD.
+        static LLSD::Boolean b{true};
+        static LLSD::Integer i{17};
+        static LLSD::Real    f{3.14};
+        static LLSD::String  s{ "hello" };
+        static LLSD::UUID    uu{ "baadf00d-dead-beef-baad-feedb0ef" };
+        static LLSD::Date    dt{ "2022-12-19" };
+        static LLSD::URI     uri{ "http://secondlife.com" };
+        static LLSD::Binary  bin{ 0x01, 0x02, 0x03, 0x04, 0x05 };
+
+        static std::vector<LLSD::String> quick
+        {
+            "The", "quick", "brown", "fox", "etc."
+        };
+
+        static std::array<int, 5> fibs
+        {
+            0, 1, 1, 2, 3
+        };
+
+        // ensure that apply() actually reaches the target method --
+        // lack of ensure_equals() failure could be due to no-op apply()
+        bool called{ false };
+        // capture calls from collect()
+        std::vector<std::string> collected;
+
+        /*------------------------- test functions -------------------------*/
+        void various(LLSD::Boolean b, LLSD::Integer i, LLSD::Real f, const LLSD::String& s,
+                     const LLSD::UUID& uu, const LLSD::Date& dt,
+                     const LLSD::URI& uri, const LLSD::Binary& bin)
+        {
+            called = true;
+            ensure_equals(  "b mismatch", b,   statics::b);
+            ensure_equals(  "i mismatch", i,   statics::i);
+            ensure_equals(  "f mismatch", f,   statics::f);
+            ensure_equals(  "s mismatch", s,   statics::s);
+            ensure_equals( "uu mismatch", uu,  statics::uu);
+            ensure_equals( "dt mismatch", dt,  statics::dt);
+            ensure_equals("uri mismatch", uri, statics::uri);
+            ensure_equals("bin mismatch", bin, statics::bin);
+        }
+
+        void strings(std::string s0, std::string s1, std::string s2, std::string s3, std::string s4)
+        {
+            called = true;
+            ensure_equals("s0 mismatch", s0, statics::quick[0]);
+            ensure_equals("s1 mismatch", s1, statics::quick[1]);
+            ensure_equals("s2 mismatch", s2, statics::quick[2]);
+            ensure_equals("s3 mismatch", s3, statics::quick[3]);
+            ensure_equals("s4 mismatch", s4, statics::quick[4]);
+        }
+
+        void ints(int i0, int i1, int i2, int i3, int i4)
+        {
+            called = true;
+            ensure_equals("i0 mismatch", i0, statics::fibs[0]);
+            ensure_equals("i1 mismatch", i1, statics::fibs[1]);
+            ensure_equals("i2 mismatch", i2, statics::fibs[2]);
+            ensure_equals("i3 mismatch", i3, statics::fibs[3]);
+            ensure_equals("i4 mismatch", i4, statics::fibs[4]);
+        }
+
+        void sdfunc(const LLSD& sd)
+        {
+            called = true;
+            ensure_equals("sd mismatch", sd.asInteger(), statics::i);
+        }
+
+        void intfunc(int i)
+        {
+            called = true;
+            ensure_equals("i mismatch", i, statics::i);
+        }
+
+        void voidfunc()
+        {
+            called = true;
+        }
+
+        // recursion tail
+        void collect()
+        {
+            called = true;
+        }
+
+        // collect(arbitrary)
+        template <typename... ARGS>
+        void collect(const std::string& first, ARGS&&... rest)
+        {
+            statics::collected.push_back(first);
+            collect(std::forward<ARGS>(rest)...);
+        }
+    } // namespace statics
+
+    struct apply_data
+    {
+        apply_data()
+        {
+            // reset called before each test
+            statics::called = false;
+            statics::collected.clear();
+        }
+    };
+    typedef test_group<apply_data> apply_group;
+    typedef apply_group::object object;
+    apply_group applygrp("apply");
+
+    template<> template<>
+    void object::test<1>()
+    {
+        set_test_name("apply(tuple)");
+        LL::apply(statics::various,
+                  std::make_tuple(statics::b, statics::i, statics::f, statics::s,
+                                  statics::uu, statics::dt, statics::uri, statics::bin));
+        ensure("apply(tuple) failed", statics::called);
+    }
+
+    template<> template<>
+    void object::test<2>()
+    {
+        set_test_name("apply(array)");
+        LL::apply(statics::ints, statics::fibs);
+        ensure("apply(array) failed", statics::called);
+    }
+
+    template<> template<>
+    void object::test<3>()
+    {
+        set_test_name("apply(vector)");
+        LL::apply(statics::strings, statics::quick);
+        ensure("apply(vector) failed", statics::called);
+    }
+
+    // The various apply(LLSD) tests exercise only the success cases because
+    // the failure cases trigger assert() fail, which is hard to catch.
+    template<> template<>
+    void object::test<4>()
+    {
+        set_test_name("apply(LLSD())");
+        LL::apply(statics::voidfunc, LLSD());
+        ensure("apply(LLSD()) failed", statics::called);
+    }
+
+    template<> template<>
+    void object::test<5>()
+    {
+        set_test_name("apply(fn(int), LLSD scalar)");
+        LL::apply(statics::intfunc, LLSD(statics::i));
+        ensure("apply(fn(int), LLSD scalar) failed", statics::called);
+    }
+
+    template<> template<>
+    void object::test<6>()
+    {
+        set_test_name("apply(fn(LLSD), LLSD scalar)");
+        // This test verifies that LLSDParam<LLSD> doesn't send the compiler
+        // into infinite recursion when the target is itself LLSD.
+        LL::apply(statics::sdfunc, LLSD(statics::i));
+        ensure("apply(fn(LLSD), LLSD scalar) failed", statics::called);
+    }
+
+    template<> template<>
+    void object::test<7>()
+    {
+        set_test_name("apply(LLSD array)");
+        LL::apply(statics::various,
+                  llsd::array(statics::b, statics::i, statics::f, statics::s,
+                              statics::uu, statics::dt, statics::uri, statics::bin));
+        ensure("apply(LLSD array) failed", statics::called);
+    }
+
+    template<> template<>
+    void object::test<8>()
+    {
+        set_test_name("VAPPLY()");
+        // Make a std::array<std::string> from statics::quick. We can't call a
+        // variadic function with a data structure of dynamic length.
+        std::array<std::string, 5> strray;
+        for (size_t i = 0; i < strray.size(); ++i)
+            strray[i] = statics::quick[i];
+        // This doesn't work: the compiler doesn't know which overload of
+        // collect() to pass to LL::apply().
+        // LL::apply(statics::collect, strray);
+        // That's what VAPPLY() is for.
+        VAPPLY(statics::collect, strray);
+        ensure("VAPPLY() failed", statics::called);
+        ensure_equals("collected mismatch", statics::collected, statics::quick);
+    }
+} // namespace tut
diff --git a/indra/llcommon/tests/lazyeventapi_test.cpp b/indra/llcommon/tests/lazyeventapi_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..31b2d6d17fdd8fb1bb8ab936f2b6c979fe642cf6
--- /dev/null
+++ b/indra/llcommon/tests/lazyeventapi_test.cpp
@@ -0,0 +1,136 @@
+/**
+ * @file   lazyeventapi_test.cpp
+ * @author Nat Goodspeed
+ * @date   2022-06-18
+ * @brief  Test for lazyeventapi.
+ * 
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Copyright (c) 2022, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lazyeventapi.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "../test/lltut.h"
+#include "llevents.h"
+#include "llsdutil.h"
+
+// observable side effect, solely for testing
+static LLSD data;
+
+// LLEventAPI listener subclass
+class MyListener: public LLEventAPI
+{
+public:
+    // need this trivial forwarding constructor
+    // (of course do any other initialization your subclass requires)
+    MyListener(const LL::LazyEventAPIParams& params):
+        LLEventAPI(params)
+    {}
+
+    // example operation, registered by LazyEventAPI subclass below
+    void set_data(const LLSD& event)
+    {
+        data = event["data"];
+    }
+};
+
+// LazyEventAPI registrar subclass
+class MyRegistrar: public LL::LazyEventAPI<MyListener>
+{
+    using super = LL::LazyEventAPI<MyListener>;
+    using super::listener;
+public:
+    // LazyEventAPI subclass initializes like a classic LLEventAPI subclass
+    // constructor, with API name and desc plus add() calls for the defined
+    // operations
+    MyRegistrar():
+        super("Test", "This is a test LLEventAPI")
+    {
+        add("set", "This is a set operation", &listener::set_data);
+    }
+};
+// Normally we'd declare a static instance of MyRegistrar -- but because we
+// want to test both with and without, defer declaration to individual test
+// methods.
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+namespace tut
+{
+    struct lazyeventapi_data
+    {
+        lazyeventapi_data()
+        {
+            // before every test, reset 'data'
+            data.clear();
+        }
+        ~lazyeventapi_data()
+        {
+            // after every test, reset LLEventPumps
+            LLEventPumps::deleteSingleton();
+        }
+    };
+    typedef test_group<lazyeventapi_data> lazyeventapi_group;
+    typedef lazyeventapi_group::object object;
+    lazyeventapi_group lazyeventapigrp("lazyeventapi");
+
+    template<> template<>
+    void object::test<1>()
+    {
+        set_test_name("LazyEventAPI");
+        // this is where the magic (should) happen
+        // 'register' still a keyword until C++17
+        MyRegistrar regster;
+        LLEventPumps::instance().obtain("Test").post(llsd::map("op", "set", "data", "hey"));
+        ensure_equals("failed to set data", data.asString(), "hey");
+    }
+
+    template<> template<>
+    void object::test<2>()
+    {
+        set_test_name("No LazyEventAPI");
+        // Because the MyRegistrar declaration in test<1>() is local, because
+        // it has been destroyed, we fully expect NOT to reach a MyListener
+        // instance with this post.
+        LLEventPumps::instance().obtain("Test").post(llsd::map("op", "set", "data", "moot"));
+        ensure("accidentally set data", ! data.isDefined());
+    }
+
+    template<> template<>
+    void object::test<3>()
+    {
+        set_test_name("LazyEventAPI metadata");
+        MyRegistrar regster;
+        // Of course we have 'regster' in hand; we don't need to search for
+        // it. But this next test verifies that we can find (all) LazyEventAPI
+        // instances using LazyEventAPIBase::instance_snapshot. Normally we
+        // wouldn't search; normally we'd just look at each instance in the
+        // loop body.
+        const MyRegistrar* found = nullptr;
+        for (const auto& registrar : LL::LazyEventAPIBase::instance_snapshot())
+            if ((found = dynamic_cast<const MyRegistrar*>(&registrar)))
+                break;
+        ensure("Failed to find MyRegistrar via LLInstanceTracker", found);
+
+        ensure_equals("wrong API name", found->getName(), "Test");
+        ensure_contains("wrong API desc", found->getDesc(), "test LLEventAPI");
+        ensure_equals("wrong API field", found->getDispatchKey(), "op");
+        // Normally we'd just iterate over *found. But for test purposes,
+        // actually capture the range of NameDesc pairs in a vector.
+        std::vector<LL::LazyEventAPIBase::NameDesc> ops{ found->begin(), found->end() };
+        ensure_equals("failed to find operations", ops.size(), 1);
+        ensure_equals("wrong operation name", ops[0].first, "set");
+        ensure_contains("wrong operation desc", ops[0].second, "set operation");
+        LLSD metadata{ found->getMetadata(ops[0].first) };
+        ensure_equals("bad metadata name", metadata["name"].asString(), ops[0].first);
+        ensure_equals("bad metadata desc", metadata["desc"].asString(), ops[0].second);
+    }
+} // namespace tut
diff --git a/indra/llcommon/tests/lleventdispatcher_test.cpp b/indra/llcommon/tests/lleventdispatcher_test.cpp
index 4f4195f2d5a0d51f3dcd06a42302c6dba2c5a71e..4f3be2600724de626a0bfd7a49aab0c1e365d0b6 100644
--- a/indra/llcommon/tests/lleventdispatcher_test.cpp
+++ b/indra/llcommon/tests/lleventdispatcher_test.cpp
@@ -18,9 +18,12 @@
 // external library headers
 // other Linden headers
 #include "../test/lltut.h"
+#include "lleventfilter.h"
 #include "llsd.h"
 #include "llsdutil.h"
+#include "llevents.h"
 #include "stringize.h"
+#include "StringVec.h"
 #include "tests/wrapllerrs.h"
 #include "../test/catch_and_store_what_in.h"
 #include "../test/debug.h"
@@ -175,6 +178,7 @@ struct Vars
     /*-------- Arbitrary-params (non-const, const, static) methods ---------*/
     void methodna(NPARAMSa)
     {
+        DEBUG;
         // 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
@@ -186,7 +190,7 @@ struct Vars
         else
             vcp = std::string("'") + cp + "'";
 
-        debug()("methodna(", b,
+        this->debug()("methodna(", b,
               ", ", i,
               ", ", f,
               ", ", d,
@@ -203,7 +207,7 @@ struct Vars
     void methodnb(NPARAMSb)
     {
         std::ostringstream vbin;
-        for (U8 byte : bin)
+        for (U8 byte: bin)
         {
             vbin << std::hex << std::setfill('0') << std::setw(2) << unsigned(byte);
         }
@@ -224,7 +228,8 @@ struct Vars
 
     void cmethodna(NPARAMSa) const
     {
-        debug()('c', NONL);
+        DEBUG;
+        this->debug()('c', NONL);
         const_cast<Vars*>(this)->methodna(NARGSa);
     }
 
@@ -313,6 +318,31 @@ void freenb(NPARAMSb)
 *****************************************************************************/
 namespace tut
 {
+    void ensure_has(const std::string& outer, const std::string& inner)
+    {
+        ensure(stringize("'", outer, "' does not contain '", inner, "'"),
+               outer.find(inner) != std::string::npos);
+    }
+
+    template <typename CALLABLE>
+    std::string call_exc(CALLABLE&& func, const std::string& exc_frag)
+    {
+        std::string what =
+            catch_what<LLEventDispatcher::DispatchError>(std::forward<CALLABLE>(func));
+        ensure_has(what, exc_frag);
+        return what;
+    }
+
+    template <typename CALLABLE>
+    void call_logerr(CALLABLE&& func, const std::string& frag)
+    {
+        CaptureLog capture;
+        // the error should be logged; we just need to stop the exception
+        // propagating
+        catch_what<LLEventDispatcher::DispatchError>(std::forward<CALLABLE>(func));
+        capture.messageWith(frag);
+    }
+
     struct lleventdispatcher_data
     {
         Debug debug{"test"};
@@ -395,9 +425,9 @@ namespace tut
             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));
+            work.add(name, desc, [this](const LLSD& args){ return v.method1(args); });
             addf("method1_req", "method1", &v);
-            work.add(name, desc, boost::bind(&Vars::method1, boost::ref(v), _1), required);
+            work.add(name, desc, [this](const LLSD& args){ return v.method1(args); }, required);
 
             /*--------------- Arbitrary params, array style ----------------*/
 
@@ -459,7 +489,7 @@ namespace tut
             debug("dft_array_full:\n",
                   dft_array_full);
             // Partial defaults arrays.
-            for (LLSD::String a : ab)
+            for (LLSD::String a: ab)
             {
                 LLSD::Integer partition(std::min(partial_offset, dft_array_full[a].size()));
                 dft_array_partial[a] =
@@ -469,7 +499,7 @@ namespace tut
             debug("dft_array_partial:\n",
                   dft_array_partial);
 
-            for (LLSD::String a : ab)
+            for(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]);
@@ -581,6 +611,7 @@ namespace tut
 
         void addf(const std::string& n, const std::string& d, Vars* v)
         {
+            debug("addf('", n, "', '", d, "')");
             // This method is to capture in our own DescMap the name and
             // description of every registered function, for metadata query
             // testing.
@@ -596,19 +627,14 @@ namespace tut
         {
             // 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.
-            for (LLEventDispatcher::NameDesc nd : const_cast<const Dispatcher&>(work))
+            for (LLEventDispatcher::NameDesc nd: work)
             {
                 DescMap::iterator found = forgotten.find(nd.first);
-                ensure(STRINGIZE("LLEventDispatcher records function '" << nd.first
-                                 << "' we didn't enter"),
+                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 << "'"),
+                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
@@ -619,41 +645,49 @@ namespace tut
                 std::ostringstream out;
                 out << "LLEventDispatcher failed to report";
                 const char* delim = ": ";
-                for (const DescMap::value_type& fme : forgotten)
+                for (const DescMap::value_type& fme: forgotten)
                 {
                     out << delim << fme.first;
                     delim = ", ";
                 }
-                ensure(out.str(), false);
+                throw failure(out.str());
             }
         }
 
         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);
+            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)
+        std::string call_exc(const std::string& func, const LLSD& args, const std::string& exc_frag)
         {
-            ensure(STRINGIZE("'" << outer << "' does not contain '" << inner << "'").c_str(),
-                   outer.find(inner) != std::string::npos);
+            return tut::call_exc(
+                [this, func, args]()
+                {
+                    if (func.empty())
+                    {
+                        work(args);
+                    }
+                    else
+                    {
+                        work(func, args);
+                    }
+                },
+                exc_frag);
         }
 
-        void call_exc(const std::string& func, const LLSD& args, const std::string& exc_frag)
+        void call_logerr(const std::string& func, const LLSD& args, const std::string& frag)
         {
-            std::string threw = catch_what<std::runtime_error>([this, &func, &args](){
-                    work(func, args);
-                });
-            ensure_has(threw, exc_frag);
+            tut::call_logerr([this, func, args](){ work(func, args); }, frag);
         }
 
         LLSD getMetadata(const std::string& name)
         {
             LLSD meta(work.getMetadata(name));
-            ensure(STRINGIZE("No metadata for " << name), meta.isDefined());
+            ensure(stringize("No metadata for ", name), meta.isDefined());
             return meta;
         }
 
@@ -722,7 +756,7 @@ namespace tut
         set_test_name("map-style registration with non-array params");
         // Pass "param names" as scalar or as map
         LLSD attempts(llsd::array(17, LLSDMap("pi", 3.14)("two", 2)));
-        for (LLSD ae : inArray(attempts))
+        for (LLSD ae: inArray(attempts))
         {
             std::string threw = catch_what<std::exception>([this, &ae](){
                     work.add("freena_err", "freena", freena, ae);
@@ -797,7 +831,7 @@ namespace tut
     {
         set_test_name("query Callables with/out required params");
         LLSD names(llsd::array("free1", "Dmethod1", "Dcmethod1", "method1"));
-        for (LLSD nm : inArray(names))
+        for (LLSD nm: inArray(names))
         {
             LLSD metadata(getMetadata(nm));
             ensure_equals("name mismatch", metadata["name"], nm);
@@ -826,19 +860,19 @@ namespace tut
                        (5, llsd::array("freena_array", "smethodna_array", "methodna_array")),
                        llsd::array
                        (5, llsd::array("freenb_array", "smethodnb_array", "methodnb_array"))));
-        for (LLSD ae : inArray(expected))
+        for (LLSD ae: inArray(expected))
         {
             LLSD::Integer arity(ae[0].asInteger());
             LLSD names(ae[1]);
             LLSD req(LLSD::emptyArray());
             if (arity)
                 req[arity - 1] = LLSD();
-            for (LLSD nm : inArray(names))
+            for (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()),
+                ensure_equals(stringize("mismatched required for ", nm.asString()),
                               metadata["required"], req);
                 ensure("should not have optional", metadata["optional"].isUndefined());
             }
@@ -852,7 +886,7 @@ namespace tut
         // - (Free function | non-static method), map style, no params (ergo
         //   no defaults)
         LLSD names(llsd::array("free0_map", "smethod0_map", "method0_map"));
-        for (LLSD nm : inArray(names))
+        for (LLSD nm: inArray(names))
         {
             LLSD metadata(getMetadata(nm));
             ensure_equals("name mismatch", metadata["name"], nm);
@@ -882,7 +916,7 @@ namespace tut
                            llsd::array("smethodnb_map_adft", "smethodnb_map_mdft"),
                            llsd::array("methodna_map_adft", "methodna_map_mdft"),
                            llsd::array("methodnb_map_adft", "methodnb_map_mdft")));
-        for (LLSD eq : inArray(equivalences))
+        for (LLSD eq: inArray(equivalences))
         {
             LLSD adft(eq[0]);
             LLSD mdft(eq[1]);
@@ -896,8 +930,8 @@ namespace tut
             ensure_equals("mdft name", mdft, mmeta["name"]);
             ameta.erase("name");
             mmeta.erase("name");
-            ensure_equals(STRINGIZE("metadata for " << adft.asString()
-                                    << " vs. " << mdft.asString()),
+            ensure_equals(stringize("metadata for ", adft.asString(),
+                                    " vs. ", mdft.asString()),
                           ameta, mmeta);
         }
     }
@@ -913,7 +947,7 @@ namespace tut
         // params are required. Also maps containing left requirements for
         // partial defaults arrays. Also defaults maps from defaults arrays.
         LLSD allreq, leftreq, rightdft;
-        for (LLSD::String a : ab)
+        for (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
@@ -941,7 +975,7 @@ namespace tut
         // Generate maps containing parameter names not provided by the
         // dft_map_partial maps.
         LLSD skipreq(allreq);
-        for (LLSD::String a : ab)
+        for (LLSD::String a: ab)
         {
             for (const MapEntry& me : inMap(dft_map_partial[a]))
             {
@@ -988,7 +1022,7 @@ namespace tut
                      (llsd::array("freenb_map_mdft", "smethodnb_map_mdft", "methodnb_map_mdft"),
                       llsd::array(LLSD::emptyMap(), dft_map_full["b"])))); // required, optional
 
-        for (LLSD grp : inArray(groups))
+        for (LLSD grp: inArray(groups))
         {
             // Internal structure of each group in 'groups':
             LLSD names(grp[0]);
@@ -1001,14 +1035,14 @@ namespace tut
                   optional);
 
             // Loop through 'names'
-            for (LLSD nm : inArray(names))
+            for (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"),
+                ensure_equals(stringize(nm, " required mismatch"),
                               metadata["required"], required);
-                ensure_equals(STRINGIZE(nm << " optional mismatch"),
+                ensure_equals(stringize(nm, " optional mismatch"),
                               metadata["optional"], optional);
             }
         }
@@ -1029,13 +1063,7 @@ namespace tut
     {
         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 = catch_what<std::runtime_error>([this](){
-                work(LLSDMap("op", "freek"));
-            });
-        ensure_has(threw, "bad");
+        std::string threw = call_exc("", LLSDMap("op", "freek"), "bad");
         ensure_has(threw, "op");
         ensure_has(threw, "freek");
     }
@@ -1077,7 +1105,7 @@ namespace tut
         // LLSD value matching 'required' according to llsd_matches() rules.
         LLSD matching(LLSDMap("d", 3.14)("array", llsd::array("answer", true, answer)));
         // Okay, walk through 'tests'.
-        for (const CallablesTriple& tr : tests)
+        for (const CallablesTriple& tr: tests)
         {
             // Should be able to pass 'answer' to Callables registered
             // without 'required'.
@@ -1085,7 +1113,7 @@ namespace tut
             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");
+            call_logerr(tr.name_req, answer, "bad request");
             // But SHOULD be able to pass 'matching' to Callables registered
             // with 'required'.
             work(tr.name_req, matching);
@@ -1099,17 +1127,20 @@ namespace tut
         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);
+        // functions. 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.
+        // Now that LLEventDispatcher has been extended to treat an LLSD
+        // scalar as a single-entry array, the error we expect in this case is
+        // that apply() is trying to pass that non-empty array to a nullary
+        // function.
+        call_logerr("free0_array", 17, "LL::apply");
+        // similarly, apply() doesn't accept an LLSD Map
+        call_logerr("free0_array", LLSDMap("pi", 3.14), "unsupported");
 
         std::string map_exc("needs a map");
-        call_exc("free0_map", 17, map_exc);
+        call_logerr("free0_map", 17, map_exc);
         // Passing an array to a map-style function works now! No longer an
         // error case!
 //      call_exc("free0_map", llsd::array("a", "b"), map_exc);
@@ -1123,7 +1154,7 @@ namespace tut
                    ("free0_array", "free0_map",
                     "smethod0_array", "smethod0_map",
                     "method0_array", "method0_map"));
-        for (LLSD name : inArray(names))
+        for (LLSD name: inArray(names))
         {
             // Look up the Vars instance for this function.
             Vars* vars(varsfor(name));
@@ -1148,15 +1179,21 @@ namespace tut
     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...
+        set_test_name("call array-style functions with wrong-length arrays");
+        // Could have different wrong-length arrays for *na and for *nb, but
+        // since they both take 5 params...
         LLSD tooshort(llsd::array("this", "array", "too", "short"));
-        for (const LLSD& funcsab : inArray(array_funcs))
+        LLSD toolong (llsd::array("this", "array", "is",  "one", "too", "long"));
+        LLSD badargs (llsd::array(tooshort, toolong));
+        for (const LLSD& toosomething: inArray(badargs))
         {
-            for (const llsd::MapEntry& e : inMap(funcsab))
+            for (const LLSD& funcsab: inArray(array_funcs))
             {
-                call_exc(e.second, tooshort, "requires more arguments");
+                for (const llsd::MapEntry& e: inMap(funcsab))
+                {
+                    // apply() complains about wrong number of array entries
+                    call_logerr(e.second, toosomething, "LL::apply");
+                }
             }
         }
     }
@@ -1164,7 +1201,7 @@ namespace tut
     template<> template<>
     void object::test<20>()
     {
-        set_test_name("call array-style functions with (just right | too long) arrays");
+        set_test_name("call array-style functions with right-size arrays");
         std::vector<U8> binary;
         for (size_t h(0x01), i(0); i < 5; h+= 0x22, ++i)
         {
@@ -1176,40 +1213,25 @@ namespace tut
                                            LLDate("2011-02-03T15:07:00Z"),
                                            LLURI("http://secondlife.com"),
                                            binary)));
-        LLSD argsplus(args);
-        argsplus["a"].append("bogus");
-        argsplus["b"].append("bogus");
         LLSD expect;
-        for (LLSD::String a : ab)
+        for (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() + "'";
+        expect["a"]["cp"] = stringize("'", expect["a"]["cp"].asString(), "'");
         debug("expect: ", expect);
 
-        // Use substantially the same logic for args and argsplus
-        LLSD argsarrays(llsd::array(args, argsplus));
-        // So i==0 selects 'args', i==1 selects argsplus
-        for (LLSD::Integer i(0), iend(argsarrays.size()); i < iend; ++i)
+        for (const LLSD& funcsab: inArray(array_funcs))
         {
-            for (const LLSD& funcsab : inArray(array_funcs))
+            for (LLSD::String a: ab)
             {
-                for (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.
-                }
+                // Reset the Vars instance before each call
+                Vars* vars(varsfor(funcsab[a]));
+                *vars = Vars();
+                work(funcsab[a], args[a]);
+                ensure_llsd(stringize(funcsab[a].asString(), ": expect[\"", a, "\"] mismatch"),
+                            vars->inspect(), expect[a], 7); // 7 bits ~= 2 decimal digits
             }
         }
     }
@@ -1237,7 +1259,7 @@ namespace tut
                         ("a", llsd::array(false, 255, 98.6, 1024.5, "pointer"))
                         ("b", llsd::array("object", LLUUID::generateNewID(), LLDate::now(), LLURI("http://wiki.lindenlab.com/wiki"), LLSD::Binary(boost::begin(binary), boost::end(binary)))));
         LLSD array_overfull(array_full);
-        for (LLSD::String a : ab)
+        for (LLSD::String a: ab)
         {
             array_overfull[a].append("bogus");
         }
@@ -1251,7 +1273,7 @@ namespace tut
         ensure_not_equals("UUID collision",
                           array_full["b"][1].asUUID(), dft_array_full["b"][1].asUUID());
         LLSD map_full, map_overfull;
-        for (LLSD::String a : ab)
+        for (LLSD::String a: ab)
         {
             map_full[a] = zipmap(params[a], array_full[a]);
             map_overfull[a] = map_full[a];
@@ -1292,21 +1314,360 @@ namespace tut
                      "freenb_map_mdft",    "smethodnb_map_mdft",    "methodnb_map_mdft")));
         // Treat (full | overfull) (array | map) the same.
         LLSD argssets(llsd::array(array_full, array_overfull, map_full, map_overfull));
-        for (const LLSD& args : inArray(argssets))
+        for (const LLSD& args: inArray(argssets))
         {
-            for (LLSD::String a : ab)
+            for (LLSD::String a: ab)
             {
-                for (LLSD::String name : inArray(names[a]))
+                for (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"),
+                    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?
                 }
             }
         }
     }
+
+    struct DispatchResult: public LLDispatchListener
+    {
+        using DR = DispatchResult;
+
+        DispatchResult(): LLDispatchListener("results", "op")
+        {
+            add("strfunc",   "return string",       &DR::strfunc);
+            add("voidfunc",  "void function",       &DR::voidfunc);
+            add("emptyfunc", "return empty LLSD",   &DR::emptyfunc);
+            add("intfunc",   "return Integer LLSD", &DR::intfunc);
+            add("llsdfunc",  "return passed LLSD",  &DR::llsdfunc);
+            add("mapfunc",   "return map LLSD",     &DR::mapfunc);
+            add("arrayfunc", "return array LLSD",   &DR::arrayfunc);
+        }
+
+        std::string strfunc(const std::string& str) const { return "got " + str; }
+        void voidfunc()                  const {}
+        LLSD emptyfunc()                 const { return {}; }
+        int  intfunc(int i)              const { return -i; }
+        LLSD llsdfunc(const LLSD& event) const
+        {
+            LLSD result{ event };
+            result["with"] = "string";
+            return result;
+        }
+        LLSD mapfunc(int i, const std::string& str) const
+        {
+            return llsd::map("i", intfunc(i), "str", strfunc(str));
+        }
+        LLSD arrayfunc(int i, const std::string& str) const
+        {
+            return llsd::array(intfunc(i), strfunc(str));
+        }
+    };
+
+    template<> template<>
+    void object::test<23>()
+    {
+        set_test_name("string result");
+        DispatchResult service;
+        LLSD result{ service("strfunc", "a string") };
+        ensure_equals("strfunc() mismatch", result.asString(), "got a string");
+    }
+
+    template<> template<>
+    void object::test<24>()
+    {
+        set_test_name("void result");
+        DispatchResult service;
+        LLSD result{ service("voidfunc", LLSD()) };
+        ensure("voidfunc() returned defined", result.isUndefined());
+    }
+
+    template<> template<>
+    void object::test<25>()
+    {
+        set_test_name("Integer result");
+        DispatchResult service;
+        LLSD result{ service("intfunc", -17) };
+        ensure_equals("intfunc() mismatch", result.asInteger(), 17);
+    }
+
+    template<> template<>
+    void object::test<26>()
+    {
+        set_test_name("LLSD echo");
+        DispatchResult service;
+        LLSD result{ service("llsdfunc", llsd::map("op", "llsdfunc", "reqid", 17)) };
+        ensure_equals("llsdfunc() mismatch", result,
+                      llsd::map("op", "llsdfunc", "reqid", 17, "with", "string"));
+    }
+
+    template<> template<>
+    void object::test<27>()
+    {
+        set_test_name("map LLSD result");
+        DispatchResult service;
+        LLSD result{ service("mapfunc", llsd::array(-12, "value")) };
+        ensure_equals("mapfunc() mismatch", result, llsd::map("i", 12, "str", "got value"));
+    }
+
+    template<> template<>
+    void object::test<28>()
+    {
+        set_test_name("array LLSD result");
+        DispatchResult service;
+        LLSD result{ service("arrayfunc", llsd::array(-8, "word")) };
+        ensure_equals("arrayfunc() mismatch", result, llsd::array(8, "got word"));
+    }
+
+    template<> template<>
+    void object::test<29>()
+    {
+        set_test_name("listener error, no reply");
+        DispatchResult service;
+        tut::call_exc(
+            [&service]()
+            { service.post(llsd::map("op", "nosuchfunc", "reqid", 17)); },
+            "nosuchfunc");
+    }
+
+    template<> template<>
+    void object::test<30>()
+    {
+        set_test_name("listener error with reply");
+        DispatchResult service;
+        LLCaptureListener<LLSD> result;
+        service.post(llsd::map("op", "nosuchfunc", "reqid", 17, "reply", result.getName()));
+        LLSD reply{ result.get() };
+        ensure("no reply", reply.isDefined());
+        ensure_equals("reqid not echoed", reply["reqid"].asInteger(), 17);
+        ensure_has(reply["error"].asString(), "nosuchfunc");
+    }
+
+    template<> template<>
+    void object::test<31>()
+    {
+        set_test_name("listener call to void function");
+        DispatchResult service;
+        LLCaptureListener<LLSD> result;
+        result.set("non-empty");
+        for (const auto& func: StringVec{ "voidfunc", "emptyfunc" })
+        {
+            service.post(llsd::map(
+                             "op", func,
+                             "reqid", 17,
+                             "reply", result.getName()));
+            ensure_equals("reply from " + func, result.get().asString(), "non-empty");
+        }
+    }
+
+    template<> template<>
+    void object::test<32>()
+    {
+        set_test_name("listener call to string function");
+        DispatchResult service;
+        LLCaptureListener<LLSD> result;
+        service.post(llsd::map(
+                         "op", "strfunc",
+                         "args", llsd::array("a string"),
+                         "reqid", 17,
+                         "reply", result.getName()));
+        LLSD reply{ result.get() };
+        ensure_equals("reqid not echoed", reply["reqid"].asInteger(), 17);
+        ensure_equals("bad reply from strfunc", reply["data"].asString(), "got a string");
+    }
+
+    template<> template<>
+    void object::test<33>()
+    {
+        set_test_name("listener call to map function");
+        DispatchResult service;
+        LLCaptureListener<LLSD> result;
+        service.post(llsd::map(
+                         "op", "mapfunc",
+                         "args", llsd::array(-7, "value"),
+                         "reqid", 17,
+                         "reply", result.getName()));
+        LLSD reply{ result.get() };
+        ensure_equals("reqid not echoed", reply["reqid"].asInteger(), 17);
+        ensure_equals("bad i from mapfunc", reply["i"].asInteger(), 7);
+        ensure_equals("bad str from mapfunc", reply["str"], "got value");
+    }
+
+    template<> template<>
+    void object::test<34>()
+    {
+        set_test_name("batched map success");
+        DispatchResult service;
+        LLCaptureListener<LLSD> result;
+        service.post(llsd::map(
+                         "op", llsd::map(
+                             "strfunc", "some string",
+                             "intfunc", 2,
+                             "voidfunc", LLSD(),
+                             "arrayfunc", llsd::array(-5, "other string")),
+                         "reqid", 17,
+                         "reply", result.getName()));
+        LLSD reply{ result.get() };
+        ensure_equals("reqid not echoed", reply["reqid"].asInteger(), 17);
+        reply.erase("reqid");
+        ensure_equals(
+            "bad map batch",
+            reply,
+            llsd::map(
+                "strfunc", "got some string",
+                "intfunc", -2,
+                "voidfunc", LLSD(),
+                "arrayfunc", llsd::array(5, "got other string")));
+    }
+
+    template<> template<>
+    void object::test<35>()
+    {
+        set_test_name("batched map error");
+        DispatchResult service;
+        LLCaptureListener<LLSD> result;
+        service.post(llsd::map(
+                         "op", llsd::map(
+                             "badfunc", 34, // !
+                             "strfunc", "some string",
+                             "intfunc", 2,
+                             "missing", LLSD(), // !
+                             "voidfunc", LLSD(),
+                             "arrayfunc", llsd::array(-5, "other string")),
+                         "reqid", 17,
+                         "reply", result.getName()));
+        LLSD reply{ result.get() };
+        ensure_equals("reqid not echoed", reply["reqid"].asInteger(), 17);
+        reply.erase("reqid");
+        auto error{ reply["error"].asString() };
+        reply.erase("error");
+        ensure_has(error, "badfunc");
+        ensure_has(error, "missing");
+        ensure_equals(
+            "bad partial batch",
+            reply,
+            llsd::map(
+                "strfunc", "got some string",
+                "intfunc", -2,
+                "voidfunc", LLSD(),
+                "arrayfunc", llsd::array(5, "got other string")));
+    }
+
+    template<> template<>
+    void object::test<36>()
+    {
+        set_test_name("batched map exception");
+        DispatchResult service;
+        auto error = tut::call_exc(
+            [&service]()
+            {
+                service.post(llsd::map(
+                                 "op", llsd::map(
+                                     "badfunc", 34, // !
+                                     "strfunc", "some string",
+                                     "intfunc", 2,
+                                     "missing", LLSD(), // !
+                                     "voidfunc", LLSD(),
+                                     "arrayfunc", llsd::array(-5, "other string")),
+                                 "reqid", 17));
+                // no "reply"
+            },
+            "badfunc");
+        ensure_has(error, "missing");
+    }
+
+    template<> template<>
+    void object::test<37>()
+    {
+        set_test_name("batched array success");
+        DispatchResult service;
+        LLCaptureListener<LLSD> result;
+        service.post(llsd::map(
+                         "op", llsd::array(
+                             llsd::array("strfunc", "some string"),
+                             llsd::array("intfunc", 2),
+                             "arrayfunc",
+                             "voidfunc"),
+                         "args", llsd::array(
+                             LLSD(),
+                             LLSD(),
+                             llsd::array(-5, "other string")),
+                         // args array deliberately short, since the default
+                         // [3] is undefined, which should work for voidfunc
+                         "reqid", 17,
+                         "reply", result.getName()));
+        LLSD reply{ result.get() };
+        ensure_equals("reqid not echoed", reply["reqid"].asInteger(), 17);
+        reply.erase("reqid");
+        ensure_equals(
+            "bad array batch",
+            reply,
+            llsd::map(
+                "data", llsd::array(
+                    "got some string",
+                    -2,
+                    llsd::array(5, "got other string"),
+                    LLSD())));
+    }
+
+    template<> template<>
+    void object::test<38>()
+    {
+        set_test_name("batched array error");
+        DispatchResult service;
+        LLCaptureListener<LLSD> result;
+        service.post(llsd::map(
+                         "op", llsd::array(
+                             llsd::array("strfunc", "some string"),
+                             llsd::array("intfunc", 2, "whoops"), // bad form
+                             "arrayfunc",
+                             "voidfunc"),
+                         "args", llsd::array(
+                             LLSD(),
+                             LLSD(),
+                             llsd::array(-5, "other string")),
+                         // args array deliberately short, since the default
+                         // [3] is undefined, which should work for voidfunc
+                         "reqid", 17,
+                         "reply", result.getName()));
+        LLSD reply{ result.get() };
+        ensure_equals("reqid not echoed", reply["reqid"].asInteger(), 17);
+        reply.erase("reqid");
+        auto error{ reply["error"] };
+        reply.erase("error");
+        ensure_has(error, "[1]");
+        ensure_has(error, "unsupported");
+        ensure_equals("bad array batch", reply,
+                      llsd::map("data", llsd::array("got some string")));
+    }
+
+    template<> template<>
+    void object::test<39>()
+    {
+        set_test_name("batched array exception");
+        DispatchResult service;
+        auto error = tut::call_exc(
+            [&service]()
+            {
+                service.post(llsd::map(
+                                 "op", llsd::array(
+                                     llsd::array("strfunc", "some string"),
+                                     llsd::array("intfunc", 2, "whoops"), // bad form
+                                     "arrayfunc",
+                                     "voidfunc"),
+                                 "args", llsd::array(
+                                     LLSD(),
+                                     LLSD(),
+                                     llsd::array(-5, "other string")),
+                                 // args array deliberately short, since the default
+                                 // [3] is undefined, which should work for voidfunc
+                                 "reqid", 17));
+                // no "reply"
+            },
+            "[1]");
+        ensure_has(error, "unsupported");
+    }
 } // namespace tut
diff --git a/indra/llcommon/tests/wrapllerrs.h b/indra/llcommon/tests/wrapllerrs.h
index ebf9f5253a106c8513239ca62aa963a7d26310dd..6978c296b39a3a1077fda95c6b9a62dbfd366bf6 100644
--- a/indra/llcommon/tests/wrapllerrs.h
+++ b/indra/llcommon/tests/wrapllerrs.h
@@ -226,6 +226,11 @@ class CaptureLog : public boost::noncopyable
         return std::dynamic_pointer_cast<CaptureLogRecorder>(mRecorder)->streamto(out);
     }
 
+    friend inline std::ostream& operator<<(std::ostream& out, const CaptureLog& self)
+    {
+        return self.streamto(out);
+    }
+
 private:
     LLError::FatalFunction mFatalFunction;
     LLError::SettingsStoragePtr mOldSettings;
diff --git a/indra/llui/lllineeditor.cpp b/indra/llui/lllineeditor.cpp
index 224f45fa174be6ec1e690eeb1dbbc3f276d0c820..f5d16479fc26061720524e3798c589a7a3f562d0 100644
--- a/indra/llui/lllineeditor.cpp
+++ b/indra/llui/lllineeditor.cpp
@@ -164,7 +164,8 @@ LLLineEditor::LLLineEditor(const LLLineEditor::Params& p)
 	mHighlightColor(p.highlight_color()),
 	mPreeditBgColor(p.preedit_bg_color()),
 	mGLFont(p.font),
-	mContextMenuHandle()
+	mContextMenuHandle(),
+    mShowContextMenu(true)
 {
 	llassert( mMaxLengthBytes > 0 );
 
@@ -825,7 +826,7 @@ BOOL LLLineEditor::handleMiddleMouseDown(S32 x, S32 y, MASK mask)
 BOOL LLLineEditor::handleRightMouseDown(S32 x, S32 y, MASK mask)
 {
 	setFocus(TRUE);
-	if (!LLUICtrl::handleRightMouseDown(x, y, mask))
+    if (!LLUICtrl::handleRightMouseDown(x, y, mask) && getShowContextMenu())
 	{
 		showContextMenu(x, y);
 	}
diff --git a/indra/llui/lllineeditor.h b/indra/llui/lllineeditor.h
index 975ce73898eada0fb08af8214be384a335e9f8b1..28e19ffd1b2c15f1e21ccf4212dee3c89bd2dbdd 100644
--- a/indra/llui/lllineeditor.h
+++ b/indra/llui/lllineeditor.h
@@ -286,7 +286,10 @@ class LLLineEditor
 	void			setBgImage(LLPointer<LLUIImage> image) { mBgImage = image; }
 	void			setBgImageFocused(LLPointer<LLUIImage> image) { mBgImageFocused = image; }
 
-private:
+    void setShowContextMenu(bool show) { mShowContextMenu = show; }
+    bool getShowContextMenu() const { return mShowContextMenu; }
+
+  private:
 	// private helper methods
 
 	void                    pasteHelper(bool is_primary);
@@ -406,6 +409,8 @@ class LLLineEditor
 
 	LLHandle<LLContextMenu> mContextMenuHandle;
 
+    bool mShowContextMenu;
+
 private:
 	// Instances that by default point to the statics but can be overidden in XML.
 	LLPointer<LLUIImage> mBgImage;
diff --git a/indra/llui/lltabcontainer.cpp b/indra/llui/lltabcontainer.cpp
index 44ae56597f8dfb534f8218f1c6954cc740b74184..397f6108baa2208139437d22b81fc2bbf739e3c6 100644
--- a/indra/llui/lltabcontainer.cpp
+++ b/indra/llui/lltabcontainer.cpp
@@ -2152,14 +2152,19 @@ void LLTabContainer::commitHoveredButton(S32 x, S32 y)
 {
 	if (!getTabsHidden() && hasMouseCapture())
 	{
-		for(tuple_list_t::iterator iter = mTabList.begin(); iter != mTabList.end(); ++iter)
+		for (tuple_list_t::iterator iter = mTabList.begin(); iter != mTabList.end(); ++iter)
 		{
-			LLTabTuple* tuple = *iter;
-			S32 local_x = x - tuple->mButton->getRect().mLeft;
-			S32 local_y = y - tuple->mButton->getRect().mBottom;
-			if (tuple->mButton->pointInView(local_x, local_y) && tuple->mButton->getEnabled() && tuple->mButton->getVisible() && !tuple->mTabPanel->getVisible())
+			LLButton* button = (*iter)->mButton;
+			LLPanel* panel = (*iter)->mTabPanel;
+			if (button->getEnabled() && button->getVisible() && !panel->getVisible())
 			{
-				tuple->mButton->onCommit();
+				S32 local_x = x - button->getRect().mLeft;
+				S32 local_y = y - button->getRect().mBottom;
+				if (button->pointInView(local_x, local_y))
+				{
+					button->onCommit();
+					break;
+				}
 			}
 		}
 	}
diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp
index cdf9ccef8ef491c05f1444396a348f5f9007f021..e8cf5ad7c452c0cbfd6fc7dc150f8f4aa2a8e9c1 100644
--- a/indra/llui/lltextbase.cpp
+++ b/indra/llui/lltextbase.cpp
@@ -1613,7 +1613,13 @@ S32 LLTextBase::getLeftOffset(S32 width)
 	case LLFontGL::HCENTER:
 		return mHPad + llmax(0, (mVisibleTextRect.getWidth() - width - mHPad) / 2);
 	case LLFontGL::RIGHT:
-		return mVisibleTextRect.getWidth() - width;
+        {
+            // Font's rendering rounds string size, if value gets rounded
+            // down last symbol might not have enough space to render,
+            // compensate by adding an extra pixel as padding
+            const S32 right_padding = 1;
+            return llmax(mHPad, mVisibleTextRect.getWidth() - width - right_padding);
+        }
 	default:
 		return mHPad;
 	}
diff --git a/indra/llui/llurlentry.cpp b/indra/llui/llurlentry.cpp
index 9f6adcff1b3f2f6b6cfbf2e177d2e9908eff07b9..589471a5618e6f6462f2a14f82d10536e0f50525 100644
--- a/indra/llui/llurlentry.cpp
+++ b/indra/llui/llurlentry.cpp
@@ -35,7 +35,9 @@
 
 #include "llavatarnamecache.h"
 #include "llcachename.h"
+#include "llkeyboard.h"
 #include "llregex.h"
+#include "llscrolllistctrl.h" // for LLUrlEntryKeybinding file parsing
 #include "lltrans.h"
 #include "lluicolortable.h"
 #include "message.h"
@@ -1605,3 +1607,122 @@ std::string LLUrlEntryIPv6::getUrl(const std::string &string) const
 {
 	return string;
 }
+
+
+//
+// LLUrlEntryKeybinding Displays currently assigned key
+//
+LLUrlEntryKeybinding::LLUrlEntryKeybinding()
+    : LLUrlEntryBase()
+    , pHandler(NULL)
+{
+    mPattern = boost::regex(APP_HEADER_REGEX "/keybinding/\\w+(\\?mode=\\w+)?$",
+                            boost::regex::perl | boost::regex::icase);
+    mMenuName = "menu_url_experience.xml";
+
+    initLocalization();
+}
+
+std::string LLUrlEntryKeybinding::getLabel(const std::string& url, const LLUrlLabelCallback& cb)
+{
+    std::string control = getControlName(url);
+
+    std::map<std::string, LLLocalizationData>::iterator iter = mLocalizations.find(control);
+
+    std::string keybind;
+    if (pHandler)
+    {
+        keybind = pHandler->getKeyBindingAsString(getMode(url), control);
+    }
+
+    if (iter != mLocalizations.end())
+    {
+        return iter->second.mLocalization + ": " + keybind;
+    }
+
+    return control + ": " + keybind;
+}
+
+std::string LLUrlEntryKeybinding::getTooltip(const std::string& url) const
+{
+    std::string control = getControlName(url);
+
+    std::map<std::string, LLLocalizationData>::const_iterator iter = mLocalizations.find(control);
+    if (iter != mLocalizations.end())
+    {
+        return iter->second.mTooltip;
+    }
+    return url;
+}
+
+std::string LLUrlEntryKeybinding::getControlName(const std::string& url) const
+{
+    std::string search = "/keybinding/";
+    size_t pos_start = url.find(search);
+    if (pos_start == std::string::npos)
+    {
+        return std::string();
+    }
+    pos_start += search.size();
+
+    size_t pos_end = url.find("?mode=");
+    if (pos_end == std::string::npos)
+    {
+        pos_end = url.size();
+    }
+    return url.substr(pos_start, pos_end - pos_start);
+}
+
+std::string LLUrlEntryKeybinding::getMode(const std::string& url) const
+{
+    std::string search = "?mode=";
+    size_t pos_start = url.find(search);
+    if (pos_start == std::string::npos)
+    {
+        return std::string();
+    }
+    pos_start += search.size();
+    return url.substr(pos_start, url.size() - pos_start);
+}
+
+void LLUrlEntryKeybinding::initLocalization()
+{
+    initLocalizationFromFile("control_table_contents_movement.xml");
+    initLocalizationFromFile("control_table_contents_camera.xml");
+    initLocalizationFromFile("control_table_contents_editing.xml");
+    initLocalizationFromFile("control_table_contents_media.xml");
+}
+
+void LLUrlEntryKeybinding::initLocalizationFromFile(const std::string& filename)
+{
+    LLXMLNodePtr xmlNode;
+    LLScrollListCtrl::Contents contents;
+    if (!LLUICtrlFactory::getLayeredXMLNode(filename, xmlNode))
+    {
+        LL_WARNS() << "Failed to load " << filename << LL_ENDL;
+        return;
+    }
+    LLXUIParser parser;
+    parser.readXUI(xmlNode, contents, filename);
+
+    if (!contents.validateBlock())
+    {
+        LL_WARNS() << "Failed to validate " << filename << LL_ENDL;
+        return;
+    }
+
+    for (LLInitParam::ParamIterator<LLScrollListItem::Params>::const_iterator row_it = contents.rows.begin();
+         row_it != contents.rows.end();
+         ++row_it)
+    {
+        std::string control = row_it->value.getValue().asString();
+        if (!control.empty() && control != "menu_separator")
+        {
+            mLocalizations[control] =
+                LLLocalizationData(
+                                   row_it->columns.begin()->value.getValue().asString(),
+                                   row_it->columns.begin()->tool_tip.getValue()
+                );
+        }
+    }
+}
diff --git a/indra/llui/llurlentry.h b/indra/llui/llurlentry.h
index f07c8fc05c29ba0a356786b2454f05f5ed6ae7c8..731ac25e51479896248b93263bc93412dbd47668 100644
--- a/indra/llui/llurlentry.h
+++ b/indra/llui/llurlentry.h
@@ -563,4 +563,37 @@ class LLUrlEntryIPv6 : public LLUrlEntryBase
 	std::string mHostPath;
 };
 
+class LLKeyBindingToStringHandler;
+
+///
+/// LLUrlEntryKeybinding A way to access keybindings and show currently used one in text.
+/// secondlife:///app/keybinding/control_name
+class LLUrlEntryKeybinding: public LLUrlEntryBase
+{
+public:
+    LLUrlEntryKeybinding();
+    /*virtual*/ std::string getLabel(const std::string& url, const LLUrlLabelCallback& cb);
+    /*virtual*/ std::string getTooltip(const std::string& url) const;
+    void setHandler(LLKeyBindingToStringHandler* handler) {pHandler = handler;}
+private:
+    std::string getControlName(const std::string& url) const;
+    std::string getMode(const std::string& url) const;
+    void initLocalization();
+    void initLocalizationFromFile(const std::string& filename);
+
+    struct LLLocalizationData
+    {
+        LLLocalizationData() {}
+        LLLocalizationData(const std::string& localization, const std::string& tooltip)
+            : mLocalization(localization)
+            , mTooltip(tooltip)
+        {}
+        std::string mLocalization;
+        std::string mTooltip;
+    };
+
+    std::map<std::string, LLLocalizationData> mLocalizations;
+    LLKeyBindingToStringHandler* pHandler;
+};
+
 #endif
diff --git a/indra/llui/llurlregistry.cpp b/indra/llui/llurlregistry.cpp
index c91fc168b2addcc0e274f39785ab47fc930b60c3..e8e932c7d438b9122c1eee37cbeb281dc06f1df1 100644
--- a/indra/llui/llurlregistry.cpp
+++ b/indra/llui/llurlregistry.cpp
@@ -79,6 +79,8 @@ LLUrlRegistry::LLUrlRegistry()
 	registerUrl(new LLUrlEntryPlace());
 	registerUrl(new LLUrlEntryInventory());
     registerUrl(new LLUrlEntryExperienceProfile());
+    mUrlEntryKeybinding = new LLUrlEntryKeybinding();
+    registerUrl(mUrlEntryKeybinding);
 	//LLUrlEntrySL and LLUrlEntrySLLabel have more common pattern, 
 	//so it should be registered in the end of list
 	registerUrl(new LLUrlEntrySL());
@@ -313,3 +315,9 @@ bool LLUrlRegistry::isUrl(const LLWString &text)
 	}
 	return false;
 }
+
+void LLUrlRegistry::setKeybindingHandler(LLKeyBindingToStringHandler* handler)
+{
+    LLUrlEntryKeybinding *entry = (LLUrlEntryKeybinding*)mUrlEntryKeybinding;
+    entry->setHandler(handler);
+}
diff --git a/indra/llui/llurlregistry.h b/indra/llui/llurlregistry.h
index d52f44bec04addb26dc3dfd56e5e114455b210d0..a696cef14f3b0babd87e26fa2338a54dd4812336 100644
--- a/indra/llui/llurlregistry.h
+++ b/indra/llui/llurlregistry.h
@@ -36,6 +36,8 @@
 #include <string>
 #include <vector>
 
+class LLKeyBindingToStringHandler;
+
 /// This default callback for findUrl() simply ignores any label updates
 void LLUrlRegistryNullCallback(const std::string &url,
 							   const std::string &label,
@@ -88,6 +90,9 @@ class LLUrlRegistry final : public LLSingleton<LLUrlRegistry>
 	bool isUrl(const std::string &text);
 	bool isUrl(const LLWString &text);
 
+    // Set handler for url registry to be capable of parsing and populating keybindings
+    void setKeybindingHandler(LLKeyBindingToStringHandler* handler);
+
 private:
 	std::vector<LLUrlEntryBase *> mUrlEntry;
 	LLUrlEntryBase*	mUrlEntryTrusted;
@@ -96,6 +101,7 @@ class LLUrlRegistry final : public LLSingleton<LLUrlRegistry>
 	LLUrlEntryBase* mUrlEntryHTTPLabel;
 	LLUrlEntryBase* mUrlEntrySLLabel;
 	LLUrlEntryBase* mUrlEntryNoLink;
+    LLUrlEntryBase* mUrlEntryKeybinding;
 };
 
 #endif
diff --git a/indra/llwindow/llkeyboard.cpp b/indra/llwindow/llkeyboard.cpp
index 6cf58277ba595fcd51c54aa6816f09496d3941a8..86d12571b3f72874e139898e8ed26b64dbc94158 100644
--- a/indra/llwindow/llkeyboard.cpp
+++ b/indra/llwindow/llkeyboard.cpp
@@ -365,6 +365,45 @@ std::string LLKeyboard::stringFromKey(KEY key, bool translate)
 	return res;
 }
 
+//static
+std::string LLKeyboard::stringFromMouse(EMouseClickType click, bool translate)
+{
+    std::string res;
+    switch (click)
+    {
+        case CLICK_LEFT:
+            res = "LMB";
+            break;
+        case CLICK_MIDDLE:
+            res = "MMB";
+            break;
+        case CLICK_RIGHT:
+            res = "RMB";
+            break;
+        case CLICK_BUTTON4:
+            res = "MB4";
+            break;
+        case CLICK_BUTTON5:
+            res = "MB5";
+            break;
+        case CLICK_DOUBLELEFT:
+            res = "Double LMB";
+            break;
+        default:
+            break;
+    }
+
+    if (translate && !res.empty())
+    {
+        LLKeyStringTranslatorFunc* trans = gKeyboard->mStringTranslator;
+        if (trans != NULL)
+        {
+            res = trans(res.c_str());
+        }
+    }
+    return res;
+}
+
 //static
 std::string LLKeyboard::stringFromAccelerator(MASK accel_mask)
 {
@@ -432,6 +471,18 @@ std::string LLKeyboard::stringFromAccelerator( MASK accel_mask, KEY key )
 	return res;
 }
 
+//static
+std::string LLKeyboard::stringFromAccelerator(MASK accel_mask, EMouseClickType click)
+{
+    std::string res;
+    if (CLICK_NONE == click)
+    {
+        return res;
+    }
+    res.append(stringFromAccelerator(accel_mask));
+    res.append(stringFromMouse(click));
+    return res;
+}
 
 //static
 BOOL LLKeyboard::maskFromString(std::string_view instring, MASK* mask)
diff --git a/indra/llwindow/llkeyboard.h b/indra/llwindow/llkeyboard.h
index 3455d74314e2ce4a57eb1b45558abefd0ff82940..a776a9044be3fac2c764b1c719e6d5d52db54d4f 100644
--- a/indra/llwindow/llkeyboard.h
+++ b/indra/llwindow/llkeyboard.h
@@ -97,8 +97,10 @@ class LLKeyboard
 	static BOOL		maskFromString(std::string_view str, MASK *mask);		// False on failure
 	static BOOL		keyFromString(const std::string& str, KEY *key);			// False on failure
 	static std::string stringFromKey(KEY key, bool translate = true);
+    static std::string stringFromMouse(EMouseClickType click, bool translate = true);
 	static std::string stringFromAccelerator( MASK accel_mask ); // separated for convinience, returns with "+": "Shift+" or "Shift+Alt+"...
 	static std::string stringFromAccelerator( MASK accel_mask, KEY key );
+    static std::string stringFromAccelerator(MASK accel_mask, EMouseClickType click);
 
 	void setCallbacks(LLWindowCallbacks *cbs) { mCallbacks = cbs; }
 	F32				getKeyElapsedTime( KEY key );  // Returns time in seconds since key was pressed.
@@ -131,6 +133,13 @@ class LLKeyboard
 	static std::map<std::string,KEY> sNamesToKeys;
 };
 
+// Interface to get key from assigned command
+class LLKeyBindingToStringHandler
+{
+public:
+    virtual std::string getKeyBindingAsString(const std::string& mode, const std::string& control) const = 0;
+};
+
 extern LLKeyboard *gKeyboard;
 
 #endif
diff --git a/indra/newview/VIEWER_VERSION.txt b/indra/newview/VIEWER_VERSION.txt
index 21c8c7b46b8993bd95ff730695cad0d26e4239f3..0e7b60da8a3ba25d7f31fc658b67a85d7420bf92 100644
--- a/indra/newview/VIEWER_VERSION.txt
+++ b/indra/newview/VIEWER_VERSION.txt
@@ -1 +1 @@
-7.1.1
+7.1.2
\ No newline at end of file
diff --git a/indra/newview/app_settings/shaders/class1/deferred/emissiveF.glsl b/indra/newview/app_settings/shaders/class1/deferred/emissiveF.glsl
index 9e61b6b894da3cf43f6ed52cadd203388a941cf9..c95f791dbf395a039839ebd1ba285fafedd48caa 100644
--- a/indra/newview/app_settings/shaders/class1/deferred/emissiveF.glsl
+++ b/indra/newview/app_settings/shaders/class1/deferred/emissiveF.glsl
@@ -34,6 +34,6 @@ void main()
 {
     // NOTE: when this shader is used, only alpha is being written to
 	float a = diffuseLookup(vary_texcoord0.xy).a*vertex_color.a;
-	frag_color = vec4(0, 0, 0, a);
+	frag_color = max(vec4(0, 0, 0, a), vec4(0));
 }
 
diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp
index e0e5bc5d071fba13beef034bc47d00f3000dde57..1233458b84acf0452036574c05989b5af46efbea 100644
--- a/indra/newview/llagent.cpp
+++ b/indra/newview/llagent.cpp
@@ -511,7 +511,11 @@ void LLAgent::init()
 	
 	// *Note: this is where LLViewerCamera::getInstance() used to be constructed.
 
-	setFlying( gSavedSettings.getBOOL("FlyingAtExit") );
+    bool is_flying = gSavedSettings.getBOOL("FlyingAtExit");
+    if(is_flying)
+    {
+        setFlying(is_flying);
+    }
 
 	*mEffectColor = LLUIColorTable::instance().getColor("EffectColor");
 
@@ -2741,30 +2745,31 @@ void LLAgent::setStartPosition( U32 location_id )
 
     body["HomeLocation"] = homeLocation;
 
-	if (!requestPostCapability("HomeLocation", body,
-		boost::bind(&LLAgent::setStartPositionSuccess, this, _1)))
+    if (!requestPostCapability("HomeLocation", body, 
+            boost::bind(&LLAgent::setStartPositionSuccess, this, _1)))
 	{
-		LLMessageSystem* msg = gMessageSystem;
-		msg->newMessageFast(_PREHASH_SetStartLocationRequest);
-		msg->nextBlockFast(_PREHASH_AgentData);
-		msg->addUUIDFast(_PREHASH_AgentID, gAgentID);
-		msg->addUUIDFast(_PREHASH_SessionID, gAgentSessionID);
-		msg->nextBlockFast(_PREHASH_StartLocationData);
-		// corrected by sim
-		msg->addStringFast(_PREHASH_SimName, "");
-		msg->addU32Fast(_PREHASH_LocationID, homeLocation["LocationId"].asInteger());
-		msg->addVector3Fast(_PREHASH_LocationPos,
-			ll_vector3_from_sdmap(homeLocation["LocationPos"]));
-		msg->addVector3Fast(_PREHASH_LocationLookAt,
-			ll_vector3_from_sdmap(homeLocation["LocationLookAt"]));
-		gAgent.sendReliableMessage();
-	}
-
-    const U32 HOME_INDEX = 1;
-    if( HOME_INDEX == location_id )
-    {
-        setHomePosRegion( mRegionp->getHandle(), getPositionAgent() );
-    }
+		if(LLGridManager::instance().isInSecondlife())
+		{
+			LL_WARNS() << "Unable to post to HomeLocation capability." << LL_ENDL;
+		}
+		else
+		{
+			LLMessageSystem* msg = gMessageSystem;
+			msg->newMessageFast(_PREHASH_SetStartLocationRequest);
+			msg->nextBlockFast(_PREHASH_AgentData);
+			msg->addUUIDFast(_PREHASH_AgentID, gAgentID);
+			msg->addUUIDFast(_PREHASH_SessionID, gAgentSessionID);
+			msg->nextBlockFast(_PREHASH_StartLocationData);
+			// corrected by sim
+			msg->addStringFast(_PREHASH_SimName, "");
+			msg->addU32Fast(_PREHASH_LocationID, homeLocation["LocationId"].asInteger());
+			msg->addVector3Fast(_PREHASH_LocationPos,
+				ll_vector3_from_sdmap(homeLocation["LocationPos"]));
+			msg->addVector3Fast(_PREHASH_LocationLookAt,
+				ll_vector3_from_sdmap(homeLocation["LocationLookAt"]));
+			gAgent.sendReliableMessage();
+		}
+	}
 }
 
 void LLAgent::setStartPositionSuccess(const LLSD &result)
diff --git a/indra/newview/llagentcamera.cpp b/indra/newview/llagentcamera.cpp
index 3e454724689730f6fae63788b9650d61f9108fe6..031b02570618b09a40fd3fe891b33a5fb4001391 100644
--- a/indra/newview/llagentcamera.cpp
+++ b/indra/newview/llagentcamera.cpp
@@ -106,6 +106,12 @@ const F32 GROUND_TO_AIR_CAMERA_TRANSITION_START_TIME = 0.5f;
 
 const F32 OBJECT_EXTENTS_PADDING = 0.5f;
 
+static bool isDisableCameraConstraints()
+{
+	static LLCachedControl<bool> sDisableCameraConstraints(gSavedSettings, "DisableCameraConstraints", false);
+	return sDisableCameraConstraints;
+}
+
 // The agent instance.
 LLAgentCamera gAgentCamera;
 
@@ -591,9 +597,10 @@ LLVector3 LLAgentCamera::calcFocusOffset(LLViewerObject *object, LLVector3 origi
 BOOL LLAgentCamera::calcCameraMinDistance(F32 &obj_min_distance)
 {
 	BOOL soft_limit = FALSE; // is the bounding box to be treated literally (volumes) or as an approximation (avatars)
-	if (!mFocusObject || mFocusObject->isDead() || 
+
+	if (!mFocusObject || mFocusObject->isDead() ||
 		mFocusObject->isMesh() ||
-		ALControlCache::DisableCameraConstraints)
+		isDisableCameraConstraints())
 	{
 		obj_min_distance = 0.f;
 		return TRUE;
@@ -763,39 +770,44 @@ F32 LLAgentCamera::getCameraZoomFraction(bool get_third_person)
 		// already [0,1]
 		return mHUDTargetZoom;
 	}
-	else if (get_third_person || (mFocusOnAvatar && cameraThirdPerson()))
+
+	if (isDisableCameraConstraints())
+	{
+		return mCameraZoomFraction;
+	}
+
+	if (get_third_person || (mFocusOnAvatar && cameraThirdPerson()))
 	{
 		return clamp_rescale(mCameraZoomFraction, MIN_ZOOM_FRACTION, MAX_ZOOM_FRACTION, 1.f, 0.f);
 	}
-	else if (cameraCustomizeAvatar())
+
+	if (cameraCustomizeAvatar())
 	{
 		F32 distance = (F32)mCameraFocusOffsetTarget.magVec();
 		return clamp_rescale(distance, APPEARANCE_MIN_ZOOM, APPEARANCE_MAX_ZOOM, 1.f, 0.f );
 	}
-	else
-	{
-		F32 min_zoom;
-		F32 max_zoom = getCameraMaxZoomDistance();
 
-		F32 distance = (F32)mCameraFocusOffsetTarget.magVec();
-		if (mFocusObject.notNull())
+	F32 min_zoom;
+	F32 max_zoom = getCameraMaxZoomDistance();
+
+	F32 distance = (F32)mCameraFocusOffsetTarget.magVec();
+	if (mFocusObject.notNull())
+	{
+		if (mFocusObject->isAvatar())
 		{
-			if (mFocusObject->isAvatar())
-			{
-				min_zoom = AVATAR_MIN_ZOOM;
-			}
-			else
-			{
-				min_zoom = OBJECT_MIN_ZOOM;
-			}
+			min_zoom = AVATAR_MIN_ZOOM;
 		}
 		else
 		{
-			min_zoom = LAND_MIN_ZOOM;
+			min_zoom = OBJECT_MIN_ZOOM;
 		}
-
-		return clamp_rescale(distance, min_zoom, max_zoom, 1.f, 0.f);
 	}
+	else
+	{
+		min_zoom = LAND_MIN_ZOOM;
+	}
+
+	return clamp_rescale(distance, min_zoom, max_zoom, 1.f, 0.f);
 }
 
 void LLAgentCamera::setCameraZoomFraction(F32 fraction)
@@ -808,6 +820,10 @@ void LLAgentCamera::setCameraZoomFraction(F32 fraction)
 	{
 		mHUDTargetZoom = fraction;
 	}
+	else if (isDisableCameraConstraints())
+	{
+		mCameraZoomFraction = fraction;
+	}
 	else if (mFocusOnAvatar && cameraThirdPerson())
 	{
 		mCameraZoomFraction = rescale(fraction, 0.f, 1.f, MAX_ZOOM_FRACTION, MIN_ZOOM_FRACTION);
@@ -845,6 +861,7 @@ void LLAgentCamera::setCameraZoomFraction(F32 fraction)
 // [/RLVa:KB]
 //		mCameraFocusOffsetTarget = camera_offset_dir * rescale(fraction, 0.f, 1.f, max_zoom, min_zoom);
 	}
+
 	startCameraAnimation();
 }
 
@@ -949,15 +966,15 @@ void LLAgentCamera::cameraZoomIn(const F32 fraction)
 		return;
 	}
 
-
-	LLVector3d	camera_offset_unit(mCameraFocusOffsetTarget);
-	F32 min_zoom = LAND_MIN_ZOOM;
+	LLVector3d camera_offset_unit(mCameraFocusOffsetTarget);
 	F32 current_distance = (F32)camera_offset_unit.normalize();
 	F32 new_distance = current_distance * fraction;
 
-	static LLCachedControl<bool> disable_camera_limits(gSavedSettings, "AlchemyCameraNoZoomLimit", false);
-	if (!disable_camera_limits)
+	// Unless camera is unlocked
+	if (!isDisableCameraConstraints())
 	{
+		F32 min_zoom = LAND_MIN_ZOOM;
+
 		// Don't move through focus point
 		if (mFocusObject)
 		{
@@ -972,30 +989,17 @@ void LLAgentCamera::cameraZoomIn(const F32 fraction)
 				min_zoom = OBJECT_MIN_ZOOM;
 			}
 		}
-	
-		new_distance = llmax(new_distance, min_zoom); 
-	}
 
-	F32 max_distance = getCameraMaxZoomDistance(true);
+		new_distance = llmax(new_distance, min_zoom);
 
-    max_distance = llmin(max_distance, current_distance * 4.f); //Scaled max relative to current distance.  MAINT-3154
+		F32 max_distance = getCameraMaxZoomDistance();
+		max_distance = llmin(max_distance, current_distance * 4.f); //Scaled max relative to current distance.  MAINT-3154
+		new_distance = llmin(new_distance, max_distance);
 
-	if (new_distance > max_distance)
-	{
-		new_distance = max_distance;
-
-		/*
-		// Unless camera is unlocked
-		if (!LLViewerCamera::sDisableCameraConstraints)
+		if (cameraCustomizeAvatar())
 		{
-			return;
+			new_distance = llclamp(new_distance, APPEARANCE_MIN_ZOOM, APPEARANCE_MAX_ZOOM);
 		}
-		*/
-	}
-
-	if(cameraCustomizeAvatar())
-	{
-		new_distance = llclamp( new_distance, APPEARANCE_MIN_ZOOM, APPEARANCE_MAX_ZOOM );
 	}
 
 // [RLVa:KB] - Checked: 2.0.0
@@ -1026,19 +1030,22 @@ void LLAgentCamera::cameraOrbitIn(const F32 meters)
 			changeCameraToMouselook(FALSE);
 		}
 
-		mCameraZoomFraction = llclamp(mCameraZoomFraction, MIN_ZOOM_FRACTION, MAX_ZOOM_FRACTION);
+		if (!isDisableCameraConstraints())
+		{
+			mCameraZoomFraction = llclamp(mCameraZoomFraction, MIN_ZOOM_FRACTION, MAX_ZOOM_FRACTION);
+		}
 	}
 	else
 	{
 		LLVector3d	camera_offset_unit(mCameraFocusOffsetTarget);
 		F32 current_distance = (F32)camera_offset_unit.normalize();
 		F32 new_distance = current_distance - meters;
-		
-		static LLCachedControl<bool> disable_camera_limits(gSavedSettings, "AlchemyCameraNoZoomLimit", false);
-		if (!disable_camera_limits)
+
+		// Unless camera is unlocked
+		if (!isDisableCameraConstraints())
 		{
 			F32 min_zoom = LAND_MIN_ZOOM;
-			
+
 			// Don't move through focus point
 			if (mFocusObject.notNull())
 			{
@@ -1053,24 +1060,16 @@ void LLAgentCamera::cameraOrbitIn(const F32 meters)
 			}
 
 			new_distance = llmax(new_distance, min_zoom);
-		}
 
-		F32 max_distance = getCameraMaxZoomDistance(true);
+			F32 max_distance = getCameraMaxZoomDistance();
+			new_distance = llmin(new_distance, max_distance);
 
-		if (new_distance > max_distance)
-		{
-			// Unless camera is unlocked
-            if (!ALControlCache::DisableCameraConstraints)
+			if (CAMERA_MODE_CUSTOMIZE_AVATAR == getCameraMode())
 			{
-				return;
+				new_distance = llclamp(new_distance, APPEARANCE_MIN_ZOOM, APPEARANCE_MAX_ZOOM);
 			}
 		}
 
-		if( CAMERA_MODE_CUSTOMIZE_AVATAR == getCameraMode() )
-		{
-			new_distance = llclamp( new_distance, APPEARANCE_MIN_ZOOM, APPEARANCE_MAX_ZOOM );
-		}
-
 // [RLVa:KB] - Checked: 2.0.0
 		if ( (RlvActions::isRlvEnabled()) && (!allowFocusOffsetChange(new_distance * camera_offset_unit)) )
 			return;
@@ -1082,7 +1081,6 @@ void LLAgentCamera::cameraOrbitIn(const F32 meters)
 	}
 }
 
-
 //-----------------------------------------------------------------------------
 // cameraPanIn()
 //-----------------------------------------------------------------------------
@@ -1928,7 +1926,8 @@ LLVector3d LLAgentCamera::calcCameraPositionTargetGlobal(BOOL *hit_limit)
 				local_camera_offset = gAgent.getFrameAgent().rotateToAbsolute( local_camera_offset );
 			}
 
-			if (!mCameraCollidePlane.isExactlyZero() && (!isAgentAvatarValid() || !gAgentAvatarp->isSitting()))
+			if (!isDisableCameraConstraints() && !mCameraCollidePlane.isExactlyZero() &&
+				(!isAgentAvatarValid() || !gAgentAvatarp->isSitting()))
 			{
 				LLVector3 plane_normal;
 				plane_normal.setVec(mCameraCollidePlane.mV);
@@ -2050,7 +2049,7 @@ LLVector3d LLAgentCamera::calcCameraPositionTargetGlobal(BOOL *hit_limit)
 
 	auto& worldInst = LLWorld::instance();
 
-	if (!ALControlCache::DisableCameraConstraints && !gAgent.isGodlike())
+	if (!isDisableCameraConstraints() && !gAgent.isGodlike())
 	{
 		LLViewerRegion* regionp = worldInst.getRegionFromPosGlobal(camera_position_global);
 		bool constrain = true;
@@ -2126,16 +2125,14 @@ LLVector3d LLAgentCamera::calcCameraPositionTargetGlobal(BOOL *hit_limit)
 
 	// Don't let camera go underground
 	F32 camera_min_off_ground = getCameraMinOffGround();
-
 	camera_land_height = worldInst.resolveLandHeightGlobal(camera_position_global);
-
-	if (camera_position_global.mdV[VZ] < camera_land_height + camera_min_off_ground)
+	F32 minZ = llmax(F_ALMOST_ZERO, camera_land_height + camera_min_off_ground);
+	if (camera_position_global.mdV[VZ] < minZ)
 	{
-		camera_position_global.mdV[VZ] = camera_land_height + camera_min_off_ground;
+		camera_position_global.mdV[VZ] = minZ;
 		isConstrained = TRUE;
 	}
 
-
 	if (hit_limit)
 	{
 		*hit_limit = isConstrained;
@@ -2336,7 +2333,13 @@ F32 LLAgentCamera::getCameraMinOffGround()
 	{
 		return 0.f;
 	}
-    return ALControlCache::DisableCameraConstraints ? -1000.f : 0.5f;
+
+	if (isDisableCameraConstraints())
+	{
+		return -1000.f;
+	}
+
+	return 0.5f;
 }
 
 
diff --git a/indra/newview/llagenthandler.cpp b/indra/newview/llagenthandler.cpp
index f2b46e0e906e272e099e7e93ed6f4967860014c0..d98f943bcaa2b40feb2b6c2c365ec44add495ae6 100644
--- a/indra/newview/llagenthandler.cpp
+++ b/indra/newview/llagenthandler.cpp
@@ -52,7 +52,8 @@ class LLAgentHandler : public LLCommandHandler
             return true; // don't block, will fail later
         }
 
-        if (nav_type == NAV_TYPE_CLICKED)
+		if (nav_type == NAV_TYPE_CLICKED
+			|| nav_type == NAV_TYPE_EXTERNAL)
         {
             return true;
         }
diff --git a/indra/newview/llappearancemgr.cpp b/indra/newview/llappearancemgr.cpp
index 65a486de82b5ad309bb0424b0ac1c37cc5204960..ecdb0c91a6a110da152a0d06a7b09e8bcbaa6a7d 100644
--- a/indra/newview/llappearancemgr.cpp
+++ b/indra/newview/llappearancemgr.cpp
@@ -3074,9 +3074,23 @@ void LLAppearanceMgr::wearInventoryCategory(LLInventoryCategory* category, bool
     else
 	{
 		selfStartPhase("wear_inventory_category_fetch");
-		callAfterCategoryFetch(category->getUUID(),boost::bind(&LLAppearanceMgr::wearCategoryFinal,
-															   &LLAppearanceMgr::instance(),
-															   category->getUUID(), copy, append));
+        if (AISAPI::isAvailable() && category->getPreferredType() == LLFolderType::FT_OUTFIT)
+        {
+            // for reliability just fetch it whole, linked items included
+            LLUUID cat_id = category->getUUID();
+            LLInventoryModelBackgroundFetch::getInstance()->fetchFolderAndLinks(
+                                cat_id,
+                                [cat_id, copy, append]
+                                {
+                                    LLAppearanceMgr::instance().wearCategoryFinal(cat_id, copy, append);
+                                });
+        }
+        else
+        {
+            callAfterCategoryFetch(category->getUUID(), boost::bind(&LLAppearanceMgr::wearCategoryFinal,
+                                                                    &LLAppearanceMgr::instance(),
+                                                                    category->getUUID(), copy, append));
+        }
 	}
 }
 
@@ -3085,7 +3099,7 @@ S32 LLAppearanceMgr::getActiveCopyOperations() const
 	return LLCallAfterInventoryCopyMgr::getInstanceCount(); 
 }
 
-void LLAppearanceMgr::wearCategoryFinal(LLUUID& cat_id, bool copy_items, bool append)
+void LLAppearanceMgr::wearCategoryFinal(const LLUUID& cat_id, bool copy_items, bool append)
 {
 	LL_INFOS("Avatar") << self_av_string() << "starting" << LL_ENDL;
 
@@ -5017,30 +5031,20 @@ class CallAfterCategoryFetchStage1: public LLInventoryFetchDescendentsObserver
 
 void callAfterCOFFetch(nullary_func_t cb)
 {
-    LLUUID cat_id = LLAppearanceMgr::instance().getCOF();
-    LLViewerInventoryCategory* cat = gInventory.getCategory(cat_id);
-
     if (AISAPI::isAvailable())
     {
-        // Mark cof (update timer) so that background fetch won't request it
-        cat->setFetching(LLViewerInventoryCategory::FETCH_RECURSIVE);
         // For reliability assume that we have no relevant cache, so
         // fetch cof along with items cof's links point to.
-        AISAPI::FetchCOF([cb](const LLUUID& id)
-                         {
-                             cb();
-                             LLUUID cat_id = LLAppearanceMgr::instance().getCOF();
-                             LLViewerInventoryCategory* cat = gInventory.getCategory(cat_id);
-                             if (cat)
-                             {
-                                 cat->setFetching(LLViewerInventoryCategory::FETCH_NONE);
-                             }
-                         });
+        LLInventoryModelBackgroundFetch::getInstance()->fetchCOF(cb);
     }
     else
     {
         LL_INFOS() << "AIS API v3 not available, using callAfterCategoryFetch" << LL_ENDL;
-        // startup should have marked folder as fetching, remove that
+        LLUUID cat_id = LLAppearanceMgr::instance().getCOF();
+        LLViewerInventoryCategory* cat = gInventory.getCategory(cat_id);
+
+        // Special case, startup should have marked cof as FETCH_RECURSIVE
+        // to prevent dupplicate request, remove that
         cat->setFetching(LLViewerInventoryCategory::FETCH_NONE);
         callAfterCategoryFetch(cat_id, cb);
     }
@@ -5062,30 +5066,16 @@ void callAfterCategoryFetch(const LLUUID& cat_id, nullary_func_t cb)
 
 void callAfterCategoryLinksFetch(const LLUUID &cat_id, nullary_func_t cb)
 {
-    LLViewerInventoryCategory *cat = gInventory.getCategory(cat_id);
     if (AISAPI::isAvailable())
     {
-        // Mark folder (update timer) so that background fetch won't request it
-        cat->setFetching(LLViewerInventoryCategory::FETCH_RECURSIVE);
         // Assume that we have no relevant cache. Fetch folder, and items folder's links point to.
-        AISAPI::FetchCategoryLinks(cat_id,
-            [cb, cat_id](const LLUUID &id)
-            {
-                cb();
-                LLViewerInventoryCategory *cat = gInventory.getCategory(cat_id);
-                if (cat)
-                {
-                    cat->setFetching(LLViewerInventoryCategory::FETCH_NONE);
-                }
-            });
-        }
-        else
-        {
-            LL_WARNS() << "AIS API v3 not available, can't use AISAPI::FetchCOF" << LL_ENDL;
-            // startup should have marked folder as fetching, remove that
-            cat->setFetching(LLViewerInventoryCategory::FETCH_NONE);
-            callAfterCategoryFetch(cat_id, cb);
-        }
+        LLInventoryModelBackgroundFetch::getInstance()->fetchFolderAndLinks(cat_id, cb);
+    }
+    else
+    {
+        LL_WARNS() << "AIS API v3 not available, can't use AISAPI::FetchCOF" << LL_ENDL;
+        callAfterCategoryFetch(cat_id, cb);
+    }
     
 }
 
diff --git a/indra/newview/llappearancemgr.h b/indra/newview/llappearancemgr.h
index d7bb1c1dbe6ac1fedf830078c6dbbc7f547a0e24..d88c1d797e2d23dff077240cc5a80a8f2eca796d 100644
--- a/indra/newview/llappearancemgr.h
+++ b/indra/newview/llappearancemgr.h
@@ -63,7 +63,7 @@ class LLAppearanceMgr final : public LLSingleton<LLAppearanceMgr>
 // [/RLVa:KB]
 	void wearInventoryCategory(LLInventoryCategory* category, bool copy, bool append);
 	void wearInventoryCategoryOnAvatar(LLInventoryCategory* category, bool append);
-	void wearCategoryFinal(LLUUID& cat_id, bool copy_items, bool append);
+	void wearCategoryFinal(const LLUUID& cat_id, bool copy_items, bool append);
 	void wearOutfitByName(const std::string& name);
 	void changeOutfit(bool proceed, const LLUUID& category, bool append);
 	void replaceCurrentOutfit(const LLUUID& new_outfit);
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index cb88d73862aabf1b96645779697c3569dab8b5e0..1a5a40728bbfa70273cc73319eefb675a017f75e 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -208,6 +208,7 @@
 #include "llhudeffecttrail.h"
 #include "llvectorperfoptions.h"
 #include "llslurl.h"
+#include "llurlregistry.h"
 #include "llwatchdog.h"
 
 // Included so that constants/settings might be initialized
@@ -4186,6 +4187,7 @@ void LLAppViewer::loadKeyBindings()
 			LL_ERRS("InitInfo") << "Unable to open default key bindings from " << key_bindings_file << LL_ENDL;
 		}
 	}
+    LLUrlRegistry::instance().setKeybindingHandler(&gViewerInput);
 }
 
 void LLAppViewer::purgeCache()
diff --git a/indra/newview/llavatariconctrl.cpp b/indra/newview/llavatariconctrl.cpp
index 3374a1bc31f89e3a3e3847eb392ede7393fc8cf9..00924b3dea1b8f87a1c3d68eb898dda5548f2d10 100644
--- a/indra/newview/llavatariconctrl.cpp
+++ b/indra/newview/llavatariconctrl.cpp
@@ -247,6 +247,11 @@ void LLAvatarIconCtrl::setValue(const LLSD& value)
 				app->addObserver(mAvatarId, this);
 				app->sendAvatarPropertiesRequest(mAvatarId);
 			}
+            else if (gAgentID == mAvatarId)
+            {
+                // Always track any changes to our own icon id
+                app->addObserver(mAvatarId, this);
+            }
 		}
 	}
 	else
diff --git a/indra/newview/llavatarrenderinfoaccountant.cpp b/indra/newview/llavatarrenderinfoaccountant.cpp
index 4f16a3063aec81c0195a14fc9c78febadec1241b..6f189c1b9dacaa523b4d8d9e1fb777e33b1d6868 100644
--- a/indra/newview/llavatarrenderinfoaccountant.cpp
+++ b/indra/newview/llavatarrenderinfoaccountant.cpp
@@ -79,8 +79,14 @@ void LLAvatarRenderInfoAccountant::avatarRenderInfoGetCoro(std::string url, U64
     LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t 
         httpAdapter(std::make_shared<LLCoreHttpUtil::HttpCoroutineAdapter>("AvatarRenderInfoAccountant", httpPolicy));
     LLCore::HttpRequest::ptr_t httpRequest(std::make_shared<LLCore::HttpRequest>());
+    LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions);
 
-    LLSD result = httpAdapter->getAndSuspend(httpRequest, url);
+    // Going to request each 15 seconds either way, so don't wait
+    // too long and don't repeat
+    httpOpts->setRetries(0);
+    httpOpts->setTimeout(SECS_BETWEEN_REGION_REQUEST);
+
+    LLSD result = httpAdapter->getAndSuspend(httpRequest, url, httpOpts);
 
     LLWorld *world_inst = LLWorld::getInstance();
     if (!world_inst)
@@ -190,6 +196,11 @@ void LLAvatarRenderInfoAccountant::avatarRenderInfoReportCoro(std::string url, U
     LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t
         httpAdapter(std::make_shared<LLCoreHttpUtil::HttpCoroutineAdapter>("AvatarRenderInfoAccountant", httpPolicy));
     LLCore::HttpRequest::ptr_t httpRequest(std::make_shared<LLCore::HttpRequest>());
+    LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions);
+
+    // Going to request each 60+ seconds, timeout is 30s.
+    // Don't repeat too often, will be sending newer data soon
+    httpOpts->setRetries(1);
 
     LLWorld *world_inst = LLWorld::getInstance();
     if (!world_inst)
@@ -256,7 +267,7 @@ void LLAvatarRenderInfoAccountant::avatarRenderInfoReportCoro(std::string url, U
 
     regionp = NULL;
     world_inst = NULL;
-    LLSD result = httpAdapter->postAndSuspend(httpRequest, url, report);
+    LLSD result = httpAdapter->postAndSuspend(httpRequest, url, report, httpOpts);
 
     world_inst = LLWorld::getInstance();
     if (!world_inst)
diff --git a/indra/newview/llconversationmodel.h b/indra/newview/llconversationmodel.h
index 72572ec3bce6a861088c83d893d6a1ee96df3fdd..7efe03da21c5eab77ef349a66750fe020ac2bdfc 100644
--- a/indra/newview/llconversationmodel.h
+++ b/indra/newview/llconversationmodel.h
@@ -40,7 +40,7 @@ class LLConversationItem;
 class LLConversationItemSession;
 class LLConversationItemParticipant;
 
-typedef std::map<LLUUID, LLConversationItem*> conversations_items_map;
+typedef std::map<LLUUID, LLPointer<LLConversationItem> > conversations_items_map;
 typedef std::map<LLUUID, LLFolderViewItem*> conversations_widgets_map;
 
 typedef std::vector<std::string> menuentry_vec_t;
diff --git a/indra/newview/lldrawable.cpp b/indra/newview/lldrawable.cpp
index f2ebbae0d48d908a3aa3c7c753ca02fecc7fd0ba..257a21ef5e9bff4258b03c4c50bdf58d69808aa4 100644
--- a/indra/newview/lldrawable.cpp
+++ b/indra/newview/lldrawable.cpp
@@ -758,19 +758,6 @@ void LLDrawable::movePartition()
 	if (part)
 	{
 		part->move(this, getSpatialGroup());
-
-		// SL-18251 "On-screen animesh characters using pelvis offset animations
-		// disappear when root goes off-screen"
-		//
-		// Update extents of the root node when Control Avatar changes it's bounds
-		if (mRenderType == LLPipeline::RENDER_TYPE_CONTROL_AV && isRoot())
-		{
-			LLControlAvatar* controlAvatar = dynamic_cast<LLControlAvatar*>(getVObj().get());
-			if (controlAvatar && controlAvatar->mControlAVBridge)
-			{
-				((LLSpatialGroup*)controlAvatar->mControlAVBridge->mOctree->getListener(0))->setState(LLViewerOctreeGroup::DIRTY);
-			}
-		}
 	}
 }
 
diff --git a/indra/newview/llfloaterimcontainer.cpp b/indra/newview/llfloaterimcontainer.cpp
index 6d41754195d7e5c3b933224b70ebb40151fa0c27..a9f7308e32cd90402476b61c54f1672649659565 100644
--- a/indra/newview/llfloaterimcontainer.cpp
+++ b/indra/newview/llfloaterimcontainer.cpp
@@ -158,6 +158,20 @@ void LLFloaterIMContainer::sessionIDUpdated(const LLUUID& old_session_id, const
 	LLFloaterIMSessionTab::addToHost(new_session_id);
 }
 
+
+LLConversationItem* LLFloaterIMContainer::getSessionModel(const LLUUID& session_id)
+{
+    conversations_items_map::iterator iter = mConversationsItems.find(session_id);
+    if (iter == mConversationsItems.end())
+    {
+        return NULL;
+    }
+    else
+    {
+        return iter->second.get();
+    }
+}
+
 void LLFloaterIMContainer::sessionRemoved(const LLUUID& session_id)
 {
 	removeConversationListItem(session_id);
@@ -614,7 +628,8 @@ void LLFloaterIMContainer::handleConversationModelEvent(const LLSD& event)
 	}
 	else if (type == "add_participant")
 	{
-		LLConversationItemSession* session_model = dynamic_cast<LLConversationItemSession*>(mConversationsItems[session_id]);
+        LLConversationItem* item = getSessionModel(session_id);
+		LLConversationItemSession* session_model = dynamic_cast<LLConversationItemSession*>(item);
 		LLConversationItemParticipant* participant_model = (session_model ? session_model->findParticipant(participant_id) : NULL);
 		LLIMModel::LLIMSession * im_sessionp = LLIMModel::getInstance()->findIMSession(session_id);
 		if (!participant_view && session_model && participant_model)
@@ -1810,10 +1825,9 @@ BOOL LLFloaterIMContainer::selectConversationPair(const LLUUID& session_id, bool
 
 void LLFloaterIMContainer::setTimeNow(const LLUUID& session_id, const LLUUID& participant_id)
 {
-	LLConversationItemSession* item = dynamic_cast<LLConversationItemSession*>(get_ptr_in_map(mConversationsItems,session_id));
+	LLConversationItemSession* item = dynamic_cast<LLConversationItemSession*>(getSessionModel(session_id));
 	if (item)
 	{
-		item->setTimeNow(participant_id);
 		mConversationViewModel.requestSortAll();
 		mConversationsRoot->arrangeAll();
 	}
@@ -1822,7 +1836,7 @@ void LLFloaterIMContainer::setTimeNow(const LLUUID& session_id, const LLUUID& pa
 void LLFloaterIMContainer::setNearbyDistances()
 {
 	// Get the nearby chat session: that's the one with uuid nul
-	LLConversationItemSession* item = dynamic_cast<LLConversationItemSession*>(get_ptr_in_map(mConversationsItems,LLUUID()));
+    LLConversationItemSession* item = dynamic_cast<LLConversationItemSession*>(getSessionModel(LLUUID()));
 	if (item)
 	{
 		// Get the positions of the nearby avatars and their ids
diff --git a/indra/newview/llfloaterimcontainer.h b/indra/newview/llfloaterimcontainer.h
index d7af1ede37bbef242a5e60f0a09ffff0223a03cb..af0e5bdb2e24c2e0d4f15d9f4c04c07f2f2c8722 100644
--- a/indra/newview/llfloaterimcontainer.h
+++ b/indra/newview/llfloaterimcontainer.h
@@ -106,7 +106,7 @@ class LLFloaterIMContainer final
 	LLConversationViewModel& getRootViewModel() { return mConversationViewModel; }
     LLUUID getSelectedSession() { return mSelectedSession; }
     void setSelectedSession(LLUUID sessionID) { mSelectedSession = sessionID; }
-	LLConversationItem* getSessionModel(const LLUUID& session_id) { return get_ptr_in_map(mConversationsItems,session_id); }
+	LLConversationItem* getSessionModel(const LLUUID& session_id);
 	LLConversationSort& getSortOrder() { return mConversationViewModel.getSorter(); }
 
 	// Handling of lists of participants is public so to be common with llfloatersessiontab
diff --git a/indra/newview/llfloaterpreference.cpp b/indra/newview/llfloaterpreference.cpp
index 3745c6d4aff1bc94ae92e408f9a39272c6156ba6..e6f50bd706d4a6065f79e0a28ae4a7637ddcd7af 100644
--- a/indra/newview/llfloaterpreference.cpp
+++ b/indra/newview/llfloaterpreference.cpp
@@ -282,11 +282,49 @@ void fractionFromDecimal(F32 decimal_val, S32& numerator, S32& denominator)
 		}
 	}
 }
-// static
-std::string LLFloaterPreference::sSkin = "";
+
+// handle secondlife:///app/worldmap/{NAME}/{COORDS} URLs
+// Also see LLUrlEntryKeybinding, the value of this command type
+// is ability to show up to date value in chat
+class LLKeybindingHandler: public LLCommandHandler
+{
+public:
+    // requires trusted browser to trigger
+    LLKeybindingHandler(): LLCommandHandler("keybinding", UNTRUSTED_CLICK_ONLY)
+    {
+    }
+
+    bool handle(const LLSD& params, const LLSD& query_map,
+                const std::string& grid, LLMediaCtrl* web)
+    {
+        if (params.size() < 1) return false;
+
+        LLFloaterPreference* prefsfloater = dynamic_cast<LLFloaterPreference*>
+            (LLFloaterReg::showInstance("preferences"));
+
+        if (prefsfloater)
+        {
+            // find 'controls' panel and bring it the front
+            LLTabContainer* tabcontainer = prefsfloater->getChild<LLTabContainer>("pref core");
+            LLPanel* panel = prefsfloater->getChild<LLPanel>("controls");
+            if (tabcontainer && panel)
+            {
+                tabcontainer->selectTabPanel(panel);
+            }
+        }
+
+        return true;
+    }
+};
+LLKeybindingHandler gKeybindHandler;
+
+
 //////////////////////////////////////////////
 // LLFloaterPreference
 
+// static
+std::string LLFloaterPreference::sSkin = "";
+
 LLFloaterPreference::LLFloaterPreference(const LLSD& key)
 	: LLFloater(key),
 	mGotPersonalInfo(false),
diff --git a/indra/newview/llfloaterurlentry.cpp b/indra/newview/llfloaterurlentry.cpp
index b012c740e367140a7361168025ee84acf9148f31..bea28d24c09f8c6c25dee8fbbea5c95de90c383b 100644
--- a/indra/newview/llfloaterurlentry.cpp
+++ b/indra/newview/llfloaterurlentry.cpp
@@ -175,10 +175,9 @@ void LLFloaterURLEntry::onBtnOK( void* userdata )
 	// We assume that an empty scheme is an http url, as this is how we will treat it.
 	if(scheme == "")
 	{
-		scheme = "http";
+		scheme = "https";
 	}
 
-	// Discover the MIME type only for "http" scheme.
 	if(!media_url.empty() && 
 	   (scheme == "http" || scheme == "https"))
 	{
@@ -204,13 +203,18 @@ void LLFloaterURLEntry::getMediaTypeCoro(std::string url, LLHandle<LLFloater> pa
     LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t
         httpAdapter(std::make_shared<LLCoreHttpUtil::HttpCoroutineAdapter>("getMediaTypeCoro", httpPolicy));
     LLCore::HttpRequest::ptr_t httpRequest(std::make_shared<LLCore::HttpRequest>());
+    LLCore::HttpHeaders::ptr_t httpHeaders(new LLCore::HttpHeaders);
     LLCore::HttpOptions::ptr_t httpOpts(std::make_shared<LLCore::HttpOptions>());
 
+    httpOpts->setFollowRedirects(true);
     httpOpts->setHeadersOnly(true);
 
+    httpHeaders->append(HTTP_OUT_HEADER_ACCEPT, "*/*");
+    httpHeaders->append(HTTP_OUT_HEADER_COOKIE, "");
+
     LL_INFOS("HttpCoroutineAdapter", "genericPostCoro") << "Generic POST for " << url << LL_ENDL;
 
-    LLSD result = httpAdapter->getAndSuspend(httpRequest, url, httpOpts);
+    LLSD result = httpAdapter->getRawAndSuspend(httpRequest, url, httpOpts, httpHeaders);
 
     LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
     LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
@@ -226,12 +230,6 @@ void LLFloaterURLEntry::getMediaTypeCoro(std::string url, LLHandle<LLFloater> pa
     // which have no mime type set.
     std::string resolvedMimeType = LLMIMETypes::getDefaultMimeType();
 
-    if (!status)
-    {
-        floaterUrlEntry->headerFetchComplete(status.getType(), resolvedMimeType);
-        return;
-    }
-
     LLSD resultHeaders = httpResults[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_HEADERS];
 
     if (resultHeaders.has(HTTP_IN_HEADER_CONTENT_TYPE))
diff --git a/indra/newview/llfloaterworldmap.cpp b/indra/newview/llfloaterworldmap.cpp
index 1808584208013f6e6987d52a65cd1d0ec6be5641..b16f0f54866d5de256eeec77d2d028c21a7da7ac 100644
--- a/indra/newview/llfloaterworldmap.cpp
+++ b/indra/newview/llfloaterworldmap.cpp
@@ -128,10 +128,27 @@ static const F32 ZOOM_MAX = 128.f;
 class LLWorldMapHandler : public LLCommandHandler
 {
 public:
-	// requires trusted browser to trigger
-	LLWorldMapHandler() : LLCommandHandler("worldmap", UNTRUSTED_CLICK_ONLY ) { }
-	
-	bool handle(const LLSD& params,
+    LLWorldMapHandler() : LLCommandHandler("worldmap", UNTRUSTED_THROTTLE)
+    {
+    }
+
+    virtual bool canHandleUntrusted(
+        const LLSD& params,
+        const LLSD& query_map,
+        LLMediaCtrl* web,
+        const std::string& nav_type)
+    {
+        if (nav_type == NAV_TYPE_CLICKED
+            || nav_type == NAV_TYPE_EXTERNAL)
+        {
+            // NAV_TYPE_EXTERNAL will be throttled
+            return true;
+        }
+
+        return false;
+    }
+
+    bool handle(const LLSD& params,
                 const LLSD& query_map,
                 const std::string& grid,
                 LLMediaCtrl* web)
@@ -167,12 +184,32 @@ LLWorldMapHandler gWorldMapHandler;
 class LLMapTrackAvatarHandler : public LLCommandHandler
 {
 public:
-	// requires trusted browser to trigger
-	LLMapTrackAvatarHandler() : LLCommandHandler("maptrackavatar", UNTRUSTED_CLICK_ONLY) 
+	LLMapTrackAvatarHandler() : LLCommandHandler("maptrackavatar", UNTRUSTED_THROTTLE)
 	{ 
 	}
-	
-	bool handle(const LLSD& params,
+
+    virtual bool canHandleUntrusted(
+        const LLSD& params,
+        const LLSD& query_map,
+        LLMediaCtrl* web,
+        const std::string& nav_type)
+    {
+        if (params.size() < 1)
+        {
+            return true; // don't block, will fail later
+        }
+
+        if (nav_type == NAV_TYPE_CLICKED
+            || nav_type == NAV_TYPE_EXTERNAL)
+        {
+            // NAV_TYPE_EXTERNAL will be throttled
+            return true;
+        }
+
+        return false;
+    }
+
+    bool handle(const LLSD& params,
                 const LLSD& query_map,
                 const std::string& grid,
                 LLMediaCtrl* web)
diff --git a/indra/newview/llfriendcard.cpp b/indra/newview/llfriendcard.cpp
index 43837e448f6f64ec52369e7002bf7d247e93f893..f634bb59da14bbe58f5c66132cc0f38f30997c96 100644
--- a/indra/newview/llfriendcard.cpp
+++ b/indra/newview/llfriendcard.cpp
@@ -549,20 +549,7 @@ void LLFriendCardsManager::syncFriendsFolder()
 	// Create own calling card if it was not found in Friends/All folder
 	if (!collector.isAgentCallingCardFound())
 	{
-		LLAvatarName av_name;
-		LLAvatarNameCache::get( gAgentID, &av_name );
-
-		create_inventory_item(gAgentID,
-							  gAgent.getSessionID(),
-							  calling_cards_folder_id,
-							  LLTransactionID::tnull,
-							  av_name.getCompleteName(),
-							  gAgentID.asString(),
-							  LLAssetType::AT_CALLINGCARD,
-							  LLInventoryType::IT_CALLINGCARD,
-                              NO_INV_SUBTYPE,
-							  PERM_MOVE | PERM_TRANSFER,
-							  NULL);
+		create_inventory_callingcard(gAgentID, calling_cards_folder_id);
 	}
 
     // All folders created and updated.
diff --git a/indra/newview/llgroupactions.cpp b/indra/newview/llgroupactions.cpp
index 1d032a711bf90fe3c14e8abec200a495e1396b33..2d0aa2880d5fec9bd05f986a945c892b71e3a712 100644
--- a/indra/newview/llgroupactions.cpp
+++ b/indra/newview/llgroupactions.cpp
@@ -72,7 +72,8 @@ class LLGroupCommandHandler : public LLCommandHandler
             return true; // don't block, will fail later
         }
 
-        if (nav_type == NAV_TYPE_CLICKED)
+        if (nav_type == NAV_TYPE_CLICKED
+            || nav_type == NAV_TYPE_EXTERNAL)
         {
             return true;
         }
diff --git a/indra/newview/llinventorybridge.cpp b/indra/newview/llinventorybridge.cpp
index c0a5bf0a55820bf898d62f50881902874aa97e6d..1495a7a1d14cc51951a978bb4c96ff4d5ac23607 100644
--- a/indra/newview/llinventorybridge.cpp
+++ b/indra/newview/llinventorybridge.cpp
@@ -4558,7 +4558,8 @@ void LLFolderBridge::buildContextMenuOptions(U32 flags, menuentry_vec_t&   items
 			|| is_recent_panel
 			|| !trash
 			|| trash->getVersion() == LLViewerInventoryCategory::VERSION_UNKNOWN
-			|| trash->getDescendentCount() == LLViewerInventoryCategory::VERSION_UNKNOWN)
+			|| trash->getDescendentCount() == LLViewerInventoryCategory::VERSION_UNKNOWN
+			|| gAgentAvatarp->hasAttachmentsInTrash())
 		{
 			disabled_items.push_back(std::string("Empty Trash"));
 		}
@@ -6828,6 +6829,7 @@ void LLGestureBridge::buildContextMenu(LLMenuGL& menu, U32 flags)
 		{
 			items.push_back(std::string("Activate"));
 		}
+        items.push_back(std::string("PlayGesture"));
 	}
 	addLinkReplaceMenuOption(items, disabled_items);
 	hide_context_entries(menu, items, disabled_items);
diff --git a/indra/newview/llinventorymodelbackgroundfetch.cpp b/indra/newview/llinventorymodelbackgroundfetch.cpp
index 706c768250419b7b51be334858bd7b4b98f2d24e..e1bfe50076af1b3f792f367938f1e63adff9be2f 100644
--- a/indra/newview/llinventorymodelbackgroundfetch.cpp
+++ b/indra/newview/llinventorymodelbackgroundfetch.cpp
@@ -357,6 +357,7 @@ void LLInventoryModelBackgroundFetch::scheduleFolderFetch(const LLUUID& cat_id,
     if (mFetchFolderQueue.empty() || mFetchFolderQueue.front().mUUID != cat_id)
     {
         mBackgroundFetchActive = true;
+        mFolderFetchActive = true;
 
         // Specific folder requests go to front of queue.
         mFetchFolderQueue.push_front(FetchQueueInfo(cat_id, forced ? FT_FORCED : FT_DEFAULT));
@@ -375,6 +376,61 @@ void LLInventoryModelBackgroundFetch::scheduleItemFetch(const LLUUID& item_id, b
     }
 }
 
+void LLInventoryModelBackgroundFetch::fetchFolderAndLinks(const LLUUID& cat_id, nullary_func_t callback)
+{
+    LLViewerInventoryCategory* cat = gInventory.getCategory(cat_id);
+    if (cat)
+    {
+        // Mark folder (update timer) so that background fetch won't request it
+        cat->setFetching(LLViewerInventoryCategory::FETCH_RECURSIVE);
+    }
+    incrFetchFolderCount(1);
+    mExpectedFolderIds.push_back(cat_id);
+
+    // Assume that we have no relevant cache. Fetch folder, and items folder's links point to.
+    AISAPI::FetchCategoryLinks(cat_id,
+                               [callback, cat_id](const LLUUID& id)
+                               {
+                                   callback();
+                                   if (id.isNull())
+                                   {
+                                       LL_WARNS() << "Failed to fetch category links " << cat_id << LL_ENDL;
+                                   }
+                                   LLInventoryModelBackgroundFetch::getInstance()->onAISFolderCalback(cat_id, id, FT_DEFAULT);
+                               });
+
+    // start idle loop to track completion
+    mBackgroundFetchActive = true;
+    mFolderFetchActive = true;
+    gIdleCallbacks.addFunction(&LLInventoryModelBackgroundFetch::backgroundFetchCB, NULL);
+}
+
+void LLInventoryModelBackgroundFetch::fetchCOF(nullary_func_t callback)
+{
+    LLUUID cat_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_CURRENT_OUTFIT);
+    LLViewerInventoryCategory* cat = gInventory.getCategory(cat_id);
+    if (cat)
+    {
+        // Mark cof (update timer) so that background fetch won't request it
+        cat->setFetching(LLViewerInventoryCategory::FETCH_RECURSIVE);
+    }
+    incrFetchFolderCount(1);
+    mExpectedFolderIds.push_back(cat_id);
+    // For reliability assume that we have no relevant cache, so
+    // fetch cof along with items cof's links point to.
+    AISAPI::FetchCOF([callback](const LLUUID& id)
+                     {
+                         callback();
+                         LLUUID cat_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_CURRENT_OUTFIT);
+                         LLInventoryModelBackgroundFetch::getInstance()->onAISFolderCalback(cat_id, id, FT_DEFAULT);
+                     });
+
+    // start idle loop to track completion
+    mBackgroundFetchActive = true;
+    mFolderFetchActive = true;
+    gIdleCallbacks.addFunction(&LLInventoryModelBackgroundFetch::backgroundFetchCB, NULL);
+}
+
 void LLInventoryModelBackgroundFetch::findLostItems()
 {
     mBackgroundFetchActive = true;
diff --git a/indra/newview/llinventorymodelbackgroundfetch.h b/indra/newview/llinventorymodelbackgroundfetch.h
index 54bbafc0a2116273f050d0af78a97a9521858934..60d5d150b5662b48f306bf37ff089d6d3456b891 100644
--- a/indra/newview/llinventorymodelbackgroundfetch.h
+++ b/indra/newview/llinventorymodelbackgroundfetch.h
@@ -53,6 +53,13 @@ class LLInventoryModelBackgroundFetch final : public LLSingleton<LLInventoryMode
     void scheduleFolderFetch(const LLUUID& cat_id, bool forced = false);
     void scheduleItemFetch(const LLUUID& item_id, bool forced = false);
 
+    typedef boost::function<void()> nullary_func_t;
+    // AIS3 only, Fetches folder and everithing links inside the folder point to
+    // Intended for outfits
+    void fetchFolderAndLinks(const LLUUID& cat_id, nullary_func_t callback);
+    // AIS3 only
+    void fetchCOF(nullary_func_t callback);
+
 	BOOL folderFetchActive() const;
 	bool isEverythingFetched() const; // completing the fetch once per session should be sufficient
 
diff --git a/indra/newview/llinventoryobserver.cpp b/indra/newview/llinventoryobserver.cpp
index 35a9afac282a1c97bf15419eb6e5542c5575e5a7..5078807e835774c68f56a768619146769ec5cf8a 100644
--- a/indra/newview/llinventoryobserver.cpp
+++ b/indra/newview/llinventoryobserver.cpp
@@ -337,52 +337,49 @@ void LLInventoryFetchItemsObserver::startFetch()
     {
         for (requests_by_folders_t::value_type &folder : requests)
         {
-            if (folder.second.size() > MAX_INDIVIDUAL_ITEM_REQUESTS)
+            LLViewerInventoryCategory* cat = gInventory.getCategory(folder.first);
+            if (cat)
             {
-                // requesting one by one will take a while
-                // do whole folder
-                LLInventoryModelBackgroundFetch::getInstance()->scheduleFolderFetch(folder.first, true);
-            }
-            else
-            {
-                LLViewerInventoryCategory* cat = gInventory.getCategory(folder.first);
-                if (cat)
+                if (cat->getVersion() == LLViewerInventoryCategory::VERSION_UNKNOWN)
                 {
-                    if (cat->getVersion() == LLViewerInventoryCategory::VERSION_UNKNOWN)
-                    {
-                        // start fetching whole folder since it's not ready either way
-                        cat->fetch();
-                    }
-                    else if (cat->getViewerDescendentCount() <= folder.second.size()
-                             || cat->getDescendentCount() <= folder.second.size())
-                    {
-                        // Start fetching whole folder since we need all items
-                        LLInventoryModelBackgroundFetch::getInstance()->scheduleFolderFetch(folder.first, true);
+                    // start fetching whole folder since it's not ready either way
+                    cat->fetch();
+                }
+                else if (folder.second.size() > MAX_INDIVIDUAL_ITEM_REQUESTS)
+                {
+                    // requesting one by one will take a while
+                    // do whole folder
+                    LLInventoryModelBackgroundFetch::getInstance()->scheduleFolderFetch(folder.first, true);
+                }
+                else if (cat->getViewerDescendentCount() <= folder.second.size()
+                         || cat->getDescendentCount() <= folder.second.size())
+                {
+                    // Start fetching whole folder since we need all items
+                    LLInventoryModelBackgroundFetch::getInstance()->scheduleFolderFetch(folder.first, true);
 
-                    }
-                    else
-                    {
-                        // get items one by one
-                        for (LLUUID &item_id : folder.second)
-                        {
-                            LLInventoryModelBackgroundFetch::getInstance()->scheduleItemFetch(item_id);
-                        }
-                    }
                 }
                 else
                 {
-                    // Isn't supposed to happen? We should have all folders
-                    // and if item exists, folder is supposed to exist as well.
-                    llassert(false);
-                    LL_WARNS("Inventory") << "Missing folder: " << folder.first << " fetching items individually" << LL_ENDL;
-
                     // get items one by one
-                    for (LLUUID &item_id : folder.second)
+                    for (LLUUID& item_id : folder.second)
                     {
                         LLInventoryModelBackgroundFetch::getInstance()->scheduleItemFetch(item_id);
                     }
                 }
             }
+            else
+            {
+                // Isn't supposed to happen? We should have all folders
+                // and if item exists, folder is supposed to exist as well.
+                llassert(false);
+                LL_WARNS("Inventory") << "Missing folder: " << folder.first << " fetching items individually" << LL_ENDL;
+
+                // get items one by one
+                for (LLUUID& item_id : folder.second)
+                {
+                    LLInventoryModelBackgroundFetch::getInstance()->scheduleItemFetch(item_id);
+                }
+            }
         }
     }
     else
@@ -421,10 +418,22 @@ void LLInventoryFetchDescendentsObserver::changed(U32 mask)
 		}
 		++it;
 	}
-	if (mIncomplete.empty())
-	{
-		done();
-	}
+
+    if (mIncomplete.empty())
+    {
+        done();
+    }
+    else
+    {
+        LLInventoryModelBackgroundFetch* fetcher = LLInventoryModelBackgroundFetch::getInstance();
+        if (fetcher->isEverythingFetched()
+            && !fetcher->folderFetchActive())
+        {
+            // If fetcher is done with folders yet we are waiting, fetch either
+            // failed or version is somehow stuck at -1
+            done();
+        }
+    }
 }
 
 void LLInventoryFetchDescendentsObserver::startFetch()
@@ -435,12 +444,8 @@ void LLInventoryFetchDescendentsObserver::startFetch()
 		if (!cat) continue;
 		if (!isCategoryComplete(cat))
 		{
-			// CHECK IT: isCategoryComplete() checks both version and descendant count but
-			// fetch() only works for Unknown version and doesn't care about descentants,
-			// as result fetch won't start and folder will potentially get stuck as
-			// incomplete in observer.
-			// Likely either both should use only version or both should check descendants.
-			cat->fetch();		//blindly fetch it without seeing if anything else is fetching it.
+            //blindly fetch it without seeing if anything else is fetching it.
+            LLInventoryModelBackgroundFetch::getInstance()->scheduleFolderFetch(*it, true);
 			mIncomplete.push_back(*it);	//Add to list of things being downloaded for this observer.
 		}
 		else
diff --git a/indra/newview/llkeyconflict.cpp b/indra/newview/llkeyconflict.cpp
index e1aac23aa7c7c0dcbae50b623c50ccfb0c4871d4..0d04f8b7d2b9a7df67aa0e2fc39159e0bef8ff1c 100644
--- a/indra/newview/llkeyconflict.cpp
+++ b/indra/newview/llkeyconflict.cpp
@@ -74,40 +74,6 @@ std::string string_from_mask(MASK mask)
     return res;
 }
 
-std::string string_from_mouse(EMouseClickType click, bool translate)
-{
-    std::string res;
-    switch (click)
-    {
-    case CLICK_LEFT:
-        res = "LMB";
-        break;
-    case CLICK_MIDDLE:
-        res = "MMB";
-        break;
-    case CLICK_RIGHT:
-        res = "RMB";
-        break;
-    case CLICK_BUTTON4:
-        res = "MB4";
-        break;
-    case CLICK_BUTTON5:
-        res = "MB5";
-        break;
-    case CLICK_DOUBLELEFT:
-        res = "Double LMB";
-        break;
-    default:
-        break;
-    }
-
-    if (translate && !res.empty())
-    {
-        res = LLTrans::getString(res);
-    }
-    return res;
-}
-
 // LLKeyConflictHandler
 
 S32 LLKeyConflictHandler::sTemporaryFileUseCount = 0;
@@ -270,7 +236,7 @@ std::string LLKeyConflictHandler::getStringFromKeyData(const LLKeyData& keydata)
         result = LLKeyboard::stringFromAccelerator(keydata.mMask);
     }
 
-    result += string_from_mouse(keydata.mMouse, true);
+    result += LLKeyboard::stringFromMouse(keydata.mMouse);
 
     return result;
 }
@@ -545,7 +511,7 @@ void LLKeyConflictHandler::saveToSettings(bool temporary)
                     {
                         // set() because 'optional', for compatibility purposes
                         // just copy old keys.xml and rename to key_bindings.xml, it should work
-                        binding.mouse.set(string_from_mouse(data.mMouse, false), true);
+                        binding.mouse.set(LLKeyboard::stringFromMouse(data.mMouse, false), true);
                     }
                     binding.command = iter->first;
                     mode.bindings.add(binding);
diff --git a/indra/newview/lllocationinputctrl.cpp b/indra/newview/lllocationinputctrl.cpp
index b432cfaaf23230acf938cf255283bbb8516b4096..435250e92bbc97c33a97e62747142ad9d483cf8a 100644
--- a/indra/newview/lllocationinputctrl.cpp
+++ b/indra/newview/lllocationinputctrl.cpp
@@ -398,7 +398,9 @@ LLLocationInputCtrl::LLLocationInputCtrl(const LLLocationInputCtrl::Params& p)
 		LL_WARNS() << "Error loading navigation bar context menu" << LL_ENDL;
 		
 	}
-	getTextEntry()->setRightMouseUpCallback(boost::bind(&LLLocationInputCtrl::onTextEditorRightClicked,this,_2,_3,_4));
+    //don't show default context menu
+    getTextEntry()->setShowContextMenu(false);
+    getTextEntry()->setRightMouseDownCallback(boost::bind(&LLLocationInputCtrl::onTextEditorRightClicked, this, _2, _3, _4));
 	updateWidgetlayout();
 
 	// Connecting signal for updating location on "Show Coordinates" setting change.
diff --git a/indra/newview/llpanelface.cpp b/indra/newview/llpanelface.cpp
index 3579883c9ee246d13c274a305ebb95f19e57202e..577b7e91200f40910a2da3fc0d21e582d1c4f5b4 100644
--- a/indra/newview/llpanelface.cpp
+++ b/indra/newview/llpanelface.cpp
@@ -4666,8 +4666,6 @@ void LLPanelFace::onPasteTexture(LLViewerObject* objectp, S32 te)
         {
             LLUUID object_id = objectp->getID();
 
-            LLSelectedTEMaterial::setAlphaMaskCutoff(this, (U8)te_data["material"]["SpecRot"].asInteger(), te, object_id);
-
             // Normal
             // Replace placeholders with target's
             if (te_data["material"].has("NormMapNoCopy"))
@@ -4714,6 +4712,7 @@ void LLPanelFace::onPasteTexture(LLViewerObject* objectp, S32 te)
             LLSelectedTEMaterial::setSpecularLightExponent(this, (U8)te_data["material"]["SpecExp"].asInteger(), te, object_id);
             LLSelectedTEMaterial::setEnvironmentIntensity(this, (U8)te_data["material"]["EnvIntensity"].asInteger(), te, object_id);
             LLSelectedTEMaterial::setDiffuseAlphaMode(this, (U8)te_data["material"]["DiffuseAlphaMode"].asInteger(), te, object_id);
+            LLSelectedTEMaterial::setAlphaMaskCutoff(this, (U8)te_data["material"]["AlphaMaskCutoff"].asInteger(), te, object_id);
             if (te_data.has("te") && te_data["te"].has("shiny"))
             {
                 objectp->setTEShiny(te, (U8)te_data["te"]["shiny"].asInteger());
diff --git a/indra/newview/llpanelgrouplandmoney.cpp b/indra/newview/llpanelgrouplandmoney.cpp
index 5aa7447ff11d6c54c61a3afd1b74f538c6f7643e..3fac5f46f6a8bfd1aa5925e8a7d2a6e380d6fac3 100644
--- a/indra/newview/llpanelgrouplandmoney.cpp
+++ b/indra/newview/llpanelgrouplandmoney.cpp
@@ -1078,7 +1078,7 @@ void LLGroupMoneyDetailsTabEventHandler::processReply(LLMessageSystem* msg,
 	msg->getS32Fast(_PREHASH_MoneyData, _PREHASH_CurrentInterval, current_interval );
 	msg->getStringFast(_PREHASH_MoneyData, _PREHASH_StartDate, start_date);
 
-	std::string time_str = LLTrans::getString("GroupMoneyDate");
+	std::string time_str = LLTrans::getString("GroupMoneyStartDate");
 	LLSD substitution;
 
 	// We don't do time zone corrections of the calculated number of seconds
@@ -1233,7 +1233,7 @@ void LLGroupMoneySalesTabEventHandler::processReply(LLMessageSystem* msg,
 	// Start with the date.
 	if (text == mImplementationp->mLoadingText)
 	{
-		std::string time_str = LLTrans::getString("GroupMoneyDate");
+		std::string time_str = LLTrans::getString("GroupMoneyStartDate");
 		LLSD substitution;
 
 		// We don't do time zone corrections of the calculated number of seconds
diff --git a/indra/newview/llpanelprofile.cpp b/indra/newview/llpanelprofile.cpp
index e59258b629fc9eb3dda6cb6bd1eadfa4fa2239b6..0a5b8142ca4466f08f4cdac9f7a99ea3d53c39e4 100644
--- a/indra/newview/llpanelprofile.cpp
+++ b/indra/newview/llpanelprofile.cpp
@@ -279,9 +279,9 @@ void request_avatar_properties_coro(std::string cap_url, LLUUID agent_id)
 //TODO: changes take two minutes to propagate!
 // Add some storage that holds updated data for two minutes
 // for new instances to reuse the data
-// Profile data is only relevant to won avatar, but notes
-// are for everybody
-void put_avatar_properties_coro(std::string cap_url, LLUUID agent_id, LLSD data)
+// Profile data is only relevant to own avatar, but notes
+// are for everybody (no onger an issue?)
+void put_avatar_properties_coro(std::string cap_url, LLUUID agent_id, LLSD data, std::function<void(bool)> callback)
 {
     LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID);
     LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t
@@ -302,10 +302,16 @@ void put_avatar_properties_coro(std::string cap_url, LLUUID agent_id, LLSD data)
     if (!status)
     {
         LL_WARNS("AvatarProperties") << "Failed to put agent information " << data << " for id " << agent_id << LL_ENDL;
-        return;
+    }
+    else
+    {
+        LL_DEBUGS("AvatarProperties") << "Agent id: " << agent_id << " Data: " << data << " Result: " << httpResults << LL_ENDL;
     }
 
-    LL_DEBUGS("AvatarProperties") << "Agent id: " << agent_id << " Data: " << data << " Result: " << httpResults << LL_ENDL;
+    if (callback)
+    {
+        callback(status);
+    }
 }
 
 //////////////////////////////////////////////////////////////////////////
@@ -332,6 +338,166 @@ class LLWebProfileHandler : public LLCommandHandler
 	}
 };
 LLWebProfileHandler gWebProfileHandler;
+#if 0 // ALCHMERGE
+LLProfileHandler gProfileHandler;
+
+
+//////////////////////////////////////////////////////////////////////////
+// LLAgentHandler
+
+class LLAgentHandler : public LLCommandHandler
+{
+public:
+	// requires trusted browser to trigger
+	LLAgentHandler() : LLCommandHandler("agent", UNTRUSTED_THROTTLE) { }
+
+    virtual bool canHandleUntrusted(
+        const LLSD& params,
+        const LLSD& query_map,
+        LLMediaCtrl* web,
+        const std::string& nav_type)
+    {
+        if (params.size() < 2)
+        {
+            return true; // don't block, will fail later
+        }
+
+        if (nav_type == NAV_TYPE_CLICKED
+            || nav_type == NAV_TYPE_EXTERNAL)
+        {
+            return true;
+        }
+
+        const std::string verb = params[1].asString();
+        if (verb == "about" || verb == "inspect" || verb == "reportAbuse")
+        {
+            return true;
+        }
+        return false;
+    }
+
+	bool handle(const LLSD& params,
+                const LLSD& query_map,
+                const std::string& grid,
+                LLMediaCtrl* web)
+	{
+		if (params.size() < 2) return false;
+		LLUUID avatar_id;
+		if (!avatar_id.set(params[0], FALSE))
+		{
+			return false;
+		}
+
+		const std::string verb = params[1].asString();
+		if (verb == "about")
+		{
+			LLAvatarActions::showProfile(avatar_id);
+			return true;
+		}
+
+		if (verb == "inspect")
+		{
+			LLFloaterReg::showInstance("inspect_avatar", LLSD().with("avatar_id", avatar_id));
+			return true;
+		}
+
+		if (verb == "im")
+		{
+			LLAvatarActions::startIM(avatar_id);
+			return true;
+		}
+
+		if (verb == "pay")
+		{
+			if (!LLUI::getInstance()->mSettingGroups["config"]->getBOOL("EnableAvatarPay"))
+			{
+				LLNotificationsUtil::add("NoAvatarPay", LLSD(), LLSD(), std::string("SwitchToStandardSkinAndQuit"));
+				return true;
+			}
+
+			LLAvatarActions::pay(avatar_id);
+			return true;
+		}
+
+		if (verb == "offerteleport")
+		{
+			LLAvatarActions::offerTeleport(avatar_id);
+			return true;
+		}
+
+		if (verb == "requestfriend")
+		{
+			LLAvatarActions::requestFriendshipDialog(avatar_id);
+			return true;
+		}
+
+		if (verb == "removefriend")
+		{
+			LLAvatarActions::removeFriendDialog(avatar_id);
+			return true;
+		}
+
+		if (verb == "mute")
+		{
+			if (! LLAvatarActions::isBlocked(avatar_id))
+			{
+				LLAvatarActions::toggleBlock(avatar_id);
+			}
+			return true;
+		}
+
+		if (verb == "unmute")
+		{
+			if (LLAvatarActions::isBlocked(avatar_id))
+			{
+				LLAvatarActions::toggleBlock(avatar_id);
+			}
+			return true;
+		}
+
+		if (verb == "block")
+		{
+			if (params.size() > 2)
+			{
+				const std::string object_name = LLURI::unescape(params[2].asString());
+				LLMute mute(avatar_id, object_name, LLMute::OBJECT);
+				LLMuteList::getInstance()->add(mute);
+				LLPanelBlockedList::showPanelAndSelect(mute.mID);
+			}
+			return true;
+		}
+
+		if (verb == "unblock")
+		{
+			if (params.size() > 2)
+			{
+				const std::string object_name = params[2].asString();
+				LLMute mute(avatar_id, object_name, LLMute::OBJECT);
+				LLMuteList::getInstance()->remove(mute);
+			}
+			return true;
+		}
+
+        // reportAbuse is here due to convoluted avatar handling
+        // in LLScrollListCtrl and LLTextBase
+        if (verb == "reportAbuse" && web == NULL) 
+        {
+            LLAvatarName av_name;
+            if (LLAvatarNameCache::get(avatar_id, &av_name))
+            {
+                LLFloaterReporter::showFromAvatar(avatar_id, av_name.getCompleteName());
+            }
+            else
+            {
+                LLFloaterReporter::showFromAvatar(avatar_id, "not avaliable");
+            }
+            return true;
+        }
+		return false;
+	}
+};
+LLAgentHandler gAgentHandler;
+#endif
 
 ///----------------------------------------------------------------------------
 /// LLFloaterProfilePermissions
@@ -1466,7 +1632,7 @@ void LLPanelProfileSecondLife::onShowInSearchCallback()
         LLSD data;
         data["allow_publish"] = mAllowPublish;
         LLCoros::instance().launch("putAgentUserInfoCoro",
-            boost::bind(put_avatar_properties_coro, cap_url, getAvatarId(), data));
+            boost::bind(put_avatar_properties_coro, cap_url, getAvatarId(), data, nullptr));
     }
     else
     {
@@ -1481,7 +1647,7 @@ void LLPanelProfileSecondLife::onSaveDescriptionChanges()
     if (!cap_url.empty())
     {
         LLCoros::instance().launch("putAgentUserInfoCoro",
-            boost::bind(put_avatar_properties_coro, cap_url, getAvatarId(), LLSD().with("sl_about_text", mDescriptionText)));
+            boost::bind(put_avatar_properties_coro, cap_url, getAvatarId(), LLSD().with("sl_about_text", mDescriptionText), nullptr));
     }
     else
     {
@@ -1628,10 +1794,19 @@ void LLPanelProfileSecondLife::onCommitProfileImage(const LLUUID& id)
     std::string cap_url = gAgent.getRegionCapability(PROFILE_PROPERTIES_CAP);
     if (!cap_url.empty())
     {
+        std::function<void(bool)> callback = [id](bool result)
+        {
+            if (result)
+            {
+                LLAvatarIconIDCache::getInstance()->add(gAgentID, id);
+                // Should trigger callbacks in icon controls
+                LLAvatarPropertiesProcessor::getInstance()->sendAvatarPropertiesRequest(gAgentID);
+            }
+        };
         LLSD params;
         params["sl_image_id"] = id;
         LLCoros::instance().launch("putAgentUserInfoCoro",
-            boost::bind(put_avatar_properties_coro, cap_url, getAvatarId(), params));
+            boost::bind(put_avatar_properties_coro, cap_url, getAvatarId(), params, callback));
 
         mImageId = id;
         if (mImageId == LLUUID::null)
@@ -1968,7 +2143,7 @@ void LLPanelProfileFirstLife::onCommitPhoto(const LLUUID& id)
         LLSD params;
         params["fl_image_id"] = id;
         LLCoros::instance().launch("putAgentUserInfoCoro",
-            boost::bind(put_avatar_properties_coro, cap_url, getAvatarId(), params));
+            boost::bind(put_avatar_properties_coro, cap_url, getAvatarId(), params, nullptr));
 
         mImageId = id;
         if (mImageId.notNull())
@@ -2012,7 +2187,7 @@ void LLPanelProfileFirstLife::onSaveDescriptionChanges()
     if (!cap_url.empty())
     {
         LLCoros::instance().launch("putAgentUserInfoCoro",
-            boost::bind(put_avatar_properties_coro, cap_url, getAvatarId(), LLSD().with("fl_about_text", mCurrentDescription)));
+            boost::bind(put_avatar_properties_coro, cap_url, getAvatarId(), LLSD().with("fl_about_text", mCurrentDescription), nullptr));
     }
     else
     {
@@ -2200,7 +2375,7 @@ void LLPanelProfileNotes::onSaveNotesChanges()
     if (!cap_url.empty())
     {
         LLCoros::instance().launch("putAgentUserInfoCoro",
-            boost::bind(put_avatar_properties_coro, cap_url, getAvatarId(), LLSD().with("notes", mCurrentNotes)));
+            boost::bind(put_avatar_properties_coro, cap_url, getAvatarId(), LLSD().with("notes", mCurrentNotes), nullptr));
     }
     else
     {
diff --git a/indra/newview/llpanelprofileclassifieds.cpp b/indra/newview/llpanelprofileclassifieds.cpp
index 6f45f85b95c0aee07f9c78fb860798efecd870e1..72303a2da21fe197b1bb06d13f79e542d3f41d84 100644
--- a/indra/newview/llpanelprofileclassifieds.cpp
+++ b/indra/newview/llpanelprofileclassifieds.cpp
@@ -92,7 +92,8 @@ class LLClassifiedHandler : public LLCommandHandler, public LLAvatarPropertiesOb
             return true; // don't block, will fail later
         }
 
-        if (nav_type == NAV_TYPE_CLICKED)
+        if (nav_type == NAV_TYPE_CLICKED
+            || nav_type == NAV_TYPE_EXTERNAL)
         {
             return true;
         }
diff --git a/indra/newview/llpanelprofilepicks.cpp b/indra/newview/llpanelprofilepicks.cpp
index 3ad0fe95b4a0da36ce84ae15210556633e1a3c81..7bf98100deef7f4dadab8d7a572b44b332e54058 100644
--- a/indra/newview/llpanelprofilepicks.cpp
+++ b/indra/newview/llpanelprofilepicks.cpp
@@ -77,7 +77,8 @@ class LLPickHandler : public LLCommandHandler
             return true; // don't block, will fail later
         }
 
-        if (nav_type == NAV_TYPE_CLICKED)
+        if (nav_type == NAV_TYPE_CLICKED
+            || nav_type == NAV_TYPE_EXTERNAL)
         {
             return true;
         }
diff --git a/indra/newview/llprofileimagepicker.cpp b/indra/newview/llprofileimagepicker.cpp
index eb40cdcc105fa6c59264acf01ec7cb12e4fce190..6dfc0e1662c705d2cb0ab7fd85e56001f0a5eec2 100644
--- a/indra/newview/llprofileimagepicker.cpp
+++ b/indra/newview/llprofileimagepicker.cpp
@@ -27,6 +27,7 @@
 #include "llviewerprecompiledheaders.h"
 #include "llprofileimagepicker.h"
 
+#include "llavatariconctrl.h"
 #include "llagent.h"
 #include "llloadingindicator.h"
 #include "llnotificationsutil.h"
@@ -203,6 +204,13 @@ void post_profile_image_coro(std::string cap_url, EProfileImageType type, std::s
 
     cb(result);
 
+    if (type == PROFILE_IMAGE_SL && result.notNull())
+    {
+        LLAvatarIconIDCache::getInstance()->add(gAgentID, result);
+        // Should trigger callbacks in icon controls
+        LLAvatarPropertiesProcessor::getInstance()->sendAvatarPropertiesRequest(gAgentID);
+    }
+
     // Cleanup
     LLFile::remove(path_to_image);
 }
diff --git a/indra/newview/llspatialpartition.cpp b/indra/newview/llspatialpartition.cpp
index 3da37e3f23c3a1e99db607dfb04e5f1477431439..2215976ac30bda3e8f51e5b9463785e2289b04e6 100644
--- a/indra/newview/llspatialpartition.cpp
+++ b/indra/newview/llspatialpartition.cpp
@@ -835,8 +835,44 @@ void LLSpatialGroup::handleChildAddition(const OctreeNode* parent, OctreeNode* c
 	assert_states_valid(this);
 }
 
+//virtual
+void LLSpatialGroup::rebound()
+{
+    if (!isDirty())
+        return;
+
+    super::rebound();
+
+    if (mSpatialPartition->mDrawableType == LLPipeline::RENDER_TYPE_CONTROL_AV)
+    {
+        llassert(mSpatialPartition->mPartitionType == LLViewerRegion::PARTITION_CONTROL_AV);
+
+        LLSpatialBridge* bridge = getSpatialPartition()->asBridge();
+        if (bridge &&
+            bridge->mDrawable &&
+            bridge->mDrawable->getVObj() &&
+            bridge->mDrawable->getVObj()->isRoot())
+        {
+            LLControlAvatar* controlAvatar = bridge->mDrawable->getVObj()->getControlAvatar();
+            if (controlAvatar &&
+                controlAvatar->mDrawable &&
+                controlAvatar->mControlAVBridge)
+            {
+                llassert(controlAvatar->mControlAVBridge->mOctree);
+
+                LLSpatialGroup* root = (LLSpatialGroup*)controlAvatar->mControlAVBridge->mOctree->getListener(0);
+                if (this == root)
+                {
+                    const LLVector4a* addingExtents = controlAvatar->mDrawable->getSpatialExtents();
+                    const LLXformMatrix* currentTransform = bridge->mDrawable->getXform();
+                    expandExtents(addingExtents, *currentTransform);
+                }
+            }
+        }
+    }
+}
 
-void LLSpatialGroup::destroyGLState(bool keep_occlusion) 
+void LLSpatialGroup::destroyGLState(bool keep_occlusion)
 {
 	setState(LLSpatialGroup::GEOM_DIRTY | LLSpatialGroup::IMAGE_DIRTY);
 
diff --git a/indra/newview/llspatialpartition.h b/indra/newview/llspatialpartition.h
index 314a521d01e5e3fec6e029e538b357213daa1082..275c106be1c40e3d0ceddb4294a93f80dd7ba48b 100644
--- a/indra/newview/llspatialpartition.h
+++ b/indra/newview/llspatialpartition.h
@@ -193,6 +193,7 @@ class LLDrawInfo final : public LLRefCount
 LL_ALIGN_PREFIX(16)
 class LLSpatialGroup final : public LLOcclusionCullingGroup
 {
+	using super = LLOcclusionCullingGroup;
 	friend class LLSpatialPartition;
 	friend class LLOctreeStateCheck;
 public:
@@ -317,6 +318,9 @@ class LLSpatialGroup final : public LLOcclusionCullingGroup
 	virtual void handleDestruction(const TreeNode* node);
 	virtual void handleChildAddition(const OctreeNode* parent, OctreeNode* child);
 
+	// LLViewerOctreeGroup
+	virtual void rebound();
+
 public:
 	LL_ALIGN_16(LLVector4a mViewAngle);
 	LL_ALIGN_16(LLVector4a mLastUpdateViewAngle);
diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp
index 412cae0d15e07595f40ec34031ea767990932cbe..5fc9cfc477ac1a15260f48ec74c28f80645ec64e 100644
--- a/indra/newview/llstartup.cpp
+++ b/indra/newview/llstartup.cpp
@@ -3008,8 +3008,7 @@ void LLStartUp::loadInitialOutfit( const std::string& outfit_folder_name,
 		// Need to fetch cof contents before we can wear.
         if (do_copy)
         {
-            callAfterCategoryFetch(LLAppearanceMgr::instance().getCOF(),
-							   boost::bind(&LLAppearanceMgr::wearInventoryCategory, LLAppearanceMgr::getInstance(), cat, do_copy, do_append));
+            callAfterCOFFetch(boost::bind(&LLAppearanceMgr::wearInventoryCategory, LLAppearanceMgr::getInstance(), cat, do_copy, do_append));
         }
         else
         {
diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp
index 2883b1a2e94c89eeed090a701471650ec5ad2f39..8ac8656448d20a04c2edb8fe6dbc7d72b4dc8ba1 100644
--- a/indra/newview/llviewerfloaterreg.cpp
+++ b/indra/newview/llviewerfloaterreg.cpp
@@ -225,7 +225,11 @@ class LLFloaterOpenHandler : public LLCommandHandler
         
         std::string fl_name = params[0].asString();
 
-        if (nav_type == NAV_TYPE_CLICKED)
+        // External browsers explicitly ask user about opening links
+        // so treat "external" same as "clicked" in this case,
+        // despite it being treated as untrusted.
+        if (nav_type == NAV_TYPE_CLICKED
+            || nav_type == NAV_TYPE_EXTERNAL)
         {
             const std::list<std::string> blacklist_clicked = {
                 "camera_presets",
diff --git a/indra/newview/llviewerinput.cpp b/indra/newview/llviewerinput.cpp
index bdc8613e5521417c38ca161516185370146eb534..e7e0d52bfd35bb653976ecb024fcd6bf4358df26 100644
--- a/indra/newview/llviewerinput.cpp
+++ b/indra/newview/llviewerinput.cpp
@@ -1067,34 +1067,50 @@ LLViewerInput::LLViewerInput()
 	}
 }
 
+LLViewerInput::~LLViewerInput()
+{
+
+}
+
 // static
-BOOL LLViewerInput::modeFromString(const std::string& string, S32 *mode)
+bool LLViewerInput::modeFromString(const std::string& string, S32 *mode)
 {
-	if (string == "FIRST_PERSON")
+    if (string.empty())
+    {
+        return false;
+    }
+
+    std::string cmp_string = string;
+    LLStringUtil::toLower(cmp_string);
+	if (cmp_string == "first_person")
 	{
 		*mode = MODE_FIRST_PERSON;
-		return TRUE;
+		return true;
 	}
-	else if (string == "THIRD_PERSON")
+	else if (cmp_string == "third_person")
 	{
 		*mode = MODE_THIRD_PERSON;
-		return TRUE;
+		return true;
 	}
-	else if (string == "EDIT_AVATAR")
+	else if (cmp_string == "edit_avatar")
 	{
 		*mode = MODE_EDIT_AVATAR;
-		return TRUE;
+		return true;
 	}
-	else if (string == "SITTING")
+	else if (cmp_string == "sitting")
 	{
 		*mode = MODE_SITTING;
-		return TRUE;
-	}
-	else
-	{
-		*mode = MODE_THIRD_PERSON;
-		return FALSE;
+		return true;
 	}
+
+    S32 val = atoi(string.c_str());
+    if (val >= 0 && val < MODE_COUNT)
+    {
+        *mode = val;
+        return true;
+    }
+
+    return false;
 }
 
 // static
@@ -1298,6 +1314,7 @@ BOOL LLViewerInput::bindKey(const S32 mode, const KEY key, const MASK mask, cons
     bind.mKey = key;
     bind.mMask = mask;
     bind.mFunction = function;
+    bind.mFunctionName = function_name;
 
     if (result->mIsGlobal)
     {
@@ -1379,6 +1396,7 @@ BOOL LLViewerInput::bindMouse(const S32 mode, const EMouseClickType mouse, const
     bind.mMouse = mouse;
     bind.mMask = mask;
     bind.mFunction = function;
+    bind.mFunctionName = function_name;
 
     if (result->mIsGlobal)
     {
@@ -1877,3 +1895,49 @@ bool LLViewerInput::isMouseBindUsed(const EMouseClickType mouse, const MASK mask
     }
     return false;
 }
+
+std::string LLViewerInput::getKeyBindingAsString(const std::string& mode, const std::string& control) const
+{
+    S32 keyboard_mode;
+    if (!modeFromString(mode, &keyboard_mode))
+    {
+        keyboard_mode = getMode();
+    }
+
+    std::string res;
+    bool needs_separator = false;
+
+    // keybindings are sorted from having most mask to no mask (from restrictive to less restrictive),
+    // but it's visually better to present this data in reverse
+    std::vector<LLKeyboardBinding>::const_reverse_iterator iter_key = mKeyBindings[keyboard_mode].rbegin();
+    while (iter_key != mKeyBindings[keyboard_mode].rend())
+    {
+        if (iter_key->mFunctionName == control)
+        {
+            if (needs_separator)
+            {
+                res.append(" | ");
+            }
+            res.append(LLKeyboard::stringFromAccelerator(iter_key->mMask, iter_key->mKey));
+            needs_separator = true;
+        }
+        iter_key++;
+    }
+
+    std::vector<LLMouseBinding>::const_reverse_iterator iter_mouse = mMouseBindings[keyboard_mode].rbegin();
+    while (iter_mouse != mMouseBindings[keyboard_mode].rend())
+    {
+        if (iter_mouse->mFunctionName == control)
+        {
+            if (needs_separator)
+            {
+                res.append(" | ");
+            }
+            res.append(LLKeyboard::stringFromAccelerator(iter_mouse->mMask, iter_mouse->mMouse));
+            needs_separator = true;
+        }
+        iter_mouse++;
+    }
+
+    return res;
+}
diff --git a/indra/newview/llviewerinput.h b/indra/newview/llviewerinput.h
index e2383b3b1cf4b3537d11e1635fe1f497bc76cd7d..db3d93156b1759da8e8367889317a4e4697e37a5 100644
--- a/indra/newview/llviewerinput.h
+++ b/indra/newview/llviewerinput.h
@@ -28,7 +28,6 @@
 #define LL_LLVIEWERINPUT_H
 
 #include "llkeyboard.h" // For EKeystate
-#include "llinitparam.h"
 
 #include <boost/unordered/unordered_flat_map.hpp>
 #include <boost/unordered/unordered_flat_set.hpp>
@@ -37,6 +36,8 @@ const S32 MAX_KEY_BINDINGS = 128; // was 60
 const S32 keybindings_xml_version = 1;
 const std::string script_mouse_handler_name = "script_trigger_lbutton";
 
+class LLWindow;
+
 class LLNamedFunction
 {
 public:
@@ -54,6 +55,7 @@ class LLKeyboardBinding
     MASK			mMask;
 
     LLKeyFunc		mFunction;
+    std::string     mFunctionName;
 };
 
 class LLMouseBinding
@@ -63,6 +65,7 @@ class LLMouseBinding
     MASK			mMask;
 
     LLKeyFunc		mFunction;
+    std::string     mFunctionName;
 };
 
 
@@ -75,11 +78,7 @@ typedef enum e_keyboard_mode
 	MODE_COUNT
 } EKeyboardMode;
 
-class LLWindow;
-
-void bind_keyboard_functions();
-
-class LLViewerInput
+class LLViewerInput : public LLKeyBindingToStringHandler
 {
 public:
 	struct KeyBinding : public LLInitParam::Block<KeyBinding>
@@ -110,6 +109,7 @@ class LLViewerInput
 	};
 
 	LLViewerInput();
+    virtual ~LLViewerInput();
 
 	BOOL			handleKey(KEY key, MASK mask, BOOL repeated);
 	BOOL			handleKeyUp(KEY key, MASK mask);
@@ -124,7 +124,7 @@ class LLViewerInput
 	S32				loadBindingsXML(const std::string& filename);										// returns number bound, 0 on error
 	EKeyboardMode	getMode() const;
 
-	static BOOL		modeFromString(const std::string& string, S32 *mode);			// False on failure
+	static bool		modeFromString(const std::string& string, S32 *mode);			// False on failure
 	static BOOL		mouseFromString(const std::string& string, EMouseClickType *mode);// False on failure
 
     bool            scanKey(KEY key,
@@ -139,6 +139,9 @@ class LLViewerInput
     bool            isMouseBindUsed(const EMouseClickType mouse, const MASK mask, const S32 mode) const;
     bool            isLMouseHandlingDefault(const S32 mode) const { return mLMouseDefaultHandling[mode]; }
 
+    // inherited from LLKeyBindingToStringHandler
+    virtual std::string getKeyBindingAsString(const std::string& mode, const std::string& control) const override;
+
 private:
     bool            scanKey(const std::vector<LLKeyboardBinding> &binding,
                             S32 binding_count,
diff --git a/indra/newview/llviewerinventory.cpp b/indra/newview/llviewerinventory.cpp
index 1f40c5f27d9388c7afb3c1e81dc33cae5ebc4f00..45526c2581f89032a66c7bdd463429a588627845 100644
--- a/indra/newview/llviewerinventory.cpp
+++ b/indra/newview/llviewerinventory.cpp
@@ -263,9 +263,29 @@ LLLocalizedInventoryItemsDictionary::LLLocalizedInventoryItemsDictionary()
 class LLInventoryHandler : public LLCommandHandler
 {
 public:
-	// requires trusted browser to trigger
-	LLInventoryHandler() : LLCommandHandler("inventory", UNTRUSTED_CLICK_ONLY) { }
-	
+    LLInventoryHandler() : LLCommandHandler("inventory", UNTRUSTED_THROTTLE) { }
+
+    virtual bool canHandleUntrusted(
+        const LLSD& params,
+        const LLSD& query_map,
+        LLMediaCtrl* web,
+        const std::string& nav_type)
+    {
+        if (params.size() < 1)
+        {
+            return true; // don't block, will fail later
+        }
+
+        if (nav_type == NAV_TYPE_CLICKED
+            || nav_type == NAV_TYPE_EXTERNAL)
+        {
+            // NAV_TYPE_EXTERNAL will be throttled
+            return true;
+        }
+
+        return false;
+    }
+
 	bool handle(const LLSD& params,
                 const LLSD& query_map,
                 const std::string& grid,
@@ -1195,14 +1215,29 @@ void create_inventory_item(
 	gAgent.sendReliableMessage();
 }
 
+void create_inventory_callingcard_callback(LLPointer<LLInventoryCallback> cb,
+                                           const LLUUID &parent,
+                                           const LLUUID &avatar_id,
+                                           const LLAvatarName &av_name)
+{
+    std::string item_desc = avatar_id.asString();
+    create_inventory_item(gAgent.getID(),
+                          gAgent.getSessionID(),
+                          (parent.isNull() ? gInventory.findCategoryUUIDForType(LLFolderType::FT_CALLINGCARD) : parent),
+                          LLTransactionID::tnull,
+                          av_name.getUserName(),
+                          item_desc,
+                          LLAssetType::AT_CALLINGCARD,
+                          LLInventoryType::IT_CALLINGCARD,
+                          NO_INV_SUBTYPE,
+                          PERM_MOVE | PERM_TRANSFER,
+                          cb);
+}
+
 void create_inventory_callingcard(const LLUUID& avatar_id, const LLUUID& parent /*= LLUUID::null*/, LLPointer<LLInventoryCallback> cb/*=NULL*/)
 {
-	std::string item_desc = avatar_id.asString();
 	LLAvatarName av_name;
-	LLAvatarNameCache::get(avatar_id, &av_name);
-	create_inventory_item(gAgent.getID(), gAgent.getSessionID(),
-						  (parent.isNull() ? gInventory.findCategoryUUIDForType(LLFolderType::FT_CALLINGCARD) : parent), LLTransactionID::tnull, av_name.getUserName(), item_desc, LLAssetType::AT_CALLINGCARD,
-                          LLInventoryType::IT_CALLINGCARD, NO_INV_SUBTYPE, PERM_MOVE | PERM_TRANSFER, cb);
+    LLAvatarNameCache::get(avatar_id, boost::bind(&create_inventory_callingcard_callback, cb, parent, _1, _2));
 }
 
 void create_inventory_wearable(const LLUUID& agent_id, const LLUUID& session_id,
diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp
index 161927b16ab8ec6bfa6f8e34a8dbcae32454c5f8..9614b5747f4e897f439ba10d2fdba911039f5e24 100644
--- a/indra/newview/llviewerobject.cpp
+++ b/indra/newview/llviewerobject.cpp
@@ -3545,6 +3545,12 @@ void LLViewerObject::doInventoryCallback()
 
 void LLViewerObject::removeInventory(const LLUUID& item_id)
 {
+    // close associated floater properties
+    LLSD params;
+    params["id"] = item_id;
+    params["object"] = mID;
+    LLFloaterReg::hideInstance("item_properties", params);
+
 	LLMessageSystem* msg = gMessageSystem;
 	msg->newMessageFast(_PREHASH_RemoveTaskInventory);
 	msg->nextBlockFast(_PREHASH_AgentData);
diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp
index a6d75e9956dafab4c1dd37becc76e7ad15eeaf4d..7544e8b4192a0d18e30f9c8518c6d24b624594fb 100644
--- a/indra/newview/llvoavatar.cpp
+++ b/indra/newview/llvoavatar.cpp
@@ -5391,6 +5391,9 @@ U32 LLVOAvatar::renderRigid()
 		return 0;
 	}
 
+	bool should_alpha_mask = shouldAlphaMask();
+	LLGLState test(GL_ALPHA_TEST, should_alpha_mask);
+
 	if (isTextureVisible(TEX_EYES_BAKED) || (getOverallAppearance() == AOA_JELLYDOLL && !isControlAvatar()) || isUIAvatar())
 	{
 		LLViewerJoint* eyeball_left = getViewerJoint(MESH_ID_EYEBALL_LEFT);
diff --git a/indra/newview/llvoavatarself.cpp b/indra/newview/llvoavatarself.cpp
index f5af599db82e694b82f1496b92b3239724c8ddb1..481457d816f5c1eb12c79fb13f55b5a3c3718415 100644
--- a/indra/newview/llvoavatarself.cpp
+++ b/indra/newview/llvoavatarself.cpp
@@ -1005,7 +1005,7 @@ void LLVOAvatarSelf::updateRegion(LLViewerRegion *regionp)
 }
 
 //--------------------------------------------------------------------
-// draw tractor beam when editing objects
+// draw tractor (selection) beam when editing objects
 //--------------------------------------------------------------------
 //virtual
 void LLVOAvatarSelf::idleUpdateTractorBeam()
@@ -1365,6 +1365,27 @@ BOOL LLVOAvatarSelf::detachObject(LLViewerObject *viewer_object)
 	return FALSE;
 }
 
+bool LLVOAvatarSelf::hasAttachmentsInTrash()
+{
+    const LLUUID trash_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_TRASH);
+
+    for (attachment_map_t::const_iterator iter = mAttachmentPoints.begin(); iter != mAttachmentPoints.end(); ++iter)
+    {
+        LLViewerJointAttachment *attachment = iter->second;
+        for (LLViewerJointAttachment::attachedobjs_vec_t::iterator attachment_iter = attachment->mAttachedObjects.begin();
+             attachment_iter != attachment->mAttachedObjects.end();
+             ++attachment_iter)
+        {
+            LLViewerObject *attached_object = attachment_iter->get();
+            if (attached_object && gInventory.isObjectDescendentOf(attached_object->getAttachmentItemID(), trash_id))
+            {
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
 // static
 BOOL LLVOAvatarSelf::detachAttachmentIntoInventory(const LLUUID &item_id)
 {
@@ -3161,12 +3182,14 @@ BOOL LLVOAvatarSelf::needsRenderBeam()
 	LLTool *tool = LLToolMgr::getInstance()->getCurrentTool();
 
 	BOOL is_touching_or_grabbing = (tool == LLToolGrab::getInstance() && LLToolGrab::getInstance()->isEditing());
-	if (LLToolGrab::getInstance()->getEditingObject() &&
-		LLToolGrab::getInstance()->getEditingObject()->isAttachment())
-	{
-		// don't render selection beam on hud objects
-		is_touching_or_grabbing = FALSE;
-	}
+    LLViewerObject* objp = LLToolGrab::getInstance()->getEditingObject();
+    if (objp // might need to be "!objp ||" instead of "objp &&".
+        && (objp->isAttachment() || objp->isAvatar()))
+    {
+        // don't render grab tool's selection beam on hud objects,
+        // attachments or avatars
+        is_touching_or_grabbing = FALSE;
+    }
 	return is_touching_or_grabbing || (getAttachmentState() & AGENT_STATE_EDITING && LLSelectMgr::getInstance()->shouldShowSelection());
 }
 
diff --git a/indra/newview/llvoavatarself.h b/indra/newview/llvoavatarself.h
index c90f304f901b8557cdf0c3bfdf9c3615e50768fa..c99f450fd506402baa6174813b7c66666e486ca6 100644
--- a/indra/newview/llvoavatarself.h
+++ b/indra/newview/llvoavatarself.h
@@ -302,6 +302,8 @@ class LLVOAvatarSelf final :
 	/*virtual*/ BOOL 	detachObject(LLViewerObject *viewer_object) override;
 	static BOOL			detachAttachmentIntoInventory(const LLUUID& item_id);
 
+    bool hasAttachmentsInTrash();
+
 // [RLVa:KB] - Checked: 2012-07-28 (RLVa-1.4.7)
 	enum EAttachAction { ACTION_ATTACH, ACTION_DETACH };
 	typedef boost::signals2::signal<void (LLViewerObject*, const LLViewerJointAttachment*, EAttachAction)> attachment_signal_t;
diff --git a/indra/newview/skins/default/xui/da/panel_main_inventory.xml b/indra/newview/skins/default/xui/da/panel_main_inventory.xml
index d6406939c19dfd8a70f97545f042c2add1a8f3ca..37a17f4bd666dbafbc4096f83d109a8956ddf5be 100644
--- a/indra/newview/skins/default/xui/da/panel_main_inventory.xml
+++ b/indra/newview/skins/default/xui/da/panel_main_inventory.xml
@@ -9,20 +9,17 @@
 	<text name="ItemcountText">
 		Genstande:
 	</text>
-	<filter_editor label="Filter" name="inventory search editor"/>
-	<tab_container name="inventory filter tabs">
-		<inventory_panel label="Alle ting" name="All Items"/>
-		<recent_inventory_panel label="Nye ting" name="Recent Items"/>
-	</tab_container>
-	<layout_stack name="bottom_panel">
-		<layout_panel name="options_gear_btn_panel">
-			<button name="options_gear_btn" tool_tip="Vis yderligere valg"/>
-		</layout_panel>
-		<layout_panel name="add_btn_panel">
-			<button name="add_btn" tool_tip="Tilføj ny genstand"/>
-		</layout_panel>
-		<layout_panel name="trash_btn_panel">
-			<dnd_button name="trash_btn" tool_tip="Fjern valgte genstand"/>
-		</layout_panel>
-	</layout_stack>
+    <layout_stack name="top_stack">
+        <layout_panel name="filter_layout_panel">
+            <filter_editor label="Filter" name="inventory search editor"/>
+            <button name="options_gear_btn" tool_tip="Vis yderligere valg"/>
+            <button name="add_btn" tool_tip="Tilføj ny genstand"/>
+        </layout_panel>
+    </layout_stack>
+    <panel name="default_inventory_panel">
+        <tab_container name="inventory filter tabs">
+            <inventory_panel label="Alle ting" name="All Items"/>
+            <recent_inventory_panel label="Nye ting" name="Recent Items"/>
+        </tab_container>
+    </panel>
 </panel>
diff --git a/indra/newview/skins/default/xui/de/panel_main_inventory.xml b/indra/newview/skins/default/xui/de/panel_main_inventory.xml
index 175f6e1003f281de1f60837dc2a5857d436447c5..072bd993937b203ab381622a842d63602e316588 100644
--- a/indra/newview/skins/default/xui/de/panel_main_inventory.xml
+++ b/indra/newview/skins/default/xui/de/panel_main_inventory.xml
@@ -13,27 +13,24 @@
 	<text name="ItemcountText">
 		Objekte:
 	</text>
-	<filter_editor label="Suchtext eingeben" name="inventory search editor"/>
-	<combo_box name="search_type">
-		<item label="Name" name="Name" value="search_by_name"/>
-		<item label="Ersteller" name="Creator" value="search_by_creator"/>
-		<item label="Beschreibung" name="Description" value="search_by_description"/>
-		<item label="UUID" name="UUID" value="search_by_UUID"/>
-	</combo_box>
-	<tab_container name="inventory filter tabs">
-		<inventory_panel label="MEIN INVENTAR" name="All Items"/>
-		<recent_inventory_panel label="AKTUELL" name="Recent Items"/>
-		<inventory_panel label="GETRAGEN" name="Worn Items"/>
-	</tab_container>
-	<layout_stack name="bottom_panel">
-		<layout_panel name="options_gear_btn_panel">
-			<menu_button name="options_gear_btn" tool_tip="Zusätzliche Optionen anzeigen"/>
-		</layout_panel>
-		<layout_panel name="add_btn_panel">
-			<button name="add_btn" tool_tip="Neues Objekt hinzufügen"/>
-		</layout_panel>
-		<layout_panel name="trash_btn_panel">
-			<dnd_button name="trash_btn" tool_tip="Auswahl löschen"/>
-		</layout_panel>
-	</layout_stack>
+    <layout_stack name="top_stack">
+        <layout_panel name="filter_layout_panel">
+            <combo_box name="search_type">
+                <item label="Name" name="Name" value="search_by_name"/>
+                <item label="Ersteller" name="Creator" value="search_by_creator"/>
+                <item label="Beschreibung" name="Description" value="search_by_description"/>
+                <item label="UUID" name="UUID" value="search_by_UUID"/>
+            </combo_box>
+            <filter_editor label="Suchtext eingeben" name="inventory search editor"/>
+            <menu_button name="options_gear_btn" tool_tip="Zusätzliche Optionen anzeigen"/>
+            <button name="add_btn" tool_tip="Neues Objekt hinzufügen"/>
+        </layout_panel>
+    </layout_stack>
+    <panel name="default_inventory_panel">
+        <tab_container name="inventory filter tabs">
+            <inventory_panel label="MEIN INVENTAR" name="All Items"/>
+            <recent_inventory_panel label="AKTUELL" name="Recent Items"/>
+            <inventory_panel label="GETRAGEN" name="Worn Items"/>
+        </tab_container>
+    </panel>
 </panel>
diff --git a/indra/newview/skins/default/xui/en/menu_inventory.xml b/indra/newview/skins/default/xui/en/menu_inventory.xml
index 6692086b844d21ff450f3d941cddf0291030348a..d2887ab84ece5432f758967ca7e475e8f0b72380 100644
--- a/indra/newview/skins/default/xui/en/menu_inventory.xml
+++ b/indra/newview/skins/default/xui/en/menu_inventory.xml
@@ -575,6 +575,14 @@
     <menu_item_separator
      layout="topleft" 
      name="Gesture Separator" />
+    <menu_item_call
+     label="Play"
+     layout="topleft"
+     name="PlayGesture">
+        <menu_item_call.on_click
+         function="Inventory.DoToSelected"
+         parameter="play" />
+    </menu_item_call>
     <menu_item_call
      label="Activate"
      layout="topleft"
diff --git a/indra/newview/skins/default/xui/en/panel_main_inventory.xml b/indra/newview/skins/default/xui/en/panel_main_inventory.xml
index 9ba9a597ff0b8621fd950ec34e968de549e9bf04..0f555f07f52c787bae076d5953fdf847ad229b8e 100644
--- a/indra/newview/skins/default/xui/en/panel_main_inventory.xml
+++ b/indra/newview/skins/default/xui/en/panel_main_inventory.xml
@@ -47,7 +47,8 @@
    top_pad="10"
    left="2"
    right="-4"
-   orientation="horizontal">
+   orientation="horizontal"
+   name="top_stack">
     <layout_panel
      border="false"
      bevel_style="in"
@@ -101,7 +102,8 @@
       user_resize="false"
       height="25"
       width="381"
-      visible="true">
+      visible="true"
+      name="filter_layout_panel">
       <combo_box
        height="23"
        layout="topleft"
diff --git a/indra/newview/skins/default/xui/en/panel_region_environment.xml b/indra/newview/skins/default/xui/en/panel_region_environment.xml
index edf1e1efd47eec5c32c69e99c257939a3a8c9cd4..0b3639f7792cf4d0b355d3f947b48684b80e3210 100644
--- a/indra/newview/skins/default/xui/en/panel_region_environment.xml
+++ b/indra/newview/skins/default/xui/en/panel_region_environment.xml
@@ -259,7 +259,7 @@
                                 follows="left|top"
                                 layout="topleft"
                                 height="24"
-                                width="52"
+                                width="53"
                                 left_delta="2"
                                 top_pad="1"
                                 halign="right"
@@ -271,7 +271,7 @@
                                 follows="left|top"
                                 enabled="false"
                                 top_delta="3"
-                                left_pad="21"
+                                left_pad="20"
                                 height="20"
                                 layout="topleft"
                                 name="edt_invname_alt1"
@@ -305,7 +305,7 @@
                                 follows="left|top"
                                 layout="topleft"
                                 height="24"
-                                width="52"
+                                width="53"
                                 left_delta="2"
                                 top_pad="1"
                                 halign="right"
@@ -317,7 +317,7 @@
                                 follows="left|top"
                                 enabled="false"
                                 top_delta="3"
-                                left_pad="21"
+                                left_pad="20"
                                 height="20"
                                 layout="topleft"
                                 name="edt_invname_alt2"
@@ -351,7 +351,7 @@
                                 follows="left|top"
                                 layout="topleft"
                                 height="25"
-                                width="52"
+                                width="53"
                                 left_delta="2"
                                 top_pad="1"
                                 halign="right"
@@ -363,7 +363,7 @@
                                 follows="left|top"
                                 enabled="false"
                                 top_delta="3"
-                                left_pad="21"
+                                left_pad="20"
                                 height="20"
                                 layout="topleft"
                                 name="edt_invname_alt3"
@@ -460,7 +460,7 @@
                            follows="left|top"
                            layout="topleft"
                            height="12"
-                           width="52"
+                           width="53"
                            left_delta="2"
                            top_pad="2"
                            halign="right"
@@ -477,7 +477,7 @@
                             mouse_opaque="false"
                             visible="true"
                            top_delta="-3"
-                           left_pad="2"/>
+                           left_pad="1"/>
                         <line_editor
                            follows="left|top"
                            enabled="false"
@@ -516,7 +516,7 @@
                            follows="left|top"
                            layout="topleft"
                            height="12"
-                           width="52"
+                           width="53"
                            left_delta="2"
                            top_pad="2"
                            halign="right"
@@ -533,7 +533,7 @@
                            mouse_opaque="false"
                            visible="true"
                            top_delta="-3"
-                           left_pad="2"/>
+                           left_pad="1"/>
                         <line_editor
                                 follows="left|top"
                                 enabled="false"
diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml
index fd01bd0a955baab5929f9d4573411e25d5b910e5..e621f0dcda69adddd67b2b774802746554bfe7e7 100644
--- a/indra/newview/skins/default/xui/en/strings.xml
+++ b/indra/newview/skins/default/xui/en/strings.xml
@@ -2907,7 +2907,7 @@ If you continue to receive this message, please contact Second Life support for
 	<string name="GroupMoneyBalance">Balance</string>
 	<string name="GroupMoneyCredits">Credits</string>
 	<string name="GroupMoneyDebits">Debits</string>
-	<string name="GroupMoneyDate">[weekday,datetime,utc] [mth,datetime,utc] [day,datetime,utc], [year,datetime,utc]</string>
+	<string name="GroupMoneyStartDate">Transactions since [weekday,datetime,utc] [mth,datetime,utc] [day,datetime,utc], [year,datetime,utc]</string>
 
 	<!-- Viewer menu -->
 	<string name="AcquiredItems">Acquired Items</string>
diff --git a/indra/newview/skins/default/xui/es/panel_main_inventory.xml b/indra/newview/skins/default/xui/es/panel_main_inventory.xml
index bf1205046bf54bf05c40372bfa75af4b3bc86dab..a1a88b095e82e90d057902d32532183b7bfc2459 100644
--- a/indra/newview/skins/default/xui/es/panel_main_inventory.xml
+++ b/indra/newview/skins/default/xui/es/panel_main_inventory.xml
@@ -13,27 +13,24 @@
 	<text name="ItemcountText">
 		Ítems:
 	</text>
-	<filter_editor label="Ingresar texto de búsqueda" name="inventory search editor"/>
-	<combo_box name="search_type">
-		<item label="Nombre" name="Name" value="search_by_name"/>
-		<item label="Creador" name="Creator" value="search_by_creator"/>
-		<item label="Descripción" name="Description" value="search_by_description"/>
-		<item label="UUID" name="UUID" value="search_by_UUID"/>
-	</combo_box>
-	<tab_container name="inventory filter tabs">
-		<inventory_panel label="Todos los ítems" name="All Items"/>
-		<recent_inventory_panel label="Ítems recientes" name="Recent Items"/>
-		<inventory_panel label="(VESTIMENTA)" name="Worn Items"/>
-	</tab_container>
-	<layout_stack name="bottom_panel">
-		<layout_panel name="options_gear_btn_panel">
-			<menu_button name="options_gear_btn" tool_tip="Ver más opciones"/>
-		</layout_panel>
-		<layout_panel name="add_btn_panel">
-			<button name="add_btn" tool_tip="Añadir un ítem nuevo"/>
-		</layout_panel>
-		<layout_panel name="trash_btn_panel">
-			<dnd_button name="trash_btn" tool_tip="Quitar el ítem seleccionado"/>
-		</layout_panel>
-	</layout_stack>
+    <layout_stack name="top_stack">
+        <layout_panel name="filter_layout_panel">
+            <combo_box name="search_type">
+                <item label="Nombre" name="Name" value="search_by_name"/>
+                <item label="Creador" name="Creator" value="search_by_creator"/>
+                <item label="Descripción" name="Description" value="search_by_description"/>
+                <item label="UUID" name="UUID" value="search_by_UUID"/>
+            </combo_box>
+            <filter_editor label="Ingresar texto de búsqueda" name="inventory search editor"/>
+            <menu_button name="options_gear_btn" tool_tip="Ver más opciones"/>
+            <button name="add_btn" tool_tip="Añadir un ítem nuevo"/>
+        </layout_panel>
+    </layout_stack>
+    <panel name="default_inventory_panel">
+        <tab_container name="inventory filter tabs">
+            <inventory_panel label="Todos los ítems" name="All Items"/>
+            <recent_inventory_panel label="Ítems recientes" name="Recent Items"/>
+            <inventory_panel label="(VESTIMENTA)" name="Worn Items"/>
+        </tab_container>
+    </panel>
 </panel>
diff --git a/indra/newview/skins/default/xui/fr/panel_main_inventory.xml b/indra/newview/skins/default/xui/fr/panel_main_inventory.xml
index 5bf4d6c15d0bfe77a4e8e4adf0923036d67672ae..aebb042cfe9eaefa45991dd39ef12a0e47b3dd95 100644
--- a/indra/newview/skins/default/xui/fr/panel_main_inventory.xml
+++ b/indra/newview/skins/default/xui/fr/panel_main_inventory.xml
@@ -13,27 +13,24 @@
 	<text name="ItemcountText">
 		Articles :
 	</text>
-	<filter_editor label="Saisir ici le texte de la recherche" name="inventory search editor"/>
-	<combo_box name="search_type">
-		<item label="Nom" name="Name" value="search_by_name"/>
-		<item label="Créateur" name="Creator" value="search_by_creator"/>
-		<item label="Description" name="Description" value="search_by_description"/>
-		<item label="UUID" name="UUID" value="search_by_UUID"/>
-	</combo_box>
-	<tab_container name="inventory filter tabs">
-		<inventory_panel label="MON INVENTAIRE" name="All Items"/>
-		<recent_inventory_panel label="RÉCENT" name="Recent Items"/>
-		<inventory_panel label="PORTÉ" name="Worn Items"/>
-	</tab_container>
-	<layout_stack name="bottom_panel">
-		<layout_panel name="options_gear_btn_panel">
-			<menu_button name="options_gear_btn" tool_tip="Afficher d&apos;autres options"/>
-		</layout_panel>
-		<layout_panel name="add_btn_panel">
-			<button name="add_btn" tool_tip="Ajouter un nouvel article"/>
-		</layout_panel>
-		<layout_panel name="trash_btn_panel">
-			<dnd_button name="trash_btn" tool_tip="Supprimer l&apos;article sélectionné"/>
-		</layout_panel>
-	</layout_stack>
+    <layout_stack name="top_stack">
+        <layout_panel name="filter_layout_panel">
+            <combo_box name="search_type">
+                <item label="Nom" name="Name" value="search_by_name"/>
+                <item label="Créateur" name="Creator" value="search_by_creator"/>
+                <item label="Description" name="Description" value="search_by_description"/>
+                <item label="UUID" name="UUID" value="search_by_UUID"/>
+            </combo_box>
+            <filter_editor label="Saisir ici le texte de la recherche" name="inventory search editor"/>
+            <menu_button name="options_gear_btn" tool_tip="Afficher d&apos;autres options"/>
+            <button name="add_btn" tool_tip="Ajouter un nouvel article"/>
+        </layout_panel>
+    </layout_stack>
+    <panel name="default_inventory_panel">
+        <tab_container name="inventory filter tabs">
+            <inventory_panel label="MON INVENTAIRE" name="All Items"/>
+            <recent_inventory_panel label="RÉCENT" name="Recent Items"/>
+            <inventory_panel label="PORTÉ" name="Worn Items"/>
+        </tab_container>
+    </panel>
 </panel>
diff --git a/indra/newview/skins/default/xui/it/panel_main_inventory.xml b/indra/newview/skins/default/xui/it/panel_main_inventory.xml
index d6890229e7ef667838b99497b9144f3ca072f75a..da8592e5d7e8dc1de072e3d7e574f06d6a94b028 100644
--- a/indra/newview/skins/default/xui/it/panel_main_inventory.xml
+++ b/indra/newview/skins/default/xui/it/panel_main_inventory.xml
@@ -13,27 +13,24 @@
 	<text name="ItemcountText">
 		Oggetti:
 	</text>
-	<filter_editor label="Inserisci ricerca" name="inventory search editor"/>
-	<combo_box name="search_type">
-		<item label="Nome" name="Name" value="search_by_name"/>
-		<item label="Creatore" name="Creator" value="search_by_creator"/>
-		<item label="Descrizione" name="Description" value="search_by_description"/>
-		<item label="UUID" name="UUID" value="search_by_UUID"/>
-	</combo_box>
-	<tab_container name="inventory filter tabs">
-		<inventory_panel label="Tutti gli elementi" name="All Items"/>
-		<recent_inventory_panel label="Elementi recenti" name="Recent Items"/>
-		<inventory_panel label="INDOSSATI" name="Worn Items"/>
-	</tab_container>
-	<layout_stack name="bottom_panel">
-		<layout_panel name="options_gear_btn_panel">
-			<menu_button name="options_gear_btn" tool_tip="Mostra opzioni addizionali"/>
-		</layout_panel>
-		<layout_panel name="add_btn_panel">
-			<button name="add_btn" tool_tip="Aggiungi nuovo elemento"/>
-		</layout_panel>
-		<layout_panel name="trash_btn_panel">
-			<dnd_button name="trash_btn" tool_tip="Rimuovi l&apos;articolo selezionato"/>
-		</layout_panel>
-	</layout_stack>
+    <layout_stack name="top_stack">
+        <layout_panel name="filter_layout_panel">
+            <combo_box name="search_type">
+                <item label="Nome" name="Name" value="search_by_name"/>
+                <item label="Creatore" name="Creator" value="search_by_creator"/>
+                <item label="Descrizione" name="Description" value="search_by_description"/>
+                <item label="UUID" name="UUID" value="search_by_UUID"/>
+            </combo_box>
+            <filter_editor label="Inserisci ricerca" name="inventory search editor"/>
+            <menu_button name="options_gear_btn" tool_tip="Mostra opzioni addizionali"/>
+            <button name="add_btn" tool_tip="Aggiungi nuovo elemento"/>
+        </layout_panel>
+    </layout_stack>
+    <panel name="default_inventory_panel">
+        <tab_container name="inventory filter tabs">
+            <inventory_panel label="Tutti gli elementi" name="All Items"/>
+            <recent_inventory_panel label="Elementi recenti" name="Recent Items"/>
+            <inventory_panel label="INDOSSATI" name="Worn Items"/>
+        </tab_container>
+    </panel>
 </panel>
diff --git a/indra/newview/skins/default/xui/pl/panel_main_inventory.xml b/indra/newview/skins/default/xui/pl/panel_main_inventory.xml
index 1011c38378fedc193b3453c9a14b948d105383ff..34d9d995bc093080e7bb1f023c92223b88651dcd 100644
--- a/indra/newview/skins/default/xui/pl/panel_main_inventory.xml
+++ b/indra/newview/skins/default/xui/pl/panel_main_inventory.xml
@@ -10,20 +10,17 @@
 	<text name="ItemcountText">
 		Obiekty:
 	</text>
-	<filter_editor label="Filtruj SzafÄ™" name="inventory search editor" />
-	<tab_container name="inventory filter tabs">
-		<inventory_panel label="MOJA SZAFA" name="All Items" />
-		<recent_inventory_panel label="OSTATNIE" name="Recent Items" />
-	</tab_container>
-	<layout_stack name="bottom_panel">
-		<layout_panel name="options_gear_btn_panel">
-			<menu_button tool_tip="Pokaż dodatkowe opcje" name="options_gear_btn" />
-		</layout_panel>
-		<layout_panel name="add_btn_panel">
-			<button name="add_btn" tool_tip="Dodaj nowy obiekt" />
-		</layout_panel>
-		<layout_panel name="trash_btn_panel">
-			<dnd_button name="trash_btn" tool_tip="Usuń zaznaczony obiekt" />
-		</layout_panel>
-	</layout_stack>
+    <layout_stack name="top_stack">
+        <layout_panel name="filter_layout_panel">
+            <filter_editor label="Filtruj SzafÄ™" name="inventory search editor" />
+            <menu_button tool_tip="Pokaż dodatkowe opcje" name="options_gear_btn" />
+            <button name="add_btn" tool_tip="Dodaj nowy obiekt" />
+        </layout_panel>
+    </layout_stack>
+    <panel name="default_inventory_panel">
+        <tab_container name="inventory filter tabs">
+            <inventory_panel label="MOJA SZAFA" name="All Items" />
+            <recent_inventory_panel label="OSTATNIE" name="Recent Items" />
+        </tab_container>
+    </panel>
 </panel>
diff --git a/indra/newview/skins/default/xui/pt/panel_main_inventory.xml b/indra/newview/skins/default/xui/pt/panel_main_inventory.xml
index e0cf528468ee7492b4c7debbb60a1cd4c099b3eb..debd601966de83938cfaeb1b6f5b196028e38dc4 100644
--- a/indra/newview/skins/default/xui/pt/panel_main_inventory.xml
+++ b/indra/newview/skins/default/xui/pt/panel_main_inventory.xml
@@ -13,27 +13,24 @@
 	<text name="ItemcountText">
 		Itens:
 	</text>
-	<filter_editor label="Digite o texto de pesquisa" name="inventory search editor"/>
-	<combo_box name="search_type">
-		<item label="Nome" name="Name" value="search_by_name"/>
-		<item label="Criador" name="Creator" value="search_by_creator"/>
-		<item label="Descrição" name="Description" value="search_by_description"/>
-		<item label="UUID" name="UUID" value="search_by_UUID"/>
-	</combo_box>
-	<tab_container name="inventory filter tabs">
-		<inventory_panel label="Todos os itens" name="All Items"/>
-		<recent_inventory_panel label="Itens recentes" name="Recent Items"/>
-		<inventory_panel label="USADO" name="Worn Items"/>
-	</tab_container>
-	<layout_stack name="bottom_panel">
-		<layout_panel name="options_gear_btn_panel">
-			<menu_button name="options_gear_btn" tool_tip="Mostrar opções adicionais"/>
-		</layout_panel>
-		<layout_panel name="add_btn_panel">
-			<button name="add_btn" tool_tip="Adicionar novo item"/>
-		</layout_panel>
-		<layout_panel name="trash_btn_panel">
-			<dnd_button name="trash_btn" tool_tip="Remover item selecionado"/>
-		</layout_panel>
-	</layout_stack>
+    <layout_stack name="top_stack">
+        <layout_panel name="filter_layout_panel">
+            <combo_box name="search_type">
+                <item label="Nome" name="Name" value="search_by_name"/>
+                <item label="Criador" name="Creator" value="search_by_creator"/>
+                <item label="Descrição" name="Description" value="search_by_description"/>
+                <item label="UUID" name="UUID" value="search_by_UUID"/>
+            </combo_box>
+            <filter_editor label="Digite o texto de pesquisa" name="inventory search editor"/>
+            <menu_button name="options_gear_btn" tool_tip="Mostrar opções adicionais"/>
+            <button name="add_btn" tool_tip="Adicionar novo item"/>
+        </layout_panel>
+    </layout_stack>
+    <panel name="default_inventory_panel">
+        <tab_container name="inventory filter tabs">
+            <inventory_panel label="Todos os itens" name="All Items"/>
+            <recent_inventory_panel label="Itens recentes" name="Recent Items"/>
+            <inventory_panel label="USADO" name="Worn Items"/>
+        </tab_container>
+    </panel>
 </panel>
diff --git a/indra/newview/skins/default/xui/ru/panel_main_inventory.xml b/indra/newview/skins/default/xui/ru/panel_main_inventory.xml
index b473fb8f9826c0c900077ab6cb0d11ceb0dacffe..a0ff3a9f67f63458c622671e64d4bcb875f73a15 100644
--- a/indra/newview/skins/default/xui/ru/panel_main_inventory.xml
+++ b/indra/newview/skins/default/xui/ru/panel_main_inventory.xml
@@ -13,27 +13,24 @@
 	<text name="ItemcountText">
 		Вещи:
 	</text>
-	<filter_editor label="Введите текст поиска" name="inventory search editor"/>
-	<combo_box name="search_type">
-		<item label="Название" name="Name" value="search_by_name"/>
-		<item label="Создатель" name="Creator" value="search_by_creator"/>
-		<item label="Описание" name="Description" value="search_by_description"/>
-		<item label="УУИд" name="UUID" value="search_by_UUID"/>
-	</combo_box>
-	<tab_container name="inventory filter tabs">
-		<inventory_panel label="МОЙ ИНВЕНТАРЬ" name="All Items"/>
-		<recent_inventory_panel label="НЕДАВНИЕ" name="Recent Items"/>
-		<inventory_panel label="НОСИМОЕ" name="Worn Items"/>
-	</tab_container>
-	<layout_stack name="bottom_panel">
-		<layout_panel name="options_gear_btn_panel">
-			<menu_button name="options_gear_btn" tool_tip="Показать дополнительные параметры"/>
-		</layout_panel>
-		<layout_panel name="add_btn_panel">
-			<button name="add_btn" tool_tip="Добавить новую вещь"/>
-		</layout_panel>
-		<layout_panel name="trash_btn_panel">
-			<dnd_button name="trash_btn" tool_tip="Удалить выбранную вещь"/>
-		</layout_panel>
-	</layout_stack>
+    <layout_stack name="top_stack">
+        <layout_panel name="filter_layout_panel">
+            <combo_box name="search_type">
+                <item label="Название" name="Name" value="search_by_name"/>
+                <item label="Создатель" name="Creator" value="search_by_creator"/>
+                <item label="Описание" name="Description" value="search_by_description"/>
+                <item label="УУИд" name="UUID" value="search_by_UUID"/>
+            </combo_box>
+            <filter_editor label="Введите текст поиска" name="inventory search editor"/>
+            <menu_button name="options_gear_btn" tool_tip="Показать дополнительные параметры"/>
+            <button name="add_btn" tool_tip="Добавить новую вещь"/>
+        </layout_panel>
+    </layout_stack>
+    <panel name="default_inventory_panel">
+        <tab_container name="inventory filter tabs">
+            <inventory_panel label="МОЙ ИНВЕНТАРЬ" name="All Items"/>
+            <recent_inventory_panel label="НЕДАВНИЕ" name="Recent Items"/>
+            <inventory_panel label="НОСИМОЕ" name="Worn Items"/>
+        </tab_container>
+    </panel>
 </panel>
diff --git a/indra/newview/skins/default/xui/tr/panel_main_inventory.xml b/indra/newview/skins/default/xui/tr/panel_main_inventory.xml
index 7e980786354df4f16d73c54000704aa193606497..af55d4cafc88fed12a511540658c8517e0076d45 100644
--- a/indra/newview/skins/default/xui/tr/panel_main_inventory.xml
+++ b/indra/newview/skins/default/xui/tr/panel_main_inventory.xml
@@ -13,27 +13,24 @@
 	<text name="ItemcountText">
 		Ögeler:
 	</text>
-	<filter_editor label="Arama metnini gir" name="inventory search editor"/>
-	<combo_box name="search_type">
-		<item label="Ad" name="Name" value="search_by_name"/>
-		<item label="OluÅŸturan" name="Creator" value="search_by_creator"/>
-		<item label="Açıklama" name="Description" value="search_by_description"/>
-		<item label="UUID" name="UUID" value="search_by_UUID"/>
-	</combo_box>
-	<tab_container name="inventory filter tabs">
-		<inventory_panel label="ENVANTERÄ°M" name="All Items"/>
-		<recent_inventory_panel label="SON" name="Recent Items"/>
-		<inventory_panel label="GÄ°YÄ°LEN" name="Worn Items"/>
-	</tab_container>
-	<layout_stack name="bottom_panel">
-		<layout_panel name="options_gear_btn_panel">
-			<menu_button name="options_gear_btn" tool_tip="İlave seçenekleri göster"/>
-		</layout_panel>
-		<layout_panel name="add_btn_panel">
-			<button name="add_btn" tool_tip="Yeni öge ekle"/>
-		</layout_panel>
-		<layout_panel name="trash_btn_panel">
-			<dnd_button name="trash_btn" tool_tip="Seçilen öğeyi sil"/>
-		</layout_panel>
-	</layout_stack>
+    <layout_stack name="top_stack">
+        <layout_panel name="filter_layout_panel">
+            <combo_box name="search_type">
+                <item label="Ad" name="Name" value="search_by_name"/>
+                <item label="OluÅŸturan" name="Creator" value="search_by_creator"/>
+                <item label="Açıklama" name="Description" value="search_by_description"/>
+                <item label="UUID" name="UUID" value="search_by_UUID"/>
+            </combo_box>
+            <filter_editor label="Arama metnini gir" name="inventory search editor"/>
+            <menu_button name="options_gear_btn" tool_tip="İlave seçenekleri göster"/>
+            <button name="add_btn" tool_tip="Yeni öge ekle"/>
+        </layout_panel>
+    </layout_stack>
+    <panel name="default_inventory_panel">
+        <tab_container name="inventory filter tabs">
+            <inventory_panel label="ENVANTERÄ°M" name="All Items"/>
+            <recent_inventory_panel label="SON" name="Recent Items"/>
+            <inventory_panel label="GÄ°YÄ°LEN" name="Worn Items"/>
+        </tab_container>
+    </panel>
 </panel>
diff --git a/indra/newview/skins/default/xui/zh/panel_main_inventory.xml b/indra/newview/skins/default/xui/zh/panel_main_inventory.xml
index 9ffa9323cc55c80592a7df5305e84c39e479464b..6a8ead28b41deb07f9adaa219368a73ef11e4b9a 100644
--- a/indra/newview/skins/default/xui/zh/panel_main_inventory.xml
+++ b/indra/newview/skins/default/xui/zh/panel_main_inventory.xml
@@ -13,27 +13,24 @@
 	<text name="ItemcountText">
 		物品:
 	</text>
-	<filter_editor label="輸入搜尋文字" name="inventory search editor"/>
-	<combo_box name="search_type">
-		<item label="名稱" name="Name" value="search_by_name"/>
-		<item label="創造者" name="Creator" value="search_by_creator"/>
-		<item label="描述" name="Description" value="search_by_description"/>
-		<item label="UUID" name="UUID" value="search_by_UUID"/>
-	</combo_box>
-	<tab_container name="inventory filter tabs">
-		<inventory_panel label="我的收納區" name="All Items"/>
-		<recent_inventory_panel label="最近" name="Recent Items"/>
-		<inventory_panel label="已穿戴" name="Worn Items"/>
-	</tab_container>
-	<layout_stack name="bottom_panel">
-		<layout_panel name="options_gear_btn_panel">
-			<menu_button name="options_gear_btn" tool_tip="顯示額外選項"/>
-		</layout_panel>
-		<layout_panel name="add_btn_panel">
-			<button name="add_btn" tool_tip="添加新物品"/>
-		</layout_panel>
-		<layout_panel name="trash_btn_panel">
-			<dnd_button name="trash_btn" tool_tip="移除所選擇的物品"/>
-		</layout_panel>
-	</layout_stack>
+    <layout_stack name="top_stack">
+        <layout_panel name="filter_layout_panel">
+            <combo_box name="search_type">
+                <item label="名稱" name="Name" value="search_by_name"/>
+                <item label="創造者" name="Creator" value="search_by_creator"/>
+                <item label="描述" name="Description" value="search_by_description"/>
+                <item label="UUID" name="UUID" value="search_by_UUID"/>
+            </combo_box>
+            <filter_editor label="輸入搜尋文字" name="inventory search editor"/>
+            <menu_button name="options_gear_btn" tool_tip="顯示額外選項"/>
+            <button name="add_btn" tool_tip="添加新物品"/>
+        </layout_panel>
+    </layout_stack>
+    <panel name="default_inventory_panel">
+        <tab_container name="inventory filter tabs">
+            <inventory_panel label="我的收納區" name="All Items"/>
+            <recent_inventory_panel label="最近" name="Recent Items"/>
+            <inventory_panel label="已穿戴" name="Worn Items"/>
+        </tab_container>
+    </panel>
 </panel>
diff --git a/indra/test/CMakeLists.txt b/indra/test/CMakeLists.txt
index b4d57b26099d48a679288af1002831a6fd6b5830..97f24f2e9caf7d07f75265e1486732a3c5c2a815 100644
--- a/indra/test/CMakeLists.txt
+++ b/indra/test/CMakeLists.txt
@@ -71,6 +71,13 @@ if (WINDOWS)
           LINK_FLAGS "/NODEFAULTLIB:LIBCMT"
           LINK_FLAGS_DEBUG "/NODEFAULTLIB:\"LIBCMT;LIBCMTD;MSVCRT\""
           )
+elseif (DARWIN)
+  # Support our "@executable_path/../Resources" load path for our test
+  # executable. This SHOULD properly be "$<TARGET_FILE_DIR:lltest>/Resources",
+  # but the CMake $<TARGET_FILE_DIR> generator expression isn't evaluated by
+  # CREATE_LINK, so fudge it.
+  file(CREATE_LINK "../sharedlibs/Release/Resources" "${CMAKE_BINARY_DIR}/test/Resources"
+       SYMBOLIC)
 endif (WINDOWS)
 
 set(TEST_EXE $<TARGET_FILE:lltest>)
diff --git a/indra/test/test.cpp b/indra/test/test.cpp
index 06ae583d76dfa98c16f58089a0a6e5ebc4051e90..dd1ac369eb693f7ddd61f4f52846a952b0e3e31c 100644
--- a/indra/test/test.cpp
+++ b/indra/test/test.cpp
@@ -252,7 +252,7 @@ class LLTestCallback : public chained_callback
 				break;
 			case tut::test_result::ex:
 				++mFailedTests;
-				out << "exception: " << tr.exception_typeid;
+				out << "exception: " << LLError::Log::demangle(tr.exception_typeid.c_str());
 				break;
 			case tut::test_result::warn:
 				++mFailedTests;