diff options
Diffstat (limited to 'indra')
367 files changed, 15392 insertions, 2838 deletions
diff --git a/indra/cmake/CMakeLists.txt b/indra/cmake/CMakeLists.txt index 73b614e0af..0138a1426e 100644 --- a/indra/cmake/CMakeLists.txt +++ b/indra/cmake/CMakeLists.txt @@ -40,6 +40,7 @@ set(cmake_SOURCE_FILES LLTestCommand.cmake LLWindow.cmake Linking.cmake + Lualibs.cmake Meshoptimizer.cmake NDOF.cmake OPENAL.cmake diff --git a/indra/cmake/Lualibs.cmake b/indra/cmake/Lualibs.cmake new file mode 100644 index 0000000000..d66305a8e5 --- /dev/null +++ b/indra/cmake/Lualibs.cmake @@ -0,0 +1,29 @@ +# -*- cmake -*- + +include_guard() + +include(Prebuilt) + +add_library( ll::lualibs INTERFACE IMPORTED ) + +use_system_binary( lualibs ) + +use_prebuilt_binary(luau) + +target_include_directories( ll::lualibs SYSTEM INTERFACE + ${LIBS_PREBUILT_DIR}/include +) + +if (WINDOWS) + target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/Luau.Ast.lib) + target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/Luau.CodeGen.lib) + target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/Luau.Compiler.lib) + target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/Luau.Config.lib) + target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/Luau.VM.lib) +else () + target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/libLuau.CodeGen.a) + target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/libLuau.Compiler.a) + target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/libLuau.Config.a) + target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/libLuau.VM.a) + target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/libLuau.Ast.a) +endif () diff --git a/indra/cmake/Prebuilt.cmake b/indra/cmake/Prebuilt.cmake index 634cc15c21..a8c702bfef 100644 --- a/indra/cmake/Prebuilt.cmake +++ b/indra/cmake/Prebuilt.cmake @@ -40,6 +40,7 @@ macro (use_prebuilt_binary _binary) --install-dir=${AUTOBUILD_INSTALL_DIR} ${_binary} ") endif(DEBUG_PREBUILT) + message(STATUS "Installing ${_binary}...") execute_process(COMMAND "${AUTOBUILD_EXECUTABLE}" install --install-dir=${AUTOBUILD_INSTALL_DIR} diff --git a/indra/cmake/run_build_test.py b/indra/cmake/run_build_test.py index 940a130a50..312d791d67 100755 --- a/indra/cmake/run_build_test.py +++ b/indra/cmake/run_build_test.py @@ -122,19 +122,17 @@ def main(command, arguments=[], libpath=[], vars={}): # Make sure we see all relevant output *before* child-process output. sys.stdout.flush() try: - return subprocess.call(command_list) - except OSError as err: + return subprocess.run(command_list).returncode + except FileNotFoundError as err: # If the caller is trying to execute a test program that doesn't # exist, we want to produce a reasonable error message rather than a # traceback. This happens when the build is halted by errors, but # CMake tries to proceed with testing anyway <eyeroll/>. However, do # NOT attempt to handle any error but "doesn't exist." - if err.errno != errno.ENOENT: - raise # In practice, the pathnames into CMake's build tree are so long as to # obscure the name of the test program. Just log its basename. - log.warn("No such program %s; check for preceding build errors" % \ - os.path.basename(command[0])) + log.warning("No such program %s; check for preceding build errors" % + os.path.basename(command[0])) # What rc should we simulate for missing executable? Windows produces # 9009. return 9009 diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 437b8d0168..f47136f781 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -16,6 +16,8 @@ include(Tracy) set(llcommon_SOURCE_FILES apply.cpp commoncontrol.cpp + coro_scheduler.cpp + hbxxh.cpp indra_constants.cpp lazyeventapi.cpp llapp.cpp @@ -53,8 +55,8 @@ set(llcommon_SOURCE_FILES llframetimer.cpp llheartbeat.cpp llheteromap.cpp - llinitparam.cpp llinitdestroyclass.cpp + llinitparam.cpp llinstancetracker.cpp llkeybind.cpp llleap.cpp @@ -64,15 +66,15 @@ set(llcommon_SOURCE_FILES llmd5.cpp llmemory.cpp llmemorystream.cpp - llmetrics.cpp llmetricperformancetester.cpp + llmetrics.cpp llmortician.cpp llmutex.cpp - llptrto.cpp llpredicate.cpp llprocess.cpp llprocessor.cpp llprocinfo.cpp + llptrto.cpp llqueuedthread.cpp llrand.cpp llrefcount.cpp @@ -102,9 +104,13 @@ set(llcommon_SOURCE_FILES lluriparser.cpp lluuid.cpp llworkerthread.cpp - hbxxh.cpp - u64.cpp + lockstatic.cpp + lua_function.cpp + lualistener.cpp + resultset.cpp threadpool.cpp + throttle.cpp + u64.cpp workqueue.cpp StackWalker.cpp ) @@ -117,9 +123,12 @@ set(llcommon_HEADER_FILES chrono.h classic_callback.h commoncontrol.h + coro_scheduler.h ctype_workaround.h fix_macros.h + fsyspath.h function_types.h + hbxxh.h indra_constants.h lazyeventapi.h linden_common.h @@ -138,6 +147,7 @@ set(llcommon_HEADER_FILES llcommonutils.h llcond.h llcoros.h + llcoromutex.h llcrc.h llcriticaldamp.h lldate.h @@ -154,9 +164,9 @@ set(llcommon_HEADER_FILES lleventapi.h lleventcoro.h lleventdispatcher.h + lleventemitter.h lleventfilter.h llevents.h - lleventemitter.h llexception.h llfasttimer.h llfile.h @@ -173,6 +183,7 @@ set(llcommon_HEADER_FILES llinitparam.h llinstancetracker.h llinstancetrackersubclass.h + llinttracker.h llkeybind.h llkeythrottle.h llleap.h @@ -183,14 +194,12 @@ set(llcommon_HEADER_FILES llmd5.h llmemory.h llmemorystream.h - llmetrics.h llmetricperformancetester.h + llmetrics.h llmortician.h llmutex.h llnametable.h llpointer.h - llprofiler.h - llprofilercategories.h llpounceable.h llpredicate.h llpreprocessor.h @@ -198,6 +207,8 @@ set(llcommon_HEADER_FILES llprocess.h llprocessor.h llprocinfo.h + llprofiler.h + llprofilercategories.h llptrto.h llqueuedthread.h llrand.h @@ -215,14 +226,14 @@ set(llcommon_HEADER_FILES llsimplehash.h llsingleton.h llstacktrace.h + llstaticstringtable.h + llstatsaccumulator.h llstl.h llstreamqueue.h llstreamtools.h llstrider.h llstring.h llstringtable.h - llstaticstringtable.h - llstatsaccumulator.h llsys.h lltempredirect.h llthread.h @@ -242,13 +253,17 @@ set(llcommon_HEADER_FILES llwin32headers.h llwin32headerslean.h llworkerthread.h - hbxxh.h lockstatic.h + lua_function.h + lualistener.h + resultset.h stdtypes.h stringize.h + tempset.h threadpool.h threadpool_fwd.h threadsafeschedule.h + throttle.h timer.h tuple.h u64.h diff --git a/indra/llcommon/coro_scheduler.cpp b/indra/llcommon/coro_scheduler.cpp new file mode 100644 index 0000000000..02b9f11333 --- /dev/null +++ b/indra/llcommon/coro_scheduler.cpp @@ -0,0 +1,164 @@ +/** + * @file coro_scheduler.cpp + * @author Nat Goodspeed + * @date 2024-08-05 + * @brief Implementation for llcoro::scheduler. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "coro_scheduler.h" +// STL headers +// std headers +#include <iomanip> +// external library headers +#include <boost/fiber/operations.hpp> +// other Linden headers +#include "llcallbacklist.h" +#include "lldate.h" +#include "llerror.h" + +namespace llcoro +{ + +const F64 scheduler::DEFAULT_TIMESLICE{ LL::Timers::DEFAULT_TIMESLICE }; + +const std::string qname("General"); + +scheduler::scheduler(): + // Since use_scheduling_algorithm() must be called before any other + // Boost.Fibers operations, we can assume that the calling fiber is in + // fact the main fiber. + mMainID(boost::this_fiber::get_id()), + mStart(LLDate::now().secondsSinceEpoch()), + mQueue(LL::WorkQueue::getInstance(qname)) +{} + +void scheduler::awakened( boost::fibers::context* ctx) noexcept +{ + if (ctx->get_id() == mMainID) + { + // If the fiber that just came ready is the main fiber, record its + // pointer. + llassert(! mMainCtx); + mMainCtx = ctx; + } + // Delegate to round_robin::awakened() as usual, even for the main fiber. + // This way, as long as other fibers don't take too long, we can just let + // normal round_robin processing pass control to the main fiber. + super::awakened(ctx); +} + +boost::fibers::context* scheduler::pick_next() noexcept +{ + // count calls to pick_next() + ++mSwitches; + // pick_next() is called when the previous fiber has suspended, and we + // need to pick another. Did the previous pick_next() call pick the main + // fiber? If so, it's the main fiber that just suspended. + auto now = LLDate::now().secondsSinceEpoch(); + if (mMainRunning) + { + mMainRunning = false; + mMainLast = now; + } + + boost::fibers::context* next; + + // When the main fiber is ready, and it's been more than mTimeslice since + // the main fiber last ran, it's time to intervene. + F64 elapsed(now - mMainLast); + if (mMainCtx && elapsed > mTimeslice) + { + // We claim that the main fiber is not only stored in mMainCtx, but is + // also queued (somewhere) in our ready list. + llassert(mMainCtx->ready_is_linked()); + // The usefulness of a doubly-linked list is that, given only a + // pointer to an item, we can unlink it. + mMainCtx->ready_unlink(); + // Instead of delegating to round_robin::pick_next() to pop the head + // of the queue, override by returning mMainCtx. + next = mMainCtx; + + /*------------------------- logging stuff --------------------------*/ + // Unless this log tag is enabled, don't even bother posting. + LL_DEBUGS("LLCoros.scheduler"); + // This feature is inherently hard to verify. The logging in the + // lambda below seems useful, but also seems like a lot of overhead + // for a coroutine context switch. Try posting the logging lambda to a + // ThreadPool to offload that overhead. However, if this is still + // taking an unreasonable amount of context-switch time, this whole + // passage could be skipped. + + // Record this event for logging, but push it off to a thread pool to + // perform that work. Presumably std::weak_ptr::lock() is cheaper than + // WorkQueue::getInstance(). + LL::WorkQueue::ptr_t queue{ mQueue.lock() }; + // We probably started before the relevant WorkQueue was created. + if (! queue) + { + // Try again to locate the specified WorkQueue. + queue = LL::WorkQueue::getInstance(qname); + mQueue = queue; + } + // Both the lock() call and the getInstance() call might have failed. + if (queue) + { + // Bind values. Do NOT bind 'this' to avoid cross-thread access! + // It would be interesting to know from what queue position we + // unlinked the main fiber, out of how many in the ready list. + // Unfortunately round_robin::rqueue_ is private, not protected, + // so we have no access. + queue->post( + [switches=mSwitches, start=mStart, elapsed, now] + () + { + U32 runtime(U32(now) - U32(start)); + U32 minutes(runtime / 60u); + U32 seconds(runtime % 60u); + // use stringize to avoid lasting side effects to the + // logging ostream + LL_DEBUGS("LLCoros.scheduler") + << "At time " + << stringize(minutes, ":", std::setw(2), std::setfill('0'), seconds) + << " (" << switches << " switches), coroutines took " + << stringize(std::setprecision(4), elapsed) + << " sec, main coroutine jumped queue" + << LL_ENDL; + }); + } + LL_ENDL; + /*----------------------- end logging stuff ------------------------*/ + } + else + { + // Either the main fiber isn't yet ready, or it hasn't yet been + // mTimeslice seconds since the last time the main fiber ran. Business + // as usual. + next = super::pick_next(); + } + + // super::pick_next() could also have returned the main fiber, which is + // why this is a separate test instead of being folded into the override + // case above. + if (next && next->get_id() == mMainID) + { + // we're about to resume the main fiber: it's no longer "ready" + mMainCtx = nullptr; + // instead, it's "running" + mMainRunning = true; + } + return next; +} + +void scheduler::use() +{ + boost::fibers::use_scheduling_algorithm<scheduler>(); +} + +} // namespace llcoro diff --git a/indra/llcommon/coro_scheduler.h b/indra/llcommon/coro_scheduler.h new file mode 100644 index 0000000000..eee2d746b5 --- /dev/null +++ b/indra/llcommon/coro_scheduler.h @@ -0,0 +1,73 @@ +/** + * @file coro_scheduler.h + * @author Nat Goodspeed + * @date 2024-08-05 + * @brief Custom scheduler for viewer's Boost.Fibers (aka coroutines) + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_CORO_SCHEDULER_H) +#define LL_CORO_SCHEDULER_H + +#include "workqueue.h" +#include <boost/fiber/fiber.hpp> +#include <boost/fiber/algo/round_robin.hpp> + +/** + * llcoro::scheduler is specifically intended for the viewer's main thread. + * Its role is to ensure that the main coroutine, responsible for UI + * operations and coordinating everything else, doesn't get starved by + * secondary coroutines -- however many of those there might be. + * + * The simple boost::fibers::algo::round_robin scheduler could result in + * arbitrary time lag between resumptions of the main coroutine. Of course + * every well-behaved viewer coroutine must be coded to yield before too much + * real time has elapsed, but sheer volume of secondary coroutines could still + * consume unreasonable real time before cycling back to the main coroutine. + */ + +namespace llcoro +{ + +class scheduler: public boost::fibers::algo::round_robin +{ + using super = boost::fibers::algo::round_robin; +public: + // If the main fiber is ready, and it's been at least this long since the + // main fiber last ran, jump the main fiber to the head of the queue. + static const F64 DEFAULT_TIMESLICE; + + scheduler(); + void awakened( boost::fibers::context*) noexcept override; + boost::fibers::context* pick_next() noexcept override; + + static void use(); + +private: + // This is the fiber::id of the main fiber. We use this to discover + // whether the fiber passed to awakened() is in fact the main fiber. + boost::fibers::fiber::id mMainID; + // This context* is nullptr until awakened() notices that the main fiber + // has become ready, at which point it contains the main fiber's context*. + boost::fibers::context* mMainCtx{}; + // Set when pick_next() returns the main fiber. + bool mMainRunning{ false }; + // If it's been at least this long since the last time the main fiber got + // control, jump it to the head of the queue. + F64 mTimeslice{ DEFAULT_TIMESLICE }; + // Timestamp as of the last time we suspended the main fiber. + F64 mMainLast{ 0 }; + // Timestamp of start time + F64 mStart{ 0 }; + // count context switches + U64 mSwitches{ 0 }; + // WorkQueue for deferred logging + LL::WorkQueue::weak_t mQueue; +}; + +} // namespace llcoro + +#endif /* ! defined(LL_CORO_SCHEDULER_H) */ diff --git a/indra/llcommon/fsyspath.h b/indra/llcommon/fsyspath.h new file mode 100644 index 0000000000..1b4aec09b4 --- /dev/null +++ b/indra/llcommon/fsyspath.h @@ -0,0 +1,79 @@ +/** + * @file fsyspath.h + * @author Nat Goodspeed + * @date 2024-04-03 + * @brief Adapt our UTF-8 std::strings for std::filesystem::path + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_FSYSPATH_H) +#define LL_FSYSPATH_H + +#include <filesystem> + +// While std::filesystem::path can be directly constructed from std::string on +// both Posix and Windows, that's not what we want on Windows. Per +// https://en.cppreference.com/w/cpp/filesystem/path/path: + +// ... the method of conversion to the native character set depends on the +// character type used by source. +// +// * If the source character type is char, the encoding of the source is +// assumed to be the native narrow encoding (so no conversion takes place on +// POSIX systems). +// * If the source character type is char8_t, conversion from UTF-8 to native +// filesystem encoding is used. (since C++20) +// * If the source character type is wchar_t, the input is assumed to be the +// native wide encoding (so no conversion takes places on Windows). + +// The trouble is that on Windows, from std::string ("source character type is +// char"), the "native narrow encoding" isn't UTF-8, so file paths containing +// non-ASCII characters get mangled. +// +// Once we're building with C++20, we could pass a UTF-8 std::string through a +// vector<char8_t> to engage std::filesystem::path's own UTF-8 conversion. But +// sigh, as of 2024-04-03 we're not yet there. +// +// Anyway, encapsulating the important UTF-8 conversions in our own subclass +// allows us to migrate forward to C++20 conventions without changing +// referencing code. + +class fsyspath: public std::filesystem::path +{ + using super = std::filesystem::path; + +public: + // default + fsyspath() {} + // construct from UTF-8 encoded std::string + fsyspath(const std::string& path): super(std::filesystem::u8path(path)) {} + // construct from UTF-8 encoded const char* + fsyspath(const char* path): super(std::filesystem::u8path(path)) {} + // construct from existing path + fsyspath(const super& path): super(path) {} + + fsyspath& operator=(const super& p) { super::operator=(p); return *this; } + fsyspath& operator=(const std::string& p) + { + super::operator=(std::filesystem::u8path(p)); + return *this; + } + fsyspath& operator=(const char* p) + { + super::operator=(std::filesystem::u8path(p)); + return *this; + } + + // shadow base-class string() method with UTF-8 aware method + std::string string() const { return super::u8string(); } + // On Posix systems, where value_type is already char, this operator + // std::string() method shadows the base class operator string_type() + // method. But on Windows, where value_type is wchar_t, the base class + // doesn't have operator std::string(). Provide it. + operator std::string() const { return string(); } +}; + +#endif /* ! defined(LL_FSYSPATH_H) */ diff --git a/indra/test/hexdump.h b/indra/llcommon/hexdump.h index 95f1e297c3..ab5ba2b16d 100644..100755 --- a/indra/test/hexdump.h +++ b/indra/llcommon/hexdump.h @@ -1,8 +1,8 @@ /** * @file hexdump.h * @author Nat Goodspeed - * @date 2023-09-08 - * @brief Provide hexdump() and hexmix() ostream formatters + * @date 2023-10-03 + * @brief iostream manipulators to stream hex, or string with nonprinting chars * * $LicenseInfo:firstyear=2023&license=viewerlgpl$ * Copyright (c) 2023, Linden Research, Inc. @@ -17,6 +17,9 @@ #include <iostream> #include <string_view> +namespace LL +{ + // Format a given byte string as 2-digit hex values, no separators // Usage: std::cout << hexdump(somestring) << ... class hexdump @@ -30,6 +33,10 @@ public: hexdump(reinterpret_cast<const unsigned char*>(data), len) {} + hexdump(const std::vector<unsigned char>& data): + hexdump(data.data(), data.size()) + {} + hexdump(const unsigned char* data, size_t len): mData(data, data + len) {} @@ -94,4 +101,6 @@ private: std::string mData; }; +} // namespace LL + #endif /* ! defined(LL_HEXDUMP_H) */ diff --git a/indra/llcommon/lazyeventapi.cpp b/indra/llcommon/lazyeventapi.cpp index 91db0ee4a6..eebed374c3 100644 --- a/indra/llcommon/lazyeventapi.cpp +++ b/indra/llcommon/lazyeventapi.cpp @@ -47,7 +47,9 @@ LL::LazyEventAPIBase::~LazyEventAPIBase() // 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()) + // That said, in a test program, LLEventPumps might never have been + // constructed to start with. + if (mRegistered && LLEventPumps::instanceExists()) { // unregister the callback to this doomed instance LLEventPumps::instance().unregisterPumpFactory(mParams.name); diff --git a/indra/llcommon/llapp.cpp b/indra/llcommon/llapp.cpp index 3db03aec7d..eca611ad6c 100644 --- a/indra/llcommon/llapp.cpp +++ b/indra/llcommon/llapp.cpp @@ -314,7 +314,7 @@ void LLApp::stepFrame() { LLFrameTimer::updateFrameTime(); LLFrameTimer::updateFrameCount(); - LLEventTimer::updateClass(); + LLCallbackList::instance().callFunctions(); mRunner.run(); } diff --git a/indra/llcommon/llassettype.cpp b/indra/llcommon/llassettype.cpp index c09cf7abd2..b0dbc84186 100644 --- a/indra/llcommon/llassettype.cpp +++ b/indra/llcommon/llassettype.cpp @@ -30,6 +30,7 @@ #include "lldictionary.h" #include "llmemory.h" #include "llsingleton.h" +#include "llsd.h" ///---------------------------------------------------------------------------- /// Class LLAssetType @@ -246,3 +247,19 @@ bool LLAssetType::lookupIsAssetIDKnowable(EType asset_type) } return false; } + +LLSD LLAssetType::getTypeNames() +{ + LLSD type_names; + const LLAssetDictionary *dict = LLAssetDictionary::getInstance(); + for (S32 type = 0; type < AT_COUNT; ++type) + { + const AssetEntry *entry = dict->lookup(LLAssetType::EType(type)); + // skip llassettype_bad_lookup + if (entry) + { + type_names.append(entry->mTypeName); + } + } + return type_names; +} diff --git a/indra/llcommon/llassettype.h b/indra/llcommon/llassettype.h index 547c3f4329..17177d81c3 100644 --- a/indra/llcommon/llassettype.h +++ b/indra/llcommon/llassettype.h @@ -165,6 +165,8 @@ public: static bool lookupIsAssetFetchByIDAllowed(EType asset_type); // the asset allows direct download static bool lookupIsAssetIDKnowable(EType asset_type); // asset data can be known by the viewer + static LLSD getTypeNames(); + static const std::string BADLOOKUP; protected: diff --git a/indra/llcommon/llcallbacklist.cpp b/indra/llcommon/llcallbacklist.cpp index 3d5d30bd90..7b05c25c21 100644 --- a/indra/llcommon/llcallbacklist.cpp +++ b/indra/llcommon/llcallbacklist.cpp @@ -24,18 +24,23 @@ * $/LicenseInfo$ */ +#include "lazyeventapi.h" #include "llcallbacklist.h" -#include "lleventtimer.h" -#include "llerrorlegacy.h" - -// Globals -// -LLCallbackList gIdleCallbacks; +#include "llerror.h" +#include "llexception.h" +#include "llsdutil.h" +#include "tempset.h" +#include <boost/container_hash/hash.hpp> +#include <iomanip> +#include <vector> // // Member functions // +/***************************************************************************** +* LLCallbackList +*****************************************************************************/ LLCallbackList::LLCallbackList() { // nothing @@ -45,46 +50,42 @@ LLCallbackList::~LLCallbackList() { } - -void LLCallbackList::addFunction( callback_t func, void *data) +LLCallbackList::handle_t LLCallbackList::addFunction( callback_t func, void *data) { if (!func) { - return; + return {}; } // only add one callback per func/data pair // if (containsFunction(func, data)) { - return; + return {}; } - callback_pair_t t(func, data); - mCallbackList.push_back(t); + auto handle = addFunction([func, data]{ func(data); }); + mLookup.emplace(callback_pair_t(func, data), handle); + return handle; } -bool LLCallbackList::containsFunction( callback_t func, void *data) +LLCallbackList::handle_t LLCallbackList::addFunction( const callable_t& func ) { - callback_pair_t t(func, data); - callback_list_t::iterator iter = find(func,data); - if (iter != mCallbackList.end()) - { - return true; - } - else - { - return false; - } + return mCallbackList.connect(func); } +bool LLCallbackList::containsFunction( callback_t func, void *data) +{ + return mLookup.find(callback_pair_t(func, data)) != mLookup.end(); +} bool LLCallbackList::deleteFunction( callback_t func, void *data) { - callback_list_t::iterator iter = find(func,data); - if (iter != mCallbackList.end()) + auto found = mLookup.find(callback_pair_t(func, data)); + if (found != mLookup.end()) { - mCallbackList.erase(iter); + deleteFunction(found->second); + mLookup.erase(found); return true; } else @@ -93,138 +94,449 @@ bool LLCallbackList::deleteFunction( callback_t func, void *data) } } -inline -LLCallbackList::callback_list_t::iterator -LLCallbackList::find(callback_t func, void *data) +void LLCallbackList::deleteFunction( const handle_t& handle ) { - callback_pair_t t(func, data); - return std::find(mCallbackList.begin(), mCallbackList.end(), t); + handle.disconnect(); } void LLCallbackList::deleteAllFunctions() { - mCallbackList.clear(); + mCallbackList = {}; + mLookup.clear(); } - void LLCallbackList::callFunctions() { - for (callback_list_t::iterator iter = mCallbackList.begin(); iter != mCallbackList.end(); ) + mCallbackList(); +} + +LLCallbackList::handle_t LLCallbackList::doOnIdleOneTime( const callable_t& func ) +{ + // connect_extended() passes the connection to the callback + return mCallbackList.connect_extended( + [func](const handle_t& handle) + { + handle.disconnect(); + func(); + }); +} + +LLCallbackList::handle_t LLCallbackList::doOnIdleRepeating( const bool_func_t& func ) +{ + return mCallbackList.connect_extended( + [func](const handle_t& handle) + { + if (func()) + { + handle.disconnect(); + } + }); +} + +/***************************************************************************** +* LL::Timers +*****************************************************************************/ +namespace LL +{ + +Timers::Timers() {} + +// Call a given callable once at specified timestamp. +Timers::handle_t Timers::scheduleAt(nullary_func_t callable, LLDate::timestamp time) +{ + // tick() assumes you want to run periodically until you return true. + // Schedule a task that returns true after a single call. + return scheduleAtEvery(once(callable), time, 0); +} + +// Call a given callable once after specified interval. +Timers::handle_t Timers::scheduleAfter(nullary_func_t callable, F32 seconds) +{ + return scheduleEvery(once(callable), seconds); +} + +// Call a given callable every specified number of seconds, until it returns true. +Timers::handle_t Timers::scheduleEvery(bool_func_t callable, F32 seconds) +{ + return scheduleAtEvery(callable, now() + seconds, seconds); +} + +Timers::handle_t Timers::scheduleAtEvery(bool_func_t callable, + LLDate::timestamp time, F32 interval) +{ + // Pick token FIRST to store a self-reference in mQueue's managed node as + // well as in mMeta. Pre-increment to distinguish 0 from any live + // handle_t. + token_t token{ ++mToken }; + // For the moment, store a default-constructed mQueue handle -- + // we'll fill in later. + auto [iter, inserted] = mMeta.emplace(token, + Metadata{ queue_t::handle_type(), time, interval }); + // It's important that our token is unique. + llassert(inserted); + + // Remember whether this is the first entry in mQueue + bool first{ mQueue.empty() }; + auto handle{ mQueue.emplace(callable, token, time) }; + // Now that we have an mQueue handle_type, store it in mMeta entry. + iter->second.mHandle = handle; + if (first && ! mLive.connected()) { - callback_list_t::iterator curiter = iter++; - curiter->first(curiter->second); + // If this is our first entry, register for regular callbacks. + mLive = LLCallbackList::instance().doOnIdleRepeating([this]{ return tick(); }); } + // Make an Timers::handle_t from token. + return { token }; } -// Shim class to allow arbitrary boost::bind -// expressions to be run as one-time idle callbacks. -class OnIdleCallbackOneTime +bool Timers::isRunning(handle_t timer) const { -public: - OnIdleCallbackOneTime(nullary_func_t callable): - mCallable(callable) + // A default-constructed timer isn't running. + // A timer we don't find in mMeta has fired or been canceled. + return timer && mMeta.find(timer.token) != mMeta.end(); +} + +F32 Timers::timeUntilCall(handle_t timer) const +{ + MetaMap::const_iterator found; + if ((! timer) || (found = mMeta.find(timer.token)) == mMeta.end()) { + return 0.f; } - static void onIdle(void *data) + else { - gIdleCallbacks.deleteFunction(onIdle, data); - OnIdleCallbackOneTime* self = reinterpret_cast<OnIdleCallbackOneTime*>(data); - self->call(); - delete self; + return narrow(found->second.mTime - now()); } - void call() +} + +// Cancel a future timer set by scheduleAt(), scheduleAfter(), scheduleEvery() +bool Timers::cancel(handle_t& timer) +{ + // For exception safety, capture and clear timer before canceling. + // Once we've canceled this handle, don't retain the live handle. + const handle_t ctimer{ timer }; + timer = handle_t(); + return cancel(ctimer); +} + +bool Timers::cancel(const handle_t& timer) +{ + if (! timer) { - mCallable(); + return false; } -private: - nullary_func_t mCallable; -}; -void doOnIdleOneTime(nullary_func_t callable) -{ - OnIdleCallbackOneTime* cb_functor = new OnIdleCallbackOneTime(callable); - gIdleCallbacks.addFunction(&OnIdleCallbackOneTime::onIdle,cb_functor); + // fibonacci_heap documentation does not address the question of what + // happens if you call erase() twice with the same handle. Is it a no-op? + // Does it invalidate the heap? Is it UB? + + // Nor do we find any documented way to ask whether a given handle still + // tracks a valid heap node. That's why we capture all returned handles in + // mMeta and validate against that collection. What about the pop() + // call in tick()? How to map from the top() value back to the + // corresponding handle_t? That's why we store func_at::mToken. + + // fibonacci_heap provides a pair of begin()/end() methods to iterate over + // all nodes (NOT in heap order), plus a function to convert from such + // iterators to handles. Without mMeta, that would be our only chance + // to validate. + auto found{ mMeta.find(timer.token) }; + if (found == mMeta.end()) + { + // we don't recognize this handle -- maybe the timer has already + // fired, maybe it was previously canceled. + return false; + } + + // Funny case: what if the callback directly or indirectly reaches a + // cancel() call for its own handle? + if (found->second.mRunning) + { + // tick() has special logic to defer the actual deletion until the + // callback has returned + found->second.mCancel = true; + // this handle does in fact reference a live timer, + // which we're going to cancel when we get a chance + return true; + } + + // Erase from mQueue the handle_type referenced by timer.token. + mQueue.erase(found->second.mHandle); + // before erasing the mMeta entry + mMeta.erase(found); + if (mQueue.empty()) + { + // If that was the last active timer, unregister for callbacks. + //LLCallbackList::instance().deleteFunction(mLive); + // Since we're in the source file that knows the true identity of an + // LLCallbackList::handle_t, we don't even need to call instance(). + mLive.disconnect(); + } + return true; } -// Shim class to allow generic boost functions to be run as -// recurring idle callbacks. Callable should return true when done, -// false to continue getting called. -class OnIdleCallbackRepeating +void Timers::setTimeslice(F32 timeslice) { -public: - OnIdleCallbackRepeating(bool_func_t callable): - mCallable(callable) + if (timeslice < MINIMUM_TIMESLICE) { + // use stringize() so setprecision() affects only the temporary + // ostream, not the common logging ostream + LL_WARNS("Timers") << "LL::Timers::setTimeslice(" + << stringize(std::setprecision(4), timeslice) + << ") less than " + << stringize(std::setprecision(4), MINIMUM_TIMESLICE) + << ", ignoring" << LL_ENDL; } - // Will keep getting called until the callable returns true. - static void onIdle(void *data) + else + { + mTimeslice = timeslice; + } +} + +bool Timers::tick() +{ + // Fetch current time only on entry, even though running some mQueue task + // may take long enough that the next one after would become ready. We're + // sharing this thread with everything else, and there's a risk we might + // starve it if we have a sequence of tasks that take nontrivial time. + auto now{ LLDate::now().secondsSinceEpoch() }; + auto cutoff{ now + mTimeslice }; + + // Capture tasks we've processed but that want to be rescheduled. + // Defer rescheduling them immediately to avoid getting stuck looping over + // a recurring task with a nonpositive interval. + std::vector<std::pair<MetaMap::iterator, func_at>> deferred; + + while (! mQueue.empty()) { - OnIdleCallbackRepeating* self = reinterpret_cast<OnIdleCallbackRepeating*>(data); - bool done = self->call(); - if (done) + auto& top{ mQueue.top() }; + if (top.mTime > now) { - gIdleCallbacks.deleteFunction(onIdle, data); - delete self; + // we've hit an entry that's still in the future: + // done with this tick() + break; } + if (LLDate::now().secondsSinceEpoch() > cutoff) + { + // we still have ready tasks, but we've already eaten too much + // time this tick() -- defer until next tick() + break; + } + + // Found a ready task. Look up its corresponding mMeta entry. + auto meta{ mMeta.find(top.mToken) }; + llassert(meta != mMeta.end()); + bool done; + { + // Mark our mMeta entry so we don't cancel this timer while its + // callback is running, but unmark it even in case of exception. + TempSet running(meta->second.mRunning, true); + // run the callback and capture its desire to end repetition + try + { + done = top.mFunc(); + } + catch (...) + { + // Don't crash if a timer callable throws. + // But don't continue calling that callable, either. + done = true; + LOG_UNHANDLED_EXCEPTION("LL::Timers"); + } + } // clear mRunning + + // If mFunc() returned true (all done, stop calling me) or + // meta->mCancel (somebody tried to cancel this timer during the + // callback call), then we're done: clean up both entries. + if (done || meta->second.mCancel) + { + // remove the mMeta entry referencing this task + mMeta.erase(meta); + } + else + { + // mFunc returned false, and nobody asked to cancel: + // continue calling this task at a future time. + meta->second.mTime += meta->second.mInterval; + // capture this task to reschedule once we break loop + deferred.push_back({meta, top}); + // update func_at's mTime to match meta's + deferred.back().second.mTime = meta->second.mTime; + } + // Remove the mQueue entry regardless, or we risk stalling the + // queue right here if we have a nonpositive interval. + mQueue.pop(); } - bool call() + + // Now reschedule any tasks that need to be rescheduled. + for (const auto& [meta, task] : deferred) { - return mCallable(); + auto handle{ mQueue.push(task) }; + // track this new mQueue handle_type + meta->second.mHandle = handle; } -private: - bool_func_t mCallable; -}; -void doOnIdleRepeating(bool_func_t callable) -{ - OnIdleCallbackRepeating* cb_functor = new OnIdleCallbackRepeating(callable); - gIdleCallbacks.addFunction(&OnIdleCallbackRepeating::onIdle,cb_functor); + // If, after all the twiddling above, our queue ended up empty, + // stop calling every tick. + return mQueue.empty(); } -class NullaryFuncEventTimer: public LLEventTimer +/***************************************************************************** +* TimersListener +*****************************************************************************/ + +class TimersListener: public LLEventAPI { public: - NullaryFuncEventTimer(nullary_func_t callable, F32 seconds): - LLEventTimer(seconds), - mCallable(callable) + TimersListener(const LazyEventAPIParams& params): LLEventAPI(params) {} + + // Forbid a script from requesting callbacks too quickly. + static constexpr LLSD::Real MINTIMER{ 0.010 }; + + void scheduleAfter(const LLSD& params); + void scheduleEvery(const LLSD& params); + LLSD cancel(const LLSD& params); + LLSD isRunning(const LLSD& params); + LLSD timeUntilCall(const LLSD& params); + +private: + // We use the incoming reqid to distinguish different timers -- but reqid + // by itself is not unique! Each reqid is local to a calling script. + // Distinguish scripts by reply-pump name, then reqid within script. + // "Additional specializations for std::pair and the standard container + // types, as well as utility functions to compose hashes are available in + // boost::hash." + // https://en.cppreference.com/w/cpp/utility/hash + using HandleKey = std::pair<LLSD::String, LLSD::Integer>; + using HandleMap = std::unordered_map<HandleKey, Timers::temp_handle_t, + boost::hash<HandleKey>>; + HandleMap mHandles; +}; + +void TimersListener::scheduleAfter(const LLSD& params) +{ + // Timer creation functions respond immediately with the reqid of the + // created timer, as well as later when the timer fires. That lets the + // requester invoke cancel, isRunning or timeUntilCall. + Response response(LLSD(), params); + LLSD::Real after{ params["after"] }; + if (after < MINTIMER) { + return response.error(stringize("after must be at least ", MINTIMER)); } -private: - bool tick() + HandleKey key{ params["reply"], params["reqid"] }; + mHandles.emplace( + key, + Timers::instance().scheduleAfter( + [this, params, key] + { + // we don't need any content save for the "reqid" + sendReply({}, params); + // ditch mHandles entry + mHandles.erase(key); + }, + narrow(after))); +} + +void TimersListener::scheduleEvery(const LLSD& params) +{ + // Timer creation functions respond immediately with the reqid of the + // created timer, as well as later when the timer fires. That lets the + // requester invoke cancel, isRunning or timeUntilCall. + Response response(LLSD(), params); + LLSD::Real every{ params["every"] }; + if (every < MINTIMER) { - mCallable(); - return true; + return response.error(stringize("every must be at least ", MINTIMER)); } - nullary_func_t mCallable; -}; + mHandles.emplace( + HandleKey{ params["reply"], params["reqid"] }, + Timers::instance().scheduleEvery( + [params, i=0]() mutable + { + // we don't need any content save for the "reqid" + sendReply(llsd::map("i", i++), params); + // we can't use a handshake -- always keep the ball rolling + return false; + }, + narrow(every))); +} -// Call a given callable once after specified interval. -void doAfterInterval(nullary_func_t callable, F32 seconds) +LLSD TimersListener::cancel(const LLSD& params) { - new NullaryFuncEventTimer(callable, seconds); + auto found{ mHandles.find({params["reply"], params["id"]}) }; + bool ok = false; + if (found != mHandles.end()) + { + ok = true; + Timers::instance().cancel(found->second); + mHandles.erase(found); + } + return llsd::map("ok", ok); } -class BoolFuncEventTimer: public LLEventTimer +LLSD TimersListener::isRunning(const LLSD& params) { -public: - BoolFuncEventTimer(bool_func_t callable, F32 seconds): - LLEventTimer(seconds), - mCallable(callable) + auto found{ mHandles.find({params["reply"], params["id"]}) }; + bool running = false; + if (found != mHandles.end()) { + running = Timers::instance().isRunning(found->second); } -private: - bool tick() + return llsd::map("running", running); +} + +LLSD TimersListener::timeUntilCall(const LLSD& params) +{ + auto found{ mHandles.find({params["reply"], params["id"]}) }; + bool ok = false; + LLSD::Real remaining = 0; + if (found != mHandles.end()) { - return mCallable(); + ok = true; + remaining = Timers::instance().timeUntilCall(found->second); } + return llsd::map("ok", ok, "remaining", remaining); +} - bool_func_t mCallable; +class TimersRegistrar: public LazyEventAPI<TimersListener> +{ + using super = LazyEventAPI<TimersListener>; + using super::listener; + +public: + TimersRegistrar(): + super("Timers", "Provide access to viewer timer functionality.") + { + add("scheduleAfter", +R"-(Create a timer with ID "reqid". Post response after "after" seconds.)-", + &listener::scheduleAfter, + llsd::map("reqid", LLSD::Integer(), "after", LLSD::Real())); + add("scheduleEvery", +R"-(Create a timer with ID "reqid". Post response every "every" seconds +until cancel().)-", + &listener::scheduleEvery, + llsd::map("reqid", LLSD::Integer(), "every", LLSD::Real())); + add("cancel", +R"-(Cancel the timer with ID "id". Respond "ok"=true if "id" identifies +a live timer.)-", + &listener::cancel, + llsd::map("reqid", LLSD::Integer(), "id", LLSD::Integer())); + add("isRunning", +R"-(Query the timer with ID "id": respond "running"=true if "id" identifies +a live timer.)-", + &listener::isRunning, + llsd::map("reqid", LLSD::Integer(), "id", LLSD::Integer())); + add("timeUntilCall", +R"-(Query the timer with ID "id": if "id" identifies a live timer, respond +"ok"=true, "remaining"=seconds with the time left before timer expiry; +otherwise "ok"=false, "remaining"=0.)-", + &listener::timeUntilCall, + llsd::map("reqid", LLSD::Integer())); + } }; +static TimersRegistrar registrar; -// Call a given callable every specified number of seconds, until it returns true. -void doPeriodically(bool_func_t callable, F32 seconds) -{ - new BoolFuncEventTimer(callable, seconds); -} +} // namespace LL diff --git a/indra/llcommon/llcallbacklist.h b/indra/llcommon/llcallbacklist.h index d6c415f7c5..fb4696188a 100644 --- a/indra/llcommon/llcallbacklist.h +++ b/indra/llcommon/llcallbacklist.h @@ -27,53 +27,282 @@ #ifndef LL_LLCALLBACKLIST_H #define LL_LLCALLBACKLIST_H +#include "lldate.h" +#include "llsingleton.h" #include "llstl.h" -#include <boost/function.hpp> -#include <list> +#include <boost/container_hash/hash.hpp> +#include <boost/heap/fibonacci_heap.hpp> +#include <boost/signals2.hpp> +#include <functional> +#include <unordered_map> -class LLCallbackList +/***************************************************************************** +* LLCallbackList: callbacks every idle tick (every callFunctions() call) +*****************************************************************************/ +class LLCallbackList: public LLSingleton<LLCallbackList> { + LLSINGLETON(LLCallbackList); public: typedef void (*callback_t)(void*); - typedef std::pair< callback_t,void* > callback_pair_t; - // NOTE: It is confirmed that we DEPEND on the order provided by using a list :( - // - typedef std::list< callback_pair_t > callback_list_t; + typedef boost::signals2::signal<void()> callback_list_t; + typedef callback_list_t::slot_type callable_t; + typedef boost::signals2::connection handle_t; + typedef boost::signals2::scoped_connection temp_handle_t; + typedef std::function<bool ()> bool_func_t; - LLCallbackList(); ~LLCallbackList(); - void addFunction( callback_t func, void *data = NULL ); // register a callback, which will be called as func(data) + handle_t addFunction( callback_t func, void *data = NULL ); // register a callback, which will be called as func(data) + handle_t addFunction( const callable_t& func ); bool containsFunction( callback_t func, void *data = NULL ); // true if list already contains the function/data pair bool deleteFunction( callback_t func, void *data = NULL ); // removes the first instance of this function/data pair from the list, false if not found - void callFunctions(); // calls all functions + void deleteFunction( const handle_t& handle ); + void callFunctions(); // calls all functions void deleteAllFunctions(); + handle_t doOnIdleOneTime( const callable_t& func ); + handle_t doOnIdleRepeating( const bool_func_t& func ); + bool isRunning(const handle_t& handle) const { return handle.connected(); }; + static void test(); protected: - - inline callback_list_t::iterator find(callback_t func, void *data); - callback_list_t mCallbackList; + + // "Additional specializations for std::pair and the standard container + // types, as well as utility functions to compose hashes are available in + // boost::hash." + // https://en.cppreference.com/w/cpp/utility/hash + typedef std::pair< callback_t,void* > callback_pair_t; + typedef std::unordered_map<callback_pair_t, handle_t, + boost::hash<callback_pair_t>> lookup_table; + lookup_table mLookup; }; -typedef boost::function<void ()> nullary_func_t; -typedef boost::function<bool ()> bool_func_t; +/*-------------------- legacy names in global namespace --------------------*/ +#define gIdleCallbacks (LLCallbackList::instance()) + +using nullary_func_t = LLCallbackList::callable_t; +using bool_func_t = LLCallbackList::bool_func_t; // Call a given callable once in idle loop. -void doOnIdleOneTime(nullary_func_t callable); +inline +LLCallbackList::handle_t doOnIdleOneTime(nullary_func_t callable) +{ + return gIdleCallbacks.doOnIdleOneTime(callable); +} // Repeatedly call a callable in idle loop until it returns true. -void doOnIdleRepeating(bool_func_t callable); +inline +LLCallbackList::handle_t doOnIdleRepeating(bool_func_t callable) +{ + return gIdleCallbacks.doOnIdleRepeating(callable); +} + +/***************************************************************************** +* LL::Timers: callbacks at some future time +*****************************************************************************/ +namespace LL +{ + +class Timers: public LLSingleton<Timers> +{ + LLSINGLETON(Timers); + + using token_t = U32; + + // Define a struct for our priority queue entries, instead of using + // a tuple, because we need to define the comparison operator. + struct func_at + { + // callback to run when this timer fires + bool_func_t mFunc; + // key to look up metadata in mHandles + token_t mToken; + // time at which this timer is supposed to fire + LLDate::timestamp mTime; + + func_at(const bool_func_t& func, token_t token, LLDate::timestamp tm): + mFunc(func), + mToken(token), + mTime(tm) + {} + + friend bool operator<(const func_at& lhs, const func_at& rhs) + { + // use greater-than because we want fibonacci_heap to select the + // EARLIEST time as the top() + return lhs.mTime > rhs.mTime; + } + }; + + // Accept default stable<false>: when two funcs have the same timestamp, + // we don't care in what order they're called. + // Specify constant_time_size<false>: we don't need to optimize the size() + // method, iow we don't need to store and maintain a count of entries. + typedef boost::heap::fibonacci_heap<func_at, boost::heap::constant_time_size<false>> + queue_t; + +public: + // If tasks that come ready during a given tick() take longer than this, + // defer any subsequent ready tasks to a future tick() call. + static constexpr F32 DEFAULT_TIMESLICE{ 0.005f }; + // Setting timeslice to be less than MINIMUM_TIMESLICE could lock up + // Timers processing, causing it to believe it's exceeded the allowable + // time every tick before processing ANY queue items. + static constexpr F32 MINIMUM_TIMESLICE{ 0.001f }; + + class handle_t + { + private: + friend class Timers; + token_t token; + public: + handle_t(token_t token=0): token(token) {} + bool operator==(const handle_t& rhs) const { return this->token == rhs.token; } + explicit operator bool() const { return bool(token); } + bool operator!() const { return ! bool(*this); } + }; + // Call a given callable once at specified timestamp. + handle_t scheduleAt(nullary_func_t callable, LLDate::timestamp time); + + // Call a given callable once after specified interval. + handle_t scheduleAfter(nullary_func_t callable, F32 seconds); + + // Call a given callable every specified number of seconds, until it returns true. + handle_t scheduleEvery(bool_func_t callable, F32 seconds); + + // test whether specified handle is still live + bool isRunning(handle_t timer) const; + // check remaining time + F32 timeUntilCall(handle_t timer) const; + + // Cancel a future timer set by scheduleAt(), scheduleAfter(), scheduleEvery(). + // Return true if and only if the handle corresponds to a live timer. + bool cancel(const handle_t& timer); + // If we're canceling a non-const handle_t, also clear it so we need not + // cancel again. + bool cancel(handle_t& timer); + + F32 getTimeslice() const { return mTimeslice; } + void setTimeslice(F32 timeslice); + + // Store a handle_t returned by scheduleAt(), scheduleAfter() or + // scheduleEvery() in a temp_handle_t to cancel() automatically on + // destruction of the temp_handle_t. + class temp_handle_t + { + public: + temp_handle_t() = default; + temp_handle_t(const handle_t& hdl): mHandle(hdl) {} + temp_handle_t(const temp_handle_t&) = delete; + temp_handle_t(temp_handle_t&&) = default; + temp_handle_t& operator=(const handle_t& hdl) + { + // initializing a new temp_handle_t, then swapping it into *this, + // takes care of destroying any previous mHandle + temp_handle_t replacement(hdl); + swap(replacement); + return *this; + } + temp_handle_t& operator=(const temp_handle_t&) = delete; + temp_handle_t& operator=(temp_handle_t&&) = default; + ~temp_handle_t() + { + cancel(); + } + + // temp_handle_t should be usable wherever handle_t is + operator handle_t() const { return mHandle; } + // If we're dealing with a non-const temp_handle_t, pass a reference + // to our handle_t member (e.g. to Timers::cancel()). + operator handle_t&() { return mHandle; } + + // For those in the know, provide a cancel() method of our own that + // avoids Timers::instance() lookup when mHandle isn't live. + bool cancel() + { + if (! mHandle) + { + return false; + } + else + { + return Timers::instance().cancel(mHandle); + } + } + + void swap(temp_handle_t& other) noexcept + { + std::swap(this->mHandle, other.mHandle); + } + + private: + handle_t mHandle; + }; + +private: + handle_t scheduleAtEvery(bool_func_t callable, LLDate::timestamp time, F32 interval); + LLDate::timestamp now() const { return LLDate::now().secondsSinceEpoch(); } + // wrap a nullary_func_t with a bool_func_t that will only execute once + bool_func_t once(nullary_func_t callable) + { + return [callable] + { + callable(); + return true; + }; + } + bool tick(); + + // NOTE: We don't lock our data members because it doesn't make sense to + // register cross-thread callbacks. If we start wanting to use Timers on + // threads other than the main thread, it would make more sense to make + // our data members thread_local than to lock them. + + // the heap aka priority queue + queue_t mQueue; + + // metadata about a given task + struct Metadata + { + // handle to mQueue entry + queue_t::handle_type mHandle; + // time at which this timer is supposed to fire + LLDate::timestamp mTime; + // interval at which this timer is supposed to fire repeatedly + F32 mInterval{ 0 }; + // mFunc is currently running: don't delete this entry + bool mRunning{ false }; + // cancel() was called while mFunc was running: deferred cancel + bool mCancel{ false }; + }; + + using MetaMap = std::unordered_map<token_t, Metadata>; + MetaMap mMeta; + token_t mToken{ 0 }; + // While mQueue is non-empty, register for regular callbacks. + LLCallbackList::temp_handle_t mLive; + F32 mTimeslice{ DEFAULT_TIMESLICE }; +}; + +} // namespace LL + +/*-------------------- legacy names in global namespace --------------------*/ // Call a given callable once after specified interval. -void doAfterInterval(nullary_func_t callable, F32 seconds); +inline +LL::Timers::handle_t doAfterInterval(nullary_func_t callable, F32 seconds) +{ + return LL::Timers::instance().scheduleAfter(callable, seconds); +} // Call a given callable every specified number of seconds, until it returns true. -void doPeriodically(bool_func_t callable, F32 seconds); - -extern LLCallbackList gIdleCallbacks; +inline +LL::Timers::handle_t doPeriodically(bool_func_t callable, F32 seconds) +{ + return LL::Timers::instance().scheduleEvery(callable, seconds); +} #endif diff --git a/indra/llcommon/llcoromutex.h b/indra/llcommon/llcoromutex.h new file mode 100644 index 0000000000..c0ceac4b22 --- /dev/null +++ b/indra/llcommon/llcoromutex.h @@ -0,0 +1,64 @@ +/** + * @file llcoromutex.h + * @author Nat Goodspeed + * @date 2024-09-04 + * @brief Coroutine-aware synchronization primitives + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLCOROMUTEX_H) +#define LL_LLCOROMUTEX_H + +#include "mutex.h" +#include <boost/fiber/future/promise.hpp> +#include <boost/fiber/future/future.hpp> + +// e.g. #include LLCOROS_MUTEX_HEADER +#define LLCOROS_MUTEX_HEADER <boost/fiber/mutex.hpp> +#define LLCOROS_RMUTEX_HEADER <boost/fiber/recursive_mutex.hpp> +#define LLCOROS_CONDVAR_HEADER <boost/fiber/condition_variable.hpp> + +namespace boost { + namespace fibers { + class mutex; + class recursive_mutex; + enum class cv_status; + class condition_variable; + } +} + +namespace llcoro +{ + +/** + * Aliases for promise and future. An older underlying future implementation + * required us to wrap future; that's no longer needed. However -- if it's + * important to restore kill() functionality, we might need to provide a + * proxy, so continue using the aliases. + */ +template <typename T> +using Promise = boost::fibers::promise<T>; +template <typename T> +using Future = boost::fibers::future<T>; +template <typename T> +inline +static Future<T> getFuture(Promise<T>& promise) { return promise.get_future(); } + +// use mutex, lock, condition_variable suitable for coroutines +using Mutex = boost::fibers::mutex; +using RMutex = boost::fibers::recursive_mutex; +// With C++17, LockType is deprecated: at this point we can directly +// declare 'std::unique_lock lk(some_mutex)' without explicitly stating +// the mutex type. Sadly, making LockType an alias template for +// std::unique_lock doesn't work the same way: Class Template Argument +// Deduction only works for class templates, not alias templates. +using LockType = std::unique_lock<Mutex>; +using cv_status = boost::fibers::cv_status; +using ConditionVariable = boost::fibers::condition_variable; + +} // namespace llcoro + +#endif /* ! defined(LL_LLCOROMUTEX_H) */ diff --git a/indra/llcommon/llcoros.cpp b/indra/llcommon/llcoros.cpp index 1539b48bd3..0b46802295 100644 --- a/indra/llcommon/llcoros.cpp +++ b/indra/llcommon/llcoros.cpp @@ -51,11 +51,12 @@ #endif // other Linden headers #include "llapp.h" -#include "lltimer.h" -#include "llevents.h" #include "llerror.h" -#include "stringize.h" +#include "llevents.h" #include "llexception.h" +#include "llsdutil.h" +#include "lltimer.h" +#include "stringize.h" #if LL_WINDOWS #include <excpt.h> @@ -64,12 +65,8 @@ // static bool LLCoros::on_main_coro() { - if (!LLCoros::instanceExists() || LLCoros::getName().empty()) - { - return true; - } - - return false; + return (!LLCoros::instanceExists() || + LLCoros::getName().empty()); } // static @@ -79,7 +76,7 @@ bool LLCoros::on_main_thread_main_coro() } // static -LLCoros::CoroData& LLCoros::get_CoroData(const std::string& caller) +LLCoros::CoroData& LLCoros::get_CoroData(const std::string&) { CoroData* current{ nullptr }; // be careful about attempted accesses in the final throes of app shutdown @@ -111,7 +108,7 @@ LLCoros::coro::id LLCoros::get_self() //static void LLCoros::set_consuming(bool consuming) { - CoroData& data(get_CoroData("set_consuming()")); + auto& data(get_CoroData("set_consuming()")); // DO NOT call this on the main() coroutine. llassert_always(! data.mName.empty()); data.mConsuming = consuming; @@ -145,6 +142,15 @@ LLCoros::LLCoros(): // points to it. So initialize it with a no-op deleter. mCurrent{ [](CoroData*){} } { + auto& llapp{ LLEventPumps::instance().obtain("LLApp") }; + if (llapp.getListener("LLCoros") == LLBoundListener()) + { + // chain our "LLCoros" pump onto "LLApp" pump: echo events posted to "LLApp" + mConn = llapp.listen( + "LLCoros", + [](const LLSD& event) + { return LLEventPumps::instance().obtain("LLCoros").post(event); }); + } } LLCoros::~LLCoros() @@ -194,26 +200,26 @@ std::string LLCoros::generateDistinctName(const std::string& prefix) const // Until we find an unused name, append a numeric suffix for uniqueness. while (CoroData::getInstance(name)) { - name = STRINGIZE(prefix << unique++); + name = stringize(prefix, unique++); } return name; } -/*==========================================================================*| -bool LLCoros::kill(const std::string& name) +bool LLCoros::killreq(const std::string& name) { - CoroMap::iterator found = mCoros.find(name); - if (found == mCoros.end()) + auto found = CoroData::getInstance(name); + if (! found) { return false; } - // Because this is a boost::ptr_map, erasing the map entry also destroys - // the referenced heap object, in this case the boost::coroutine object, - // which will terminate the coroutine. - mCoros.erase(found); + // Next time the subject coroutine calls checkStop(), make it terminate. + found->mKilledBy = getName(); + // But if it's waiting for something, notify anyone in a position to poke + // it. + LLEventPumps::instance().obtain("LLCoros").post( + llsd::map("status", "killreq", "coro", name)); return true; } -|*==========================================================================*/ //static std::string LLCoros::getName() @@ -224,7 +230,7 @@ std::string LLCoros::getName() //static std::string LLCoros::logname() { - LLCoros::CoroData& data(get_CoroData("logname()")); + auto& data(get_CoroData("logname()")); return data.mName.empty()? data.getKey() : data.mName; } @@ -277,7 +283,7 @@ std::string LLCoros::launch(const std::string& prefix, const callable_t& callabl // std::allocator_arg is a flag to indicate that the following argument is // a StackAllocator. // protected_fixedsize_stack sets a guard page past the end of the new - // stack so that stack underflow will result in an access violation + // stack so that stack overflow will result in an access violation // instead of weird, subtle, possibly undiagnosed memory stomps. try @@ -362,10 +368,12 @@ void LLCoros::toplevel(std::string name, callable_t callable) // set it as current mCurrent.reset(&corodata); + LL_DEBUGS("LLCoros") << "entering " << name << LL_ENDL; // run the code the caller actually wants in the coroutine try { sehandle(callable); + LL_DEBUGS("LLCoros") << "done " << name << LL_ENDL; } catch (const Stop& exc) { @@ -377,7 +385,7 @@ void LLCoros::toplevel(std::string name, callable_t callable) // Any uncaught exception derived from LLContinueError will be caught // here and logged. This coroutine will terminate but the rest of the // viewer will carry on. - LOG_UNHANDLED_EXCEPTION(STRINGIZE("coroutine " << name)); + LOG_UNHANDLED_EXCEPTION("coroutine " + name); } catch (...) { @@ -390,15 +398,24 @@ void LLCoros::toplevel(std::string name, callable_t callable) } //static -void LLCoros::checkStop() +void LLCoros::checkStop(callable_t cleanup) { + // don't replicate this 'if' test throughout the code below + if (! cleanup) + { + cleanup = {[](){}}; // hey, look, I'm coding in Haskell! + } + if (wasDeleted()) { + cleanup(); LLTHROW(Shutdown("LLCoros was deleted")); } - // do this AFTER the check above, because getName() depends on - // get_CoroData(), which depends on the local_ptr in our instance(). - if (getName().empty()) + + // do this AFTER the check above, because get_CoroData() depends on the + // local_ptr in our instance(). + auto& data(get_CoroData("checkStop()")); + if (data.mName.empty()) { // Our Stop exception and its subclasses are intended to stop loitering // coroutines. Don't throw it from the main coroutine. @@ -406,19 +423,80 @@ void LLCoros::checkStop() } if (LLApp::isStopped()) { + cleanup(); LLTHROW(Stopped("viewer is stopped")); } if (! LLApp::isRunning()) { + cleanup(); LLTHROW(Stopping("viewer is stopping")); } + if (! data.mKilledBy.empty()) + { + // Someone wants to kill this coroutine + cleanup(); + LLTHROW(Killed(stringize("coroutine ", data.mName, " killed by ", data.mKilledBy))); + } +} + +LLBoundListener LLCoros::getStopListener(const std::string& caller, LLVoidListener cleanup) +{ + if (! cleanup) + return {}; + + // This overload only responds to viewer shutdown. + return LLEventPumps::instance().obtain("LLCoros") + .listen( + LLEventPump::inventName(caller), + [cleanup](const LLSD& event) + { + auto status{ event["status"].asString() }; + if (status != "running" && status != "killreq") + { + cleanup(event); + } + return false; + }); +} + +LLBoundListener LLCoros::getStopListener(const std::string& caller, + const std::string& cnsmr, + LLVoidListener cleanup) +{ + if (! cleanup) + return {}; + + std::string consumer{cnsmr}; + if (consumer.empty()) + { + consumer = getName(); + } + + // This overload responds to viewer shutdown and to killreq(consumer). + return LLEventPumps::instance().obtain("LLCoros") + .listen( + LLEventPump::inventName(caller), + [consumer, cleanup](const LLSD& event) + { + auto status{ event["status"].asString() }; + if (status == "killreq") + { + if (event["coro"].asString() == consumer) + { + cleanup(event); + } + } + else if (status != "running") + { + cleanup(event); + } + return false; + }); } LLCoros::CoroData::CoroData(const std::string& name): LLInstanceTracker<CoroData, std::string>(name), mName(name), - // don't consume events unless specifically directed - mConsuming(false), mCreationTime(LLTimer::getTotalSeconds()) { } @@ -431,7 +509,6 @@ LLCoros::CoroData::CoroData(int n): // empty string as its visible name because some consumers test for that. LLInstanceTracker<CoroData, std::string>("main" + stringize(n)), mName(), - mConsuming(false), mCreationTime(LLTimer::getTotalSeconds()) { } diff --git a/indra/llcommon/llcoros.h b/indra/llcommon/llcoros.h index c3820ae987..0291d7f1d9 100644 --- a/indra/llcommon/llcoros.h +++ b/indra/llcommon/llcoros.h @@ -29,30 +29,16 @@ #if ! defined(LL_LLCOROS_H) #define LL_LLCOROS_H +#include "llcoromutex.h" +#include "llevents.h" #include "llexception.h" -#include <boost/fiber/fss.hpp> -#include <boost/fiber/future/future.hpp> -#include <boost/fiber/future/promise.hpp> -#include <boost/fiber/recursive_mutex.hpp> -#include "mutex.h" -#include "llsingleton.h" #include "llinstancetracker.h" -#include <boost/function.hpp> -#include <string> +#include "llsingleton.h" +#include <boost/fiber/fss.hpp> #include <exception> +#include <functional> #include <queue> - -// e.g. #include LLCOROS_MUTEX_HEADER -#define LLCOROS_MUTEX_HEADER <boost/fiber/mutex.hpp> -#define LLCOROS_CONDVAR_HEADER <boost/fiber/condition_variable.hpp> - -namespace boost { - namespace fibers { - class mutex; - enum class cv_status; - class condition_variable; - } -} +#include <string> /** * Registry of named Boost.Coroutine instances @@ -112,7 +98,7 @@ public: /// stuck with the term "coroutine." typedef boost::fibers::fiber coro; /// Canonical callable type - typedef boost::function<void()> callable_t; + typedef std::function<void()> callable_t; /** * Create and start running a new coroutine with specified name. The name @@ -154,13 +140,13 @@ public: std::string launch(const std::string& prefix, const callable_t& callable); /** - * Abort a running coroutine by name. Normally, when a coroutine either + * Ask the named coroutine to abort. Normally, when a coroutine either * runs to completion or terminates with an exception, LLCoros quietly * cleans it up. This is for use only when you must explicitly interrupt * one prematurely. Returns @c true if the specified name was found and * still running at the time. */ -// bool kill(const std::string& name); + bool killreq(const std::string& name); /** * From within a coroutine, look up the (tweaked) name string by which @@ -262,15 +248,21 @@ public: /// thrown by checkStop() // It may sound ironic that Stop is derived from LLContinueError, but the // point is that LLContinueError is the category of exception that should - // not immediately crash the viewer. Stop and its subclasses are to notify - // coroutines that the viewer intends to shut down. The expected response - // is to terminate the coroutine, rather than abort the viewer. + // not immediately crash the viewer. Stop and its subclasses are to tell + // coroutines to terminate, e.g. because the viewer is shutting down. We + // do not want any such exception to crash the viewer. struct Stop: public LLContinueError { Stop(const std::string& what): LLContinueError(what) {} }; - /// early stages + /// someone wants to kill this specific coroutine + struct Killed: public Stop + { + Killed(const std::string& what): Stop(what) {} + }; + + /// early shutdown stages struct Stopping: public Stop { Stopping(const std::string& what): Stop(what) {} @@ -289,34 +281,50 @@ public: }; /// Call this intermittently if there's a chance your coroutine might - /// continue running into application shutdown. Throws Stop if LLCoros has - /// been cleaned up. - static void checkStop(); + /// still be running at application shutdown. Throws one of the Stop + /// subclasses if the caller needs to terminate. Pass a cleanup function + /// if you need to execute that cleanup before terminating. + /// Of course, if your cleanup function throws, that will be the exception + /// propagated by checkStop(). + static void checkStop(callable_t cleanup={}); + + /// Call getStopListener() at the source end of a queue, promise or other + /// resource on which coroutines will wait, so that shutdown can wake up + /// consuming coroutines. @a caller should distinguish who's calling. The + /// passed @a cleanup function must close the queue, break the promise or + /// otherwise cause waiting consumers to wake up in an abnormal way. It's + /// advisable to store the returned LLBoundListener in an + /// LLTempBoundListener, or otherwise arrange to disconnect it. + static LLBoundListener getStopListener(const std::string& caller, LLVoidListener cleanup); + + /// This getStopListener() overload is like the two-argument one, for use + /// when we know the name of the only coroutine that will wait on the + /// resource in question. Pass @a consumer as the empty string if the + /// consumer coroutine is the same as the calling coroutine. Unlike the + /// two-argument getStopListener(), this one also responds to + /// killreq(target). + static LLBoundListener getStopListener(const std::string& caller, + const std::string& consumer, + LLVoidListener cleanup); /** - * Aliases for promise and future. An older underlying future implementation - * required us to wrap future; that's no longer needed. However -- if it's - * important to restore kill() functionality, we might need to provide a - * proxy, so continue using the aliases. + * LLCoros aliases for promise and future, for backwards compatibility. + * These have been hoisted out to the llcoro namespace. */ template <typename T> - using Promise = boost::fibers::promise<T>; + using Promise = llcoro::Promise<T>; template <typename T> - using Future = boost::fibers::future<T>; + using Future = llcoro::Future<T>; template <typename T> static Future<T> getFuture(Promise<T>& promise) { return promise.get_future(); } // use mutex, lock, condition_variable suitable for coroutines - using Mutex = boost::fibers::mutex; - using RMutex = boost::fibers::recursive_mutex; - // With C++17, LockType is deprecated: at this point we can directly - // declare 'std::unique_lock lk(some_mutex)' without explicitly stating - // the mutex type. Sadly, making LockType an alias template for - // std::unique_lock doesn't work the same way: Class Template Argument - // Deduction only works for class templates, not alias templates. - using LockType = std::unique_lock<Mutex>; - using cv_status = boost::fibers::cv_status; - using ConditionVariable = boost::fibers::condition_variable; + using Mutex = llcoro::Mutex; + using RMutex = llcoro::RMutex; + // LockType is deprecated; see llcoromutex.h + using LockType = llcoro::LockType; + using cv_status = llcoro::cv_status; + using ConditionVariable = llcoro::ConditionVariable; /// for data local to each running coroutine template <typename T> @@ -329,6 +337,8 @@ private: static CoroData& get_CoroData(const std::string& caller); void saveException(const std::string& name, std::exception_ptr exc); + LLTempBoundListener mConn; + struct ExceptionData { ExceptionData(const std::string& nm, std::exception_ptr exc): @@ -352,8 +362,10 @@ private: // tweaked name of the current coroutine const std::string mName; - // set_consuming() state - bool mConsuming; + // set_consuming() state -- don't consume events unless specifically directed + bool mConsuming{ false }; + // killed by which coroutine + std::string mKilledBy; // setStatus() state std::string mStatus; F64 mCreationTime; // since epoch diff --git a/indra/llcommon/lldate.cpp b/indra/llcommon/lldate.cpp index b38864688d..5f51f40232 100644 --- a/indra/llcommon/lldate.cpp +++ b/indra/llcommon/lldate.cpp @@ -41,7 +41,9 @@ #include "llstring.h" #include "llfasttimer.h" -static const F64 LL_APR_USEC_PER_SEC = 1000000.0; +static const LLDate::timestamp DATE_EPOCH = 0.0; + +static const LLDate::timestamp LL_APR_USEC_PER_SEC = 1000000.0; // should be APR_USEC_PER_SEC, but that relies on INT64_C which // isn't defined in glib under our build set up for some reason @@ -224,13 +226,13 @@ bool LLDate::fromStream(std::istream& s) return false; } - F64 seconds_since_epoch = time / LL_APR_USEC_PER_SEC; + timestamp seconds_since_epoch = time / LL_APR_USEC_PER_SEC; // check for fractional c = s.peek(); if(c == '.') { - F64 fractional = 0.0; + timestamp fractional = 0.0; s >> fractional; seconds_since_epoch += fractional; } @@ -290,12 +292,12 @@ bool LLDate::fromYMDHMS(S32 year, S32 month, S32 day, S32 hour, S32 min, S32 sec return true; } -F64 LLDate::secondsSinceEpoch() const +LLDate::timestamp LLDate::secondsSinceEpoch() const { return mSecondsSinceEpoch; } -void LLDate::secondsSinceEpoch(F64 seconds) +void LLDate::secondsSinceEpoch(timestamp seconds) { mSecondsSinceEpoch = seconds; } diff --git a/indra/llcommon/lldate.h b/indra/llcommon/lldate.h index 1a69a04232..0afe0b0599 100644 --- a/indra/llcommon/lldate.h +++ b/indra/llcommon/lldate.h @@ -45,6 +45,8 @@ class LL_COMMON_API LLDate { static constexpr F64 DATE_EPOCH = 0.0; public: + using timestamp = F64; + /** * @brief Construct a date equal to epoch. */ @@ -100,14 +102,14 @@ public: * * @return The number of seconds since epoch UTC. */ - F64 secondsSinceEpoch() const; + timestamp secondsSinceEpoch() const; /** * @brief Set the date in seconds since epoch. * * @param seconds The number of seconds since epoch UTC. */ - void secondsSinceEpoch(F64 seconds); + void secondsSinceEpoch(timestamp seconds); /** * @brief Create an LLDate object set to the current time. @@ -144,7 +146,7 @@ public: private: - F64 mSecondsSinceEpoch; + timestamp mSecondsSinceEpoch; }; // Helper function to stream out a date diff --git a/indra/llcommon/lldefs.h b/indra/llcommon/lldefs.h index 2fbb26dc1a..d4b063f88c 100644 --- a/indra/llcommon/lldefs.h +++ b/indra/llcommon/lldefs.h @@ -28,6 +28,7 @@ #define LL_LLDEFS_H #include "stdtypes.h" +#include <cassert> #include <type_traits> // Often used array indices @@ -169,6 +170,38 @@ constexpr U32 MAXADDRSTR = 17; // 123.567.901.345 = 15 chars + \0 + // llclampb(a) // clamps a to [0 .. 255] // +// llless(d0, d1) safely compares d0 < d1 even if one is signed and the other +// is unsigned. A simple (d0 < d1) expression converts the signed operand to +// unsigned before comparing. If the signed operand is negative, that flips +// the negative value to a huge positive value, producing the wrong answer! +// llless() specifically addresses that case. +template <typename T0, typename T1> +inline bool llless(T0 d0, T1 d1) +{ + if constexpr (std::is_signed_v<T0> && ! std::is_signed_v<T1>) + { + // T0 signed, T1 unsigned: negative d0 is less than any unsigned d1 + if (d0 < 0) + return true; + // both are non-negative: explicitly cast to avoid C4018 + return std::make_unsigned_t<T0>(d0) < d1; + } + else if constexpr (! std::is_signed_v<T0> && std::is_signed_v<T1>) + { + // T0 unsigned, T1 signed: any unsigned d0 is greater than negative d1 + if (d1 < 0) + return false; + // both are non-negative: explicitly cast to avoid C4018 + return d0 < std::make_unsigned_t<T1>(d1); + } + else + { + // both T0 and T1 are signed, or both are unsigned: + // straightforward comparison works + return d0 < d1; + } +} + // recursion tail template <typename T> inline auto llmax(T data) @@ -180,7 +213,7 @@ template <typename T0, typename T1, typename... Ts> inline auto llmax(T0 d0, T1 d1, Ts... rest) { auto maxrest = llmax(d1, rest...); - return (d0 > maxrest)? d0 : maxrest; + return llless(maxrest, d0)? d0 : maxrest; } // recursion tail @@ -194,12 +227,28 @@ template <typename T0, typename T1, typename... Ts> inline auto llmin(T0 d0, T1 d1, Ts... rest) { auto minrest = llmin(d1, rest...); - return (d0 < minrest) ? d0 : minrest; + return llless(d0, minrest) ? d0 : minrest; } template <typename A, typename MIN, typename MAX> inline A llclamp(A a, MIN minval, MAX maxval) { + // The only troublesome case is if A is unsigned and either minval or + // maxval is both signed and negative. Casting a negative number to + // unsigned flips it to a huge positive number, making this llclamp() call + // ineffective. + if constexpr (! std::is_signed_v<A>) + { + if constexpr (std::is_signed_v<MIN>) + { + assert(minval >= 0); + } + if constexpr (std::is_signed_v<MAX>) + { + assert(maxval >= 0); + } + } + A aminval{ static_cast<A>(minval) }, amaxval{ static_cast<A>(maxval) }; if ( a < aminval ) { diff --git a/indra/llcommon/lldependencies.h b/indra/llcommon/lldependencies.h index 47b6fedc7d..d19cdd1f25 100644 --- a/indra/llcommon/lldependencies.h +++ b/indra/llcommon/lldependencies.h @@ -30,6 +30,8 @@ #if ! defined(LL_LLDEPENDENCIES_H) #define LL_LLDEPENDENCIES_H +#include "linden_common.h" +#include "llexception.h" #include <string> #include <vector> #include <set> @@ -40,7 +42,6 @@ #include <boost/range/iterator_range.hpp> #include <boost/function.hpp> #include <boost/bind.hpp> -#include "llexception.h" /***************************************************************************** * Utilities diff --git a/indra/llcommon/llerror.cpp b/indra/llcommon/llerror.cpp index ad35bc84f2..e6fe1eca75 100644 --- a/indra/llcommon/llerror.cpp +++ b/indra/llcommon/llerror.cpp @@ -64,6 +64,8 @@ #define BOOST_STACKTRACE_GNU_SOURCE_NOT_REQUIRED #include <boost/stacktrace.hpp> +#include LLCOROS_RMUTEX_HEADER + namespace { #if LL_WINDOWS void debugger_print(const std::string& s) @@ -1431,6 +1433,7 @@ namespace LLError if (site.mLevel == LEVEL_ERROR) { + writeToRecorders(site, stringize(boost::stacktrace::stacktrace())); g->mFatalMessage = message; if (s->mCrashFunction) { diff --git a/indra/llcommon/lleventapi.cpp b/indra/llcommon/lleventapi.cpp index 8b724256b8..4672371b4f 100644 --- a/indra/llcommon/lleventapi.cpp +++ b/indra/llcommon/lleventapi.cpp @@ -55,6 +55,26 @@ LLEventAPI::~LLEventAPI() { } +bool LLEventAPI::process(const LLSD& event) const +{ + // LLDispatchListener is documented to let DispatchError propagate if the + // incoming request has no "reply" key. That may be fine for internal-only + // use, but LLEventAPI opens the door for external requests. It should NOT + // be possible for any external requester to crash the viewer with an + // unhandled exception, especially not by something as simple as omitting + // the "reply" key. + try + { + return LLDispatchListener::process(event); + } + catch (const std::exception& err) + { + // log the exception, but otherwise ignore it + LL_WARNS("LLEventAPI") << LLError::Log::classname(err) << ": " << err.what() << LL_ENDL; + return false; + } +} + 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 3ae820db51..da7c58e6f0 100644 --- a/indra/llcommon/lleventapi.h +++ b/indra/llcommon/lleventapi.h @@ -157,6 +157,7 @@ protected: LLEventAPI(const LL::LazyEventAPIParams&); private: + bool process(const LLSD& event) const override; std::string mDesc; }; diff --git a/indra/llcommon/lleventcoro.cpp b/indra/llcommon/lleventcoro.cpp index e1fc4764f6..33db27a116 100644 --- a/indra/llcommon/lleventcoro.cpp +++ b/indra/llcommon/lleventcoro.cpp @@ -119,7 +119,7 @@ void llcoro::suspendUntilTimeout(float seconds) // We used to call boost::this_fiber::sleep_for(). But some coroutines // (e.g. LLExperienceCache::idleCoro()) sit in a suspendUntilTimeout() // loop, in which case a sleep_for() call risks sleeping through shutdown. - // So instead, listen for "LLApp" state-changing events -- which + // So instead, listen for LLApp state-changing events -- which // fortunately is handled for us by suspendUntilEventOnWithTimeout(). // Wait for an event on a bogus LLEventPump on which nobody ever posts // events. Don't make it static because that would force instantiation of @@ -132,8 +132,8 @@ void llcoro::suspendUntilTimeout(float seconds) // Timeout is the NORMAL case for this call! static LLSD timedout; // Deliver, but ignore, timedout when (as usual) we did not receive any - // "LLApp" event. The point is that suspendUntilEventOnWithTimeout() will - // itself throw Stopping when "LLApp" starts broadcasting shutdown events. + // LLApp event. The point is that suspendUntilEventOnWithTimeout() will + // itself throw Stopping when LLApp starts broadcasting shutdown events. suspendUntilEventOnWithTimeout(bogus, seconds, timedout); } @@ -167,33 +167,26 @@ postAndSuspendSetup(const std::string& callerName, // "LLApp" were an LLEventMailDrop. But if we ever go there, we'd want to // notice the pending LLApp status first. LLBoundListener stopper( - LLEventPumps::instance().obtain("LLApp").listen( + LLCoros::getStopListener( listenerName, + LLCoros::instance().getName(), [&promise, listenerName](const LLSD& status) { - // anything except "running" should wake up the waiting - // coroutine - auto& statsd = status["status"]; - if (statsd.asString() != "running") + LL_DEBUGS("lleventcoro") << listenerName + << " spotted status " << status + << ", throwing Stopping" << LL_ENDL; + try + { + promise.set_exception( + std::make_exception_ptr( + LLCoros::Stopping("status " + stringize(status)))); + } + catch (const boost::fibers::promise_already_satisfied&) { - LL_DEBUGS("lleventcoro") << listenerName - << " spotted status " << statsd - << ", throwing Stopping" << LL_ENDL; - try - { - promise.set_exception( - std::make_exception_ptr( - LLCoros::Stopping("status " + statsd.asString()))); - } - catch (const boost::fibers::promise_already_satisfied&) - { - LL_WARNS("lleventcoro") << listenerName - << " couldn't throw Stopping " - "because promise already set" << LL_ENDL; - } + LL_WARNS("lleventcoro") << listenerName + << " couldn't throw Stopping " + "because promise already set" << LL_ENDL; } - // do not consume -- every listener must see status - return false; })); LLBoundListener connection( replyPump.listen( diff --git a/indra/llcommon/lleventdispatcher.cpp b/indra/llcommon/lleventdispatcher.cpp index 9dd6864cff..12b966fadf 100644 --- a/indra/llcommon/lleventdispatcher.cpp +++ b/indra/llcommon/lleventdispatcher.cpp @@ -800,7 +800,7 @@ void LLDispatchListener::call_one(const LLSD& name, const LLSD& event) const { result = (*this)(event); } - catch (const DispatchError& err) + catch (const std::exception& err) { if (! event.has(mReplyKey)) { @@ -810,7 +810,10 @@ void LLDispatchListener::call_one(const LLSD& name, const LLSD& event) const // 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); + return reply(llsd::map("error", + stringize(LLError::Log::classname(err), + ": ", err.what())), + event); } // We seem to have gotten a valid result. But we don't know whether the @@ -846,7 +849,7 @@ void LLDispatchListener::call_map(const LLSD& reqmap, const LLSD& event) const { // 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, "]]"); + SetState transient(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. @@ -869,7 +872,7 @@ void LLDispatchListener::call_map(const LLSD& reqmap, const LLSD& event) const if (! event.has(mReplyKey)) { // can't send reply, throw - sCallFail<DispatchError>(error); + callFail<DispatchError>(error); } else { @@ -923,7 +926,7 @@ void LLDispatchListener::call_array(const LLSD& reqarray, const LLSD& event) con // 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, ']'); + SetState transient(this, '[', key, '[', i, "]=", name, ']'); // With this form, capture return value even if undefined results.append((*this)(name, args)); } @@ -944,7 +947,7 @@ void LLDispatchListener::call_array(const LLSD& reqarray, const LLSD& event) con if (! event.has(mReplyKey)) { // can't send reply, throw - sCallFail<DispatchError>(error); + callFail<DispatchError>(error); } else { diff --git a/indra/llcommon/lleventdispatcher.h b/indra/llcommon/lleventdispatcher.h index 4c3c0f3414..6b5524e1eb 100644 --- a/indra/llcommon/lleventdispatcher.h +++ b/indra/llcommon/lleventdispatcher.h @@ -35,7 +35,6 @@ #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/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 @@ -48,6 +47,7 @@ #include "llevents.h" #include "llptrto.h" #include "llsdutil.h" +#include "stringize.h" class LLSD; @@ -99,7 +99,7 @@ public: template <typename CALLABLE, typename=typename std::enable_if< - boost::hof::is_invocable<CALLABLE, LLSD>::value + std::is_invocable<CALLABLE, LLSD>::value >::type> void add(const std::string& name, const std::string& desc, @@ -296,7 +296,7 @@ public: */ template <typename CALLABLE, typename=typename std::enable_if< - ! boost::hof::is_invocable<CALLABLE, LLSD>() + ! std::is_invocable<CALLABLE, LLSD>() >::type> void add(const std::string& name, const std::string& desc, @@ -338,7 +338,7 @@ public: template<typename Function, typename = typename std::enable_if< boost::function_types::is_nonmember_callable_builtin<Function>::value && - ! boost::hof::is_invocable<Function, LLSD>::value + ! std::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()); @@ -851,8 +851,12 @@ public: ARGS&&... args); virtual ~LLDispatchListener() {} + std::string getPumpName() const { return getName(); } + +protected: + virtual bool process(const LLSD& event) const; + private: - 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; diff --git a/indra/llcommon/lleventfilter.cpp b/indra/llcommon/lleventfilter.cpp index 604ee8a42d..2b5401e9f7 100644 --- a/indra/llcommon/lleventfilter.cpp +++ b/indra/llcommon/lleventfilter.cpp @@ -33,20 +33,19 @@ // STL headers // std headers // external library headers -#include <boost/bind.hpp> // other Linden headers +#include "lldate.h" #include "llerror.h" // LL_ERRS +#include "lleventtimer.h" #include "llsdutil.h" // llsd_matches() #include "stringize.h" -#include "lleventtimer.h" -#include "lldate.h" /***************************************************************************** * LLEventFilter *****************************************************************************/ LLEventFilter::LLEventFilter(LLEventPump& source, const std::string& name, bool tweak): LLEventStream(name, tweak), - mSource(source.listen(getName(), boost::bind(&LLEventFilter::post, this, _1))) + mSource(source.listen(getName(), [this](const LLSD& event){ return post(event); })) { } @@ -74,136 +73,52 @@ bool LLEventMatching::post(const LLSD& event) } /***************************************************************************** -* LLEventTimeoutBase +* LLEventTimeout *****************************************************************************/ -LLEventTimeoutBase::LLEventTimeoutBase(): +LLEventTimeout::LLEventTimeout(): LLEventFilter("timeout") { } -LLEventTimeoutBase::LLEventTimeoutBase(LLEventPump& source): +LLEventTimeout::LLEventTimeout(LLEventPump& source): LLEventFilter(source, "timeout") { } -void LLEventTimeoutBase::actionAfter(F32 seconds, const Action& action) +void LLEventTimeout::actionAfter(F32 seconds, const Action& action) { - setCountdown(seconds); - mAction = action; - if (! mMainloop.connected()) - { - LLEventPump& mainloop(LLEventPumps::instance().obtain("mainloop")); - mMainloop = mainloop.listen(getName(), boost::bind(&LLEventTimeoutBase::tick, this, _1)); - } + mTimer = LL::Timers::instance().scheduleAfter(action, seconds); } -class ErrorAfter +void LLEventTimeout::errorAfter(F32 seconds, const std::string& message) { -public: - ErrorAfter(const std::string& message): mMessage(message) {} - - void operator()() - { - LL_ERRS("LLEventTimeout") << mMessage << LL_ENDL; - } - -private: - std::string mMessage; -}; - -void LLEventTimeoutBase::errorAfter(F32 seconds, const std::string& message) -{ - actionAfter(seconds, ErrorAfter(message)); + actionAfter( + seconds, + [message=message] + { + LL_ERRS("LLEventTimeout") << message << LL_ENDL; + }); } -class EventAfter +void LLEventTimeout::eventAfter(F32 seconds, const LLSD& event) { -public: - EventAfter(LLEventPump& pump, const LLSD& event): - mPump(pump), - mEvent(event) - {} - - void operator()() - { - mPump.post(mEvent); - } - -private: - LLEventPump& mPump; - LLSD mEvent; -}; - -void LLEventTimeoutBase::eventAfter(F32 seconds, const LLSD& event) -{ - actionAfter(seconds, EventAfter(*this, event)); + actionAfter(seconds, [this, event]{ post(event); }); } -bool LLEventTimeoutBase::post(const LLSD& event) +bool LLEventTimeout::post(const LLSD& event) { cancel(); return LLEventStream::post(event); } -void LLEventTimeoutBase::cancel() +void LLEventTimeout::cancel() { - mMainloop.disconnect(); + mTimer.cancel(); } -bool LLEventTimeoutBase::tick(const LLSD&) +bool LLEventTimeout::running() const { - if (countdownElapsed()) - { - cancel(); - mAction(); - } - return false; // show event to other listeners -} - -bool LLEventTimeoutBase::running() const -{ - return mMainloop.connected(); -} - -/***************************************************************************** -* LLEventTimeout -*****************************************************************************/ -LLEventTimeout::LLEventTimeout() {} - -LLEventTimeout::LLEventTimeout(LLEventPump& source): - LLEventTimeoutBase(source) -{ -} - -void LLEventTimeout::setCountdown(F32 seconds) -{ - mTimer.setTimerExpirySec(seconds); -} - -bool LLEventTimeout::countdownElapsed() const -{ - return mTimer.hasExpired(); -} - -LLEventTimer* LLEventTimeout::post_every(F32 period, const std::string& pump, const LLSD& data) -{ - return LLEventTimer::run_every( - period, - [pump, data](){ LLEventPumps::instance().obtain(pump).post(data); }); -} - -LLEventTimer* LLEventTimeout::post_at(const LLDate& time, const std::string& pump, const LLSD& data) -{ - return LLEventTimer::run_at( - time, - [pump, data](){ LLEventPumps::instance().obtain(pump).post(data); }); -} - -LLEventTimer* LLEventTimeout::post_after(F32 interval, const std::string& pump, const LLSD& data) -{ - return LLEventTimer::run_after( - interval, - [pump, data](){ LLEventPumps::instance().obtain(pump).post(data); }); + return LL::Timers::instance().isRunning(mTimer); } /***************************************************************************** @@ -246,21 +161,21 @@ void LLEventBatch::setSize(std::size_t size) } /***************************************************************************** -* LLEventThrottleBase +* LLEventThrottle *****************************************************************************/ -LLEventThrottleBase::LLEventThrottleBase(F32 interval): +LLEventThrottle::LLEventThrottle(F32 interval): LLEventFilter("throttle"), mInterval(interval), mPosts(0) {} -LLEventThrottleBase::LLEventThrottleBase(LLEventPump& source, F32 interval): +LLEventThrottle::LLEventThrottle(LLEventPump& source, F32 interval): LLEventFilter(source, "throttle"), mInterval(interval), mPosts(0) {} -void LLEventThrottleBase::flush() +void LLEventThrottle::flush() { // flush() is a no-op unless there's something pending. // Don't test mPending because there's no requirement that the consumer @@ -281,12 +196,12 @@ void LLEventThrottleBase::flush() } } -LLSD LLEventThrottleBase::pending() const +LLSD LLEventThrottle::pending() const { return mPending; } -bool LLEventThrottleBase::post(const LLSD& event) +bool LLEventThrottle::post(const LLSD& event) { // Always capture most recent post() event data. If caller wants to // aggregate multiple events, let them retrieve pending() and modify @@ -311,13 +226,13 @@ bool LLEventThrottleBase::post(const LLSD& event) // timeRemaining tells us how much longer it will be until // mInterval seconds since the last flush() call. At that time, // flush() deferred events. - alarmActionAfter(timeRemaining, boost::bind(&LLEventThrottleBase::flush, this)); + alarmActionAfter(timeRemaining, [this]{ flush(); }); } } return false; } -void LLEventThrottleBase::setInterval(F32 interval) +void LLEventThrottle::setInterval(F32 interval) { F32 oldInterval = mInterval; mInterval = interval; @@ -349,41 +264,30 @@ void LLEventThrottleBase::setInterval(F32 interval) // and if mAlarm is running, reset that too if (alarmRunning()) { - alarmActionAfter(timeRemaining, boost::bind(&LLEventThrottleBase::flush, this)); + alarmActionAfter(timeRemaining, [this](){ flush(); }); } } } } -F32 LLEventThrottleBase::getDelay() const +F32 LLEventThrottle::getDelay() const { return timerGetRemaining(); } -/***************************************************************************** -* LLEventThrottle implementation -*****************************************************************************/ -LLEventThrottle::LLEventThrottle(F32 interval): - LLEventThrottleBase(interval) -{} - -LLEventThrottle::LLEventThrottle(LLEventPump& source, F32 interval): - LLEventThrottleBase(source, interval) -{} - -void LLEventThrottle::alarmActionAfter(F32 interval, const LLEventTimeoutBase::Action& action) +void LLEventThrottle::alarmActionAfter(F32 interval, const LLEventTimeout::Action& action) { - mAlarm.actionAfter(interval, action); + mAlarm = LL::Timers::instance().scheduleAfter(action, interval); } bool LLEventThrottle::alarmRunning() const { - return mAlarm.running(); + return LL::Timers::instance().isRunning(mAlarm); } void LLEventThrottle::alarmCancel() { - return mAlarm.cancel(); + LL::Timers::instance().cancel(mAlarm); } void LLEventThrottle::timerSet(F32 interval) @@ -461,21 +365,22 @@ bool LLEventLogProxy::post(const LLSD& event) /* override */ } LLBoundListener LLEventLogProxy::listen_impl(const std::string& name, - const LLEventListener& target, + const LLAwareListener& target, const NameList& after, const NameList& before) { LL_DEBUGS("LogProxy") << "LLEventLogProxy('" << getName() << "').listen('" << name << "')" << LL_ENDL; return mPump.listen(name, - [this, name, target](const LLSD& event)->bool - { return listener(name, target, event); }, + [this, name, target](const LLBoundListener& conn, const LLSD& event) + { return listener(conn, name, target, event); }, after, before); } -bool LLEventLogProxy::listener(const std::string& name, - const LLEventListener& target, +bool LLEventLogProxy::listener(const LLBoundListener& conn, + const std::string& name, + const LLAwareListener& target, const LLSD& event) const { auto eventminus = event; @@ -487,7 +392,7 @@ bool LLEventLogProxy::listener(const std::string& name, } std::string hdr{STRINGIZE(getName() << " to " << name << " " << counter)}; LL_INFOS("LogProxy") << hdr << ": " << eventminus << LL_ENDL; - bool result = target(eventminus); + bool result = target(conn, eventminus); LL_INFOS("LogProxy") << hdr << " => " << result << LL_ENDL; return result; } diff --git a/indra/llcommon/lleventfilter.h b/indra/llcommon/lleventfilter.h index d8c7e15a27..09ef81a6f5 100644 --- a/indra/llcommon/lleventfilter.h +++ b/indra/llcommon/lleventfilter.h @@ -29,13 +29,13 @@ #if ! defined(LL_LLEVENTFILTER_H) #define LL_LLEVENTFILTER_H +#include "llcallbacklist.h" #include "llevents.h" -#include "stdtypes.h" -#include "lltimer.h" #include "llsdutil.h" -#include <boost/function.hpp> +#include "lltimer.h" +#include "stdtypes.h" +#include <functional> -class LLEventTimer; class LLDate; /** @@ -78,22 +78,27 @@ private: /** * Wait for an event to be posted. If no such event arrives within a specified - * time, take a specified action. See LLEventTimeout for production - * implementation. + * time, take a specified action. * - * @NOTE This is an abstract base class so that, for testing, we can use an - * alternate "timer" that doesn't actually consume real time. + * @NOTE: Caution should be taken when using the LLEventTimeout(LLEventPump &) + * constructor to ensure that the upstream event pump is not an LLEventMaildrop + * or any other kind of store and forward pump which may have events outstanding. + * Using this constructor will cause the upstream event pump to fire any pending + * events and could result in the invocation of a virtual method before the timeout + * has been fully constructed. The timeout should instead be constructed separately + * from the event pump and attached using the listen method. + * See llcoro::suspendUntilEventOnWithTimeout() for an example. */ -class LL_COMMON_API LLEventTimeoutBase: public LLEventFilter +class LL_COMMON_API LLEventTimeout: public LLEventFilter { public: /// construct standalone - LLEventTimeoutBase(); + LLEventTimeout(); /// construct and connect - LLEventTimeoutBase(LLEventPump& source); + LLEventTimeout(LLEventPump& source); /// Callable, can be constructed with boost::bind() - typedef boost::function<void()> Action; + typedef std::function<void()> Action; /** * Start countdown timer for the specified number of @a seconds. Forward @@ -120,8 +125,8 @@ public: * @endcode * * @NOTE - * The implementation relies on frequent events on the LLEventPump named - * "mainloop". + * The implementation relies on frequent calls to + * gIdleCallbacks.callFunctions(). */ void actionAfter(F32 seconds, const Action& action); @@ -134,7 +139,7 @@ public: * Instantiate an LLEventTimeout listening to that API and call * errorAfter() on each async request with a timeout comfortably longer * than the API's time guarantee (much longer than the anticipated - * "mainloop" granularity). + * gIdleCallbacks.callFunctions() granularity). * * Then if the async API breaks its promise, the program terminates with * the specified LL_ERRS @a message. The client of the async API can @@ -184,55 +189,9 @@ public: /// Is this timer currently running? bool running() const; -protected: - virtual void setCountdown(F32 seconds) = 0; - virtual bool countdownElapsed() const = 0; - private: - bool tick(const LLSD&); - - LLTempBoundListener mMainloop; - Action mAction; -}; - -/** - * Production implementation of LLEventTimoutBase. - * - * @NOTE: Caution should be taken when using the LLEventTimeout(LLEventPump &) - * constructor to ensure that the upstream event pump is not an LLEventMaildrop - * or any other kind of store and forward pump which may have events outstanding. - * Using this constructor will cause the upstream event pump to fire any pending - * events and could result in the invocation of a virtual method before the timeout - * has been fully constructed. The timeout should instead be connected upstream - * from the event pump and attached using the listen method. - * See llcoro::suspendUntilEventOnWithTimeout() for an example. - */ - -class LL_COMMON_API LLEventTimeout: public LLEventTimeoutBase -{ -public: - LLEventTimeout(); - LLEventTimeout(LLEventPump& source); - - /// using LLEventTimeout as namespace for free functions - /// Post event to specified LLEventPump every period seconds. Delete - /// returned LLEventTimer* to cancel. - static LLEventTimer* post_every(F32 period, const std::string& pump, const LLSD& data); - /// Post event to specified LLEventPump at specified future time. Call - /// LLEventTimer::getInstance(returned pointer) to check whether it's still - /// pending; if so, delete the pointer to cancel. - static LLEventTimer* post_at(const LLDate& time, const std::string& pump, const LLSD& data); - /// Post event to specified LLEventPump after specified interval. Call - /// LLEventTimer::getInstance(returned pointer) to check whether it's still - /// pending; if so, delete the pointer to cancel. - static LLEventTimer* post_after(F32 interval, const std::string& pump, const LLSD& data); - -protected: - virtual void setCountdown(F32 seconds); - virtual bool countdownElapsed() const; - -private: - LLTimer mTimer; + // Use a temp_handle_t so it's canceled on destruction. + LL::Timers::temp_handle_t mTimer; }; /** @@ -264,7 +223,7 @@ private: }; /** - * LLEventThrottleBase: construct with a time interval. Regardless of how + * LLEventThrottle: construct with a time interval. Regardless of how * frequently you call post(), LLEventThrottle will pass on an event to * its listeners no more often than once per specified interval. * @@ -297,13 +256,13 @@ private: * alternate "timer" that doesn't actually consume real time. See * LLEventThrottle. */ -class LL_COMMON_API LLEventThrottleBase: public LLEventFilter +class LL_COMMON_API LLEventThrottle: public LLEventFilter { public: // pass time interval - LLEventThrottleBase(F32 interval); + LLEventThrottle(F32 interval); // construct and connect - LLEventThrottleBase(LLEventPump& source, F32 interval); + LLEventThrottle(LLEventPump& source, F32 interval); // force out any deferred events void flush(); @@ -324,45 +283,24 @@ public: // time until next event would be passed through, 0.0 if now F32 getDelay() const; -protected: - // Implement these time-related methods for a valid LLEventThrottleBase - // subclass (see LLEventThrottle). For testing, we use a subclass that - // doesn't involve actual elapsed time. - virtual void alarmActionAfter(F32 interval, const LLEventTimeoutBase::Action& action) = 0; - virtual bool alarmRunning() const = 0; - virtual void alarmCancel() = 0; - virtual void timerSet(F32 interval) = 0; - virtual F32 timerGetRemaining() const = 0; - private: - // remember throttle interval - F32 mInterval; - // count post() calls since last flush() - std::size_t mPosts; + void alarmActionAfter(F32 interval, const LLEventTimeout::Action& action); + bool alarmRunning() const; + void alarmCancel(); + void timerSet(F32 interval); + F32 timerGetRemaining() const; + // pending event data from most recent deferred event LLSD mPending; -}; - -/** - * Production implementation of LLEventThrottle. - */ -class LLEventThrottle: public LLEventThrottleBase -{ -public: - LLEventThrottle(F32 interval); - LLEventThrottle(LLEventPump& source, F32 interval); - -private: - virtual void alarmActionAfter(F32 interval, const LLEventTimeoutBase::Action& action) /*override*/; - virtual bool alarmRunning() const /*override*/; - virtual void alarmCancel() /*override*/; - virtual void timerSet(F32 interval) /*override*/; - virtual F32 timerGetRemaining() const /*override*/; - - // use this to arrange a deferred flush() call - LLEventTimeout mAlarm; // use this to track whether we're within mInterval of last flush() LLTimer mTimer; + // count post() calls since last flush() + std::size_t mPosts; + // remember throttle interval + F32 mInterval; + + // use this to arrange a deferred flush() call + LL::Timers::handle_t mAlarm; }; /** @@ -521,7 +459,7 @@ public: LLEventLogProxy(LLEventPump& source, const std::string& name, bool tweak=false); /// register a new listener - LLBoundListener listen_impl(const std::string& name, const LLEventListener& target, + LLBoundListener listen_impl(const std::string& name, const LLAwareListener& target, const NameList& after, const NameList& before); /// Post an event to all listeners @@ -531,8 +469,9 @@ private: /// This method intercepts each call to any target listener. We pass it /// the listener name and the caller's intended target listener plus the /// posted LLSD event. - bool listener(const std::string& name, - const LLEventListener& target, + bool listener(const LLBoundListener& conn, + const std::string& name, + const LLAwareListener& target, const LLSD& event) const; LLEventPump& mPump; diff --git a/indra/llcommon/llevents.cpp b/indra/llcommon/llevents.cpp index 3c6743eac9..6531b951b9 100644 --- a/indra/llcommon/llevents.cpp +++ b/indra/llcommon/llevents.cpp @@ -43,6 +43,7 @@ #include <typeinfo> #include <cmath> #include <cctype> +#include <iomanip> // std::quoted // external library headers #include <boost/range/iterator_range.hpp> #if LL_WINDOWS @@ -54,10 +55,11 @@ #pragma warning (pop) #endif // other Linden headers -#include "stringize.h" #include "llerror.h" -#include "llsdutil.h" +#include "lleventfilter.h" #include "llexception.h" +#include "llsdutil.h" +#include "stringize.h" #if LL_MSVC #pragma warning (disable : 4702) #endif @@ -71,7 +73,9 @@ LLEventPumps::LLEventPumps(): { "LLEventStream", [](const std::string& name, bool tweak, const std::string& /*type*/) { return new LLEventStream(name, tweak); } }, { "LLEventMailDrop", [](const std::string& name, bool tweak, const std::string& /*type*/) - { return new LLEventMailDrop(name, tweak); } } + { return new LLEventMailDrop(name, tweak); } }, + { "LLEventLogProxy", [](const std::string& name, bool tweak, const std::string& /*type*/) + { return new LLEventLogProxyFor<LLEventStream>(name, tweak); } } }, mTypes { @@ -186,8 +190,13 @@ bool LLEventPumps::post(const std::string&name, const LLSD&message) PumpMap::iterator found = mPumpMap.find(name); if (found == mPumpMap.end()) + { + LL_DEBUGS("LLEventPumps") << "LLEventPump(" << std::quoted(name) << ") not found" + << LL_ENDL; return false; + } +// LL_DEBUGS("LLEventPumps") << "posting to " << name << ": " << message << LL_ENDL; return (*found).second->post(message); } @@ -350,8 +359,7 @@ const std::string LLEventPump::ANONYMOUS = std::string(); LLEventPump::LLEventPump(const std::string& name, bool tweak): // Register every new instance with LLEventPumps - mRegistry(LLEventPumps::instance().getHandle()), - mName(mRegistry.get()->registerNew(*this, name, tweak)), + mName(LLEventPumps::instance().registerNew(*this, name, tweak)), mSignal(std::make_shared<LLStandardSignal>()), mEnabled(true) {} @@ -364,10 +372,9 @@ LLEventPump::~LLEventPump() { // Unregister this doomed instance from LLEventPumps -- but only if // LLEventPumps is still around! - LLEventPumps* registry = mRegistry.get(); - if (registry) + if (LLEventPumps::instanceExists()) { - registry->unregister(*this); + LLEventPumps::instance().unregister(*this); } } @@ -382,7 +389,7 @@ std::string LLEventPump::inventName(const std::string& pfx) void LLEventPump::clear() { - LLCoros::LockType lock(mConnectionListMutex); + llcoro::LockType lock(mConnectionListMutex); // Destroy the original LLStandardSignal instance, replacing it with a // whole new one. mSignal = std::make_shared<LLStandardSignal>(); @@ -394,7 +401,7 @@ void LLEventPump::reset() { // Resetting mSignal is supposed to disconnect everything on its own // But due to crash on 'reset' added explicit cleanup to get more data - LLCoros::LockType lock(mConnectionListMutex); + llcoro::LockType lock(mConnectionListMutex); ConnectionMap::const_iterator iter = mConnections.begin(); ConnectionMap::const_iterator end = mConnections.end(); while (iter!=end) @@ -408,18 +415,18 @@ void LLEventPump::reset() //mDeps.clear(); } -LLBoundListener LLEventPump::listen_impl(const std::string& name, const LLEventListener& listener, +LLBoundListener LLEventPump::listen_impl(const std::string& name, const LLAwareListener& listener, const NameList& after, const NameList& before) { if (!mSignal) { - LL_WARNS() << "Can't connect listener" << LL_ENDL; + LL_WARNS("LLEventPump") << "Can't connect listener" << LL_ENDL; // connect will fail, return dummy return LLBoundListener(); } - LLCoros::LockType lock(mConnectionListMutex); + llcoro::LockType lock(mConnectionListMutex); float nodePosition = 1.0; @@ -568,7 +575,7 @@ LLBoundListener LLEventPump::listen_impl(const std::string& name, const LLEventL } // Now that newNode has a value that places it appropriately in mSignal, // connect it. - LLBoundListener bound = mSignal->connect(nodePosition, listener); + LLBoundListener bound = mSignal->connect_extended(nodePosition, listener); if (!name.empty()) { // note that we are not tracking anonymous listeners here either. @@ -582,7 +589,7 @@ LLBoundListener LLEventPump::listen_impl(const std::string& name, const LLEventL LLBoundListener LLEventPump::getListener(const std::string& name) { - LLCoros::LockType lock(mConnectionListMutex); + llcoro::LockType lock(mConnectionListMutex); ConnectionMap::const_iterator found = mConnections.find(name); if (found != mConnections.end()) { @@ -594,7 +601,7 @@ LLBoundListener LLEventPump::getListener(const std::string& name) void LLEventPump::stopListening(const std::string& name) { - LLCoros::LockType lock(mConnectionListMutex); + llcoro::LockType lock(mConnectionListMutex); ConnectionMap::iterator found = mConnections.find(name); if (found != mConnections.end()) { @@ -652,7 +659,7 @@ bool LLEventMailDrop::post(const LLSD& event) } LLBoundListener LLEventMailDrop::listen_impl(const std::string& name, - const LLEventListener& listener, + const LLAwareListener& listener, const NameList& after, const NameList& before) { @@ -661,7 +668,10 @@ LLBoundListener LLEventMailDrop::listen_impl(const std::string& name, // Remove any that this listener consumes -- Effective STL, Item 9. for (auto hi(mEventHistory.begin()), hend(mEventHistory.end()); hi != hend; ) { - if (listener(*hi)) + // We don't actually have an LLBoundListener in hand, and we won't + // until the base-class listen_impl() call below. Pass an empty + // instance. + if (listener({}, *hi)) { // new listener consumed this event, erase it hi = mEventHistory.erase(hi); @@ -733,7 +743,7 @@ void LLReqID::stamp(LLSD& response) const response["reqid"] = mReqid; } -bool sendReply(const LLSD& reply, const LLSD& request, const std::string& replyKey) +bool sendReply(LLSD reply, const LLSD& request, const std::string& replyKey) { // If the original request has no value for replyKey, it's pointless to // construct or send a reply event: on which LLEventPump should we send @@ -746,13 +756,13 @@ bool sendReply(const LLSD& reply, const LLSD& request, const std::string& replyK // Here the request definitely contains replyKey; reasonable to proceed. - // Copy 'reply' to modify it. - LLSD newreply(reply); // Get the ["reqid"] element from request LLReqID reqID(request); - // and copy it to 'newreply'. - reqID.stamp(newreply); - // Send reply on LLEventPump named in request[replyKey]. Don't forget to - // send the modified 'newreply' instead of the original 'reply'. - return LLEventPumps::instance().obtain(request[replyKey]).post(newreply); + // and copy it to 'reply'. + reqID.stamp(reply); + // Send reply on LLEventPump named in request[replyKey] -- if that + // LLEventPump exists. If it does not, don't create it. + // This addresses the case in which a requester goes away before a + // particular LLEventAPI responds. + return LLEventPumps::instance().post(request[replyKey], reply); } diff --git a/indra/llcommon/llevents.h b/indra/llcommon/llevents.h index 4bf1fa07a2..d339a09117 100644 --- a/indra/llcommon/llevents.h +++ b/indra/llcommon/llevents.h @@ -32,35 +32,21 @@ #if ! defined(LL_LLEVENTS_H) #define LL_LLEVENTS_H -#include <string> +#include <deque> +#include <functional> #include <map> #include <set> +#include <string> +#include <type_traits> #include <vector> -#include <deque> -#include <functional> #include <boost/signals2.hpp> -#include <boost/bind.hpp> -#include <boost/utility.hpp> // noncopyable #include <boost/optional/optional.hpp> -#include <boost/visit_each.hpp> -#include <boost/ref.hpp> // reference_wrapper -#include <boost/type_traits/is_pointer.hpp> -#include <boost/static_assert.hpp> -#include "llsd.h" -#include "llsingleton.h" +#include "llcoromutex.h" #include "lldependencies.h" -#include "llstl.h" #include "llexception.h" -#include "llhandle.h" -#include "llcoros.h" - -/*==========================================================================*| -// override this to allow binding free functions with more parameters -#ifndef LLEVENTS_LISTENER_ARITY -#define LLEVENTS_LISTENER_ARITY 10 -#endif -|*==========================================================================*/ +#include "llsd.h" +#include "llsingleton.h" // hack for testing #ifndef testable @@ -144,6 +130,12 @@ typedef boost::signals2::signal<bool(const LLSD&), LLStopWhenHandled, float> LL /// Methods that forward listeners (e.g. constructed with /// <tt>boost::bind()</tt>) should accept (const LLEventListener&) typedef LLStandardSignal::slot_type LLEventListener; +/// Support a listener accepting (const LLBoundListener&, const LLSD&). +/// Note that LLBoundListener::disconnect() is a const method: this feature is +/// specifically intended to allow a listener to disconnect itself when done. +typedef LLStandardSignal::extended_slot_type LLAwareListener; +/// Accept a void listener too +typedef std::function<void(const LLSD&)> LLVoidListener; /// Result of registering a listener, supports <tt>connected()</tt>, /// <tt>disconnect()</tt> and <tt>blocked()</tt> typedef boost::signals2::connection LLBoundListener; @@ -218,15 +210,7 @@ class LLEventPump; * LLEventPumps is a Singleton manager through which one typically accesses * this subsystem. */ -// LLEventPumps isa LLHandleProvider only for (hopefully rare) long-lived -// class objects that must refer to this class late in their lifespan, say in -// the destructor. Specifically, the case that matters is a possible reference -// after LLEventPumps::deleteSingleton(). (Lingering LLEventPump instances are -// capable of this.) In that case, instead of calling LLEventPumps::instance() -// again -- resurrecting the deleted LLSingleton -- store an -// LLHandle<LLEventPumps> and test it before use. -class LL_COMMON_API LLEventPumps: public LLSingleton<LLEventPumps>, - public LLHandleProvider<LLEventPumps> +class LL_COMMON_API LLEventPumps: public LLSingleton<LLEventPumps> { LLSINGLETON(LLEventPumps); public: @@ -372,56 +356,13 @@ testable: }; /***************************************************************************** -* LLEventTrackable -*****************************************************************************/ -/** - * LLEventTrackable wraps boost::signals2::trackable, which resembles - * boost::trackable. Derive your listener class from LLEventTrackable instead, - * and use something like - * <tt>LLEventPump::listen(boost::bind(&YourTrackableSubclass::method, - * instance, _1))</tt>. This will implicitly disconnect when the object - * referenced by @c instance is destroyed. - * - * @note - * LLEventTrackable doesn't address a couple of cases: - * * Object destroyed during call - * - You enter a slot call in thread A. - * - Thread B destroys the object, which of course disconnects it from any - * future slot calls. - * - Thread A's call uses 'this', which now refers to a defunct object. - * Undefined behavior results. - * * Call during destruction - * - @c MySubclass is derived from LLEventTrackable. - * - @c MySubclass registers one of its own methods using - * <tt>LLEventPump::listen()</tt>. - * - The @c MySubclass object begins destruction. <tt>~MySubclass()</tt> - * runs, destroying state specific to the subclass. (For instance, a - * <tt>Foo*</tt> data member is <tt>delete</tt>d but not zeroed.) - * - The listening method will not be disconnected until - * <tt>~LLEventTrackable()</tt> runs. - * - Before we get there, another thread posts data to the @c LLEventPump - * instance, calling the @c MySubclass method. - * - The method in question relies on valid @c MySubclass state. (For - * instance, it attempts to dereference the <tt>Foo*</tt> pointer that was - * <tt>delete</tt>d but not zeroed.) - * - Undefined behavior results. - */ -typedef boost::signals2::trackable LLEventTrackable; - -/***************************************************************************** * LLEventPump *****************************************************************************/ /** * LLEventPump is the base class interface through which we access the * concrete subclasses such as LLEventStream. - * - * @NOTE - * LLEventPump derives from LLEventTrackable so that when you "chain" - * LLEventPump instances together, they will automatically disconnect on - * destruction. Please see LLEventTrackable documentation for situations in - * which this may be perilous across threads. */ -class LL_COMMON_API LLEventPump: public LLEventTrackable +class LL_COMMON_API LLEventPump { public: static const std::string ANONYMOUS; // constant for anonymous listeners. @@ -535,18 +476,37 @@ public: * call, allows us to optimize away the second and subsequent dependency * sorts. * - * If name is set to LLEventPump::ANONYMOUS listen will bypass the entire + * If name is set to LLEventPump::ANONYMOUS, listen() will bypass the entire * dependency and ordering calculation. In this case, it is critical that * the result be assigned to a LLTempBoundListener or the listener is - * manually disconnected when no longer needed since there will be no + * manually disconnected when no longer needed, since there will be no * way to later find and disconnect this listener manually. */ + template <typename LISTENER> LLBoundListener listen(const std::string& name, - const LLEventListener& listener, + LISTENER&& listener, const NameList& after=NameList(), const NameList& before=NameList()) { - return listen_impl(name, listener, after, before); + if constexpr (std::is_invocable_v<LISTENER, const LLSD&>) + { + // wrap classic LLEventListener in LLAwareListener lambda + return listenb( + name, + [listener=std::move(listener)] + (const LLBoundListener&, const LLSD& event) + { + return listener(event); + }, + after, + before); + } + else + { + static_assert(std::is_invocable_v<LISTENER, LLBoundListener, const LLSD&>, + "LLEventPump::listen() listener has bad parameter signature"); + return listenb(name, std::forward<LISTENER>(listener), after, before); + } } /// Get the LLBoundListener associated with the passed name (dummy @@ -587,17 +547,40 @@ private: virtual void clear(); virtual void reset(); - - private: - // must precede mName; see LLEventPump::LLEventPump() - LLHandle<LLEventPumps> mRegistry; - std::string mName; - LLCoros::Mutex mConnectionListMutex; + llcoro::Mutex mConnectionListMutex; protected: - virtual LLBoundListener listen_impl(const std::string& name, const LLEventListener&, + template <typename LISTENER> + LLBoundListener listenb(const std::string& name, + LISTENER&& listener, + const NameList& after=NameList(), + const NameList& before=NameList()) + { + using result_t = std::decay_t<decltype(listener(LLBoundListener(), LLSD()))>; + if constexpr (std::is_same_v<bool, result_t>) + { + return listen_impl(name, std::forward<LISTENER>(listener), after, before); + } + else + { + static_assert(std::is_same_v<void, result_t>, + "LLEventPump::listen() listener has bad return type"); + // wrap void listener in one that returns bool + return listen_impl( + name, + [listener=std::move(listener)] + (const LLBoundListener& conn, const LLSD& event) + { + listener(conn, event); + return false; + }, + after, + before); + } + } + virtual LLBoundListener listen_impl(const std::string& name, const LLAwareListener&, const NameList& after, const NameList& before); @@ -672,7 +655,7 @@ public: void discard(); protected: - virtual LLBoundListener listen_impl(const std::string& name, const LLEventListener&, + virtual LLBoundListener listen_impl(const std::string& name, const LLAwareListener&, const NameList& after, const NameList& before) override; @@ -682,6 +665,30 @@ private: }; /***************************************************************************** +* LLNamedListener +*****************************************************************************/ +/** + * LLNamedListener bundles a concrete LLEventPump subclass with a specific + * listener function, with an LLTempBoundListener to ensure that it's + * disconnected before destruction. + */ +template <class PUMP=LLEventStream> +class LL_COMMON_API LLNamedListener: PUMP +{ + using pump_t = PUMP; +public: + template <typename LISTENER> + LLNamedListener(const std::string& name, LISTENER&& listener): + pump_t(name, false), // don't tweak the name + mConn(pump_t::listen("func", std::forward<LISTENER>(listener))) + {} + +private: + LLTempBoundListener mConn; +}; +using LLStreamListener = LLNamedListener<>; + +/***************************************************************************** * LLReqID *****************************************************************************/ /** @@ -773,7 +780,7 @@ private: * Before sending the reply event, sendReply() copies the ["reqid"] item from * the request to the reply. */ -LL_COMMON_API bool sendReply(const LLSD& reply, const LLSD& request, +LL_COMMON_API bool sendReply(LLSD reply, const LLSD& request, const std::string& replyKey="reply"); #endif /* ! defined(LL_LLEVENTS_H) */ diff --git a/indra/llcommon/lleventtimer.cpp b/indra/llcommon/lleventtimer.cpp index 33fafffefd..cc227193cd 100644 --- a/indra/llcommon/lleventtimer.cpp +++ b/indra/llcommon/lleventtimer.cpp @@ -25,49 +25,44 @@ */ #include "linden_common.h" - #include "lleventtimer.h" -#include "u64.h" - - ////////////////////////////////////////////////////////////////////////////// // // LLEventTimer Implementation // ////////////////////////////////////////////////////////////////////////////// -LLEventTimer::LLEventTimer(F32 period) -: mEventTimer() +LLEventTimer::LLEventTimer(F32 period): + mPeriod(period) { - mPeriod = period; + start(); } -LLEventTimer::LLEventTimer(const LLDate& time) -: mEventTimer() +LLEventTimer::LLEventTimer(const LLDate& time): + LLEventTimer(F32(time.secondsSinceEpoch() - LLDate::now().secondsSinceEpoch())) +{} + +LLEventTimer::~LLEventTimer() { - mPeriod = (F32)(time.secondsSinceEpoch() - LLDate::now().secondsSinceEpoch()); } - -LLEventTimer::~LLEventTimer() +void LLEventTimer::start() { + mTimer = LL::Timers::instance().scheduleEvery([this]{ return tick(); }, mPeriod); } -//static -void LLEventTimer::updateClass() +void LLEventTimer::stop() { - for (auto& timer : instance_snapshot()) - { - F32 et = timer.mEventTimer.getElapsedTimeF32(); - if (timer.mEventTimer.getStarted() && et > timer.mPeriod) { - timer.mEventTimer.reset(); - if ( timer.tick() ) - { - delete &timer; - } - } - } + LL::Timers::instance().cancel(mTimer); } +bool LLEventTimer::isRunning() +{ + return LL::Timers::instance().isRunning(mTimer); +} +F32 LLEventTimer::getRemaining() +{ + return LL::Timers::instance().timeUntilCall(mTimer); +} diff --git a/indra/llcommon/lleventtimer.h b/indra/llcommon/lleventtimer.h index e0c2381807..b37d682d29 100644 --- a/indra/llcommon/lleventtimer.h +++ b/indra/llcommon/lleventtimer.h @@ -27,13 +27,12 @@ #ifndef LL_EVENTTIMER_H #define LL_EVENTTIMER_H -#include "stdtypes.h" +#include "llcallbacklist.h" #include "lldate.h" -#include "llinstancetracker.h" -#include "lltimer.h" +#include "stdtypes.h" // class for scheduling a function to be called at a given frequency (approximate, inprecise) -class LL_COMMON_API LLEventTimer : public LLInstanceTracker<LLEventTimer> +class LL_COMMON_API LLEventTimer { public: @@ -41,82 +40,18 @@ public: LLEventTimer(const LLDate& time); virtual ~LLEventTimer(); + void start(); + void stop(); + bool isRunning(); + F32 getRemaining(); + //function to be called at the supplied frequency - // Normally return FALSE; TRUE will delete the timer after the function returns. + // Normally return false; true will delete the timer after the function returns. virtual bool tick() = 0; - static void updateClass(); - - /// Schedule recurring calls to generic callable every period seconds. - /// Returns a pointer; if you delete it, cancels the recurring calls. - template <typename CALLABLE> - static LLEventTimer* run_every(F32 period, const CALLABLE& callable); - - /// Schedule a future call to generic callable. Returns a pointer. - /// CAUTION: The object referenced by that pointer WILL BE DELETED once - /// the callback has been called! LLEventTimer::getInstance(pointer) (NOT - /// pointer->getInstance(pointer)!) can be used to test whether the - /// pointer is still valid. If it is, deleting it will cancel the - /// callback. - template <typename CALLABLE> - static LLEventTimer* run_at(const LLDate& time, const CALLABLE& callable); - - /// Like run_at(), but after a time delta rather than at a timestamp. - /// Same CAUTION. - template <typename CALLABLE> - static LLEventTimer* run_after(F32 interval, const CALLABLE& callable); - protected: - LLTimer mEventTimer; + LL::Timers::temp_handle_t mTimer; F32 mPeriod; - -private: - template <typename CALLABLE> - class Generic; }; -template <typename CALLABLE> -class LLEventTimer::Generic: public LLEventTimer -{ -public: - // making TIME generic allows engaging either LLEventTimer constructor - template <typename TIME> - Generic(const TIME& time, bool once, const CALLABLE& callable): - LLEventTimer(time), - mOnce(once), - mCallable(callable) - {} - bool tick() override - { - mCallable(); - // true tells updateClass() to delete this instance - return mOnce; - } - -private: - bool mOnce; - CALLABLE mCallable; -}; - -template <typename CALLABLE> -LLEventTimer* LLEventTimer::run_every(F32 period, const CALLABLE& callable) -{ - // return false to schedule recurring calls - return new Generic<CALLABLE>(period, false, callable); -} - -template <typename CALLABLE> -LLEventTimer* LLEventTimer::run_at(const LLDate& time, const CALLABLE& callable) -{ - // return true for one-shot callback - return new Generic<CALLABLE>(time, true, callable); -} - -template <typename CALLABLE> -LLEventTimer* LLEventTimer::run_after(F32 interval, const CALLABLE& callable) -{ - // one-shot callback after specified interval - return new Generic<CALLABLE>(interval, true, callable); -} - #endif //LL_EVENTTIMER_H diff --git a/indra/llcommon/llexception.h b/indra/llcommon/llexception.h index 68e609444e..15dacd76fc 100644 --- a/indra/llcommon/llexception.h +++ b/indra/llcommon/llexception.h @@ -12,6 +12,7 @@ #if ! defined(LL_LLEXCEPTION_H) #define LL_LLEXCEPTION_H +#include "stdtypes.h" #include <stdexcept> #include <boost/exception/exception.hpp> #include <boost/throw_exception.hpp> diff --git a/indra/llcommon/llformat.h b/indra/llcommon/llformat.h index 4456a72696..0c840d6e8e 100644 --- a/indra/llcommon/llformat.h +++ b/indra/llcommon/llformat.h @@ -28,6 +28,8 @@ #ifndef LL_LLFORMAT_H #define LL_LLFORMAT_H +#include "llpreprocessor.h" + // Use as follows: // LL_INFOS() << llformat("Test:%d (%.2f %.2f)", idx, x, y) << LL_ENDL; // diff --git a/indra/llcommon/llinstancetracker.h b/indra/llcommon/llinstancetracker.h index 92b26354a1..03418e9bad 100644 --- a/indra/llcommon/llinstancetracker.h +++ b/indra/llcommon/llinstancetracker.h @@ -80,6 +80,8 @@ class LLInstanceTracker { InstanceMap mMap; }; + // Unfortunately there's no umbrella class that owns all LLInstanceTracker + // instances, so there's no good place to call LockStatic::cleanup(). typedef llthread::LockStatic<StaticData> LockStatic; public: @@ -170,23 +172,7 @@ public: } // lock static data during construction -#if ! LL_WINDOWS LockStatic mLock; -#else // LL_WINDOWS - // We want to be able to use (e.g.) our instance_snapshot subclass as: - // for (auto& inst : T::instance_snapshot()) ... - // But when this snapshot base class directly contains LockStatic, as - // above, Visual Studio 2017 requires us to code instead: - // for (auto& inst : std::move(T::instance_snapshot())) ... - // nat thinks this should be unnecessary, as an anonymous class - // instance is already a temporary. It shouldn't need to be cast to - // rvalue reference (the role of std::move()). clang evidently agrees, - // as the short form works fine with Xcode on Mac. - // To support the succinct usage, instead of directly storing - // LockStatic, store std::shared_ptr<LockStatic>, which is copyable. - std::shared_ptr<LockStatic> mLockp{std::make_shared<LockStatic>()}; - LockStatic& mLock{*mLockp}; -#endif // LL_WINDOWS VectorType mData; }; using snapshot = snapshot_of<T>; @@ -276,6 +262,35 @@ protected: public: virtual const KEY& getKey() const { return mInstanceKey; } + /// for use ONLY for an object we're sure resides on the heap! + static bool erase(const KEY& key) + { + return erase(getInstance(key)); + } + + /// for use ONLY for an object we're sure resides on the heap! + static bool erase(const weak_t& ptr) + { + return erase(ptr.lock()); + } + + /// for use ONLY for an object we're sure resides on the heap! + static bool erase(const ptr_t& ptr) + { + if (! ptr) + { + return false; + } + + // Because we store and return ptr_t instances with no-op deleters, + // merely resetting the last pointer doesn't destroy the referenced + // object. Don't even bother resetting 'ptr'. Just extract its raw + // pointer and delete that. + auto raw{ ptr.get() }; + delete raw; + return true; + } + private: LLInstanceTracker( const LLInstanceTracker& ) = delete; LLInstanceTracker& operator=( const LLInstanceTracker& ) = delete; @@ -356,6 +371,7 @@ class LLInstanceTracker<T, void, KEY_COLLISION_BEHAVIOR> { InstanceSet mSet; }; + // see LockStatic comment in the above specialization for non-void KEY typedef llthread::LockStatic<StaticData> LockStatic; public: @@ -434,23 +450,7 @@ public: } // lock static data during construction -#if ! LL_WINDOWS LockStatic mLock; -#else // LL_WINDOWS - // We want to be able to use our instance_snapshot subclass as: - // for (auto& inst : T::instance_snapshot()) ... - // But when this snapshot base class directly contains LockStatic, as - // above, Visual Studio 2017 requires us to code instead: - // for (auto& inst : std::move(T::instance_snapshot())) ... - // nat thinks this should be unnecessary, as an anonymous class - // instance is already a temporary. It shouldn't need to be cast to - // rvalue reference (the role of std::move()). clang evidently agrees, - // as the short form works fine with Xcode on Mac. - // To support the succinct usage, instead of directly storing - // LockStatic, store std::shared_ptr<LockStatic>, which is copyable. - std::shared_ptr<LockStatic> mLockp{std::make_shared<LockStatic>()}; - LockStatic& mLock{*mLockp}; -#endif // LL_WINDOWS VectorType mData; }; using snapshot = snapshot_of<T>; @@ -481,6 +481,29 @@ public: template <typename SUBCLASS> using key_snapshot_of = instance_snapshot_of<SUBCLASS>; + /// for use ONLY for an object we're sure resides on the heap! + static bool erase(const weak_t& ptr) + { + return erase(ptr.lock()); + } + + /// for use ONLY for an object we're sure resides on the heap! + static bool erase(const ptr_t& ptr) + { + if (! ptr) + { + return false; + } + + // Because we store and return ptr_t instances with no-op deleters, + // merely resetting the last pointer doesn't destroy the referenced + // object. Don't even bother resetting 'ptr'. Just extract its raw + // pointer and delete that. + auto raw{ ptr.get() }; + delete raw; + return true; + } + protected: LLInstanceTracker() { diff --git a/indra/llcommon/llinttracker.h b/indra/llcommon/llinttracker.h new file mode 100644 index 0000000000..fd6d24d0fd --- /dev/null +++ b/indra/llcommon/llinttracker.h @@ -0,0 +1,43 @@ +/** + * @file llinttracker.h + * @author Nat Goodspeed + * @date 2024-08-30 + * @brief LLIntTracker isa LLInstanceTracker with generated int keys. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLINTTRACKER_H) +#define LL_LLINTTRACKER_H + +#include "llinstancetracker.h" + +template <typename T> +class LLIntTracker: public LLInstanceTracker<T, int> +{ + using super = LLInstanceTracker<T, int>; +public: + LLIntTracker(): + super(getUniqueKey()) + {} + +private: + static int getUniqueKey() + { + // Find a random key that does NOT already correspond to an instance. + // Passing a duplicate key to LLInstanceTracker would do Bad Things. + int key; + do + { + key = std::rand(); + } while (super::getInstance(key)); + // This could be racy, if we were instantiating new LLIntTracker<T>s + // on multiple threads. If we need that, have to lock between checking + // getInstance() and constructing the new super. + return key; + } +}; + +#endif /* ! defined(LL_LLINTTRACKER_H) */ diff --git a/indra/llcommon/llleap.cpp b/indra/llcommon/llleap.cpp index 662a2511cd..306d4e48d0 100644 --- a/indra/llcommon/llleap.cpp +++ b/indra/llcommon/llleap.cpp @@ -14,8 +14,9 @@ // associated header #include "llleap.h" // STL headers -#include <sstream> #include <algorithm> +#include <memory> +#include <sstream> // std headers // external library headers // other Linden headers @@ -50,20 +51,19 @@ public: // We expect multiple LLLeapImpl instances. Definitely tweak // mDonePump's name for uniqueness. mDonePump("LLLeap", true), - // Troubling thought: what if one plugin intentionally messes with - // another plugin? LLEventPump names are in a single global namespace. - // Try to make that more difficult by generating a UUID for the reply- - // pump name -- so it should NOT need tweaking for uniqueness. - mReplyPump(LLUUID::generateNewID().asString()), mExpect(0), // Instantiate a distinct LLLeapListener for this plugin. (Every // plugin will want its own collection of managed listeners, etc.) - // Pass it a callback to our connect() method, so it can send events + // Pass it our wstdin() method as its callback, so it can send events // from a particular LLEventPump to the plugin without having to know // this class or method name. - mListener(new LLLeapListener( - [this](LLEventPump& pump, const std::string& listener) - { return connect(pump, listener); })) + mListener( + new LLLeapListener( + "LLLeap", + // Serialize any reply event to our child's stdin, suitably + // enriched with the pump name on which it was received. + [this](const std::string& pump, const LLSD& data) + { return wstdin(pump, data); })) { // Rule out unpopulated Params block if (! cparams.executable.isProvided()) @@ -122,9 +122,6 @@ public: childout.setLimit(20); childerr.setLimit(20); - // Serialize any event received on mReplyPump to our child's stdin. - mStdinConnection = connect(mReplyPump, "LLLeap"); - // Listening on stdout is stateful. In general, we're either waiting // for the length prefix or waiting for the specified length of data. mReadPrefix = true; @@ -146,7 +143,7 @@ public: // Send child a preliminary event reporting our own reply-pump name -- // which would otherwise be pretty tricky to guess! - wstdin(mReplyPump.getName(), + wstdin(mListener->getReplyPump().getName(), LLSDMap ("command", mListener->getName()) // Include LLLeap features -- this may be important for child to @@ -193,7 +190,7 @@ public: return false; } - // Listener for events on mReplyPump: send to child stdin + // Listener for reply events: send to child stdin bool wstdin(const std::string& pump, const LLSD& data) { LLSD packet(LLSDMap("pump", pump)("data", data)); @@ -344,7 +341,7 @@ public: { // The LLSD object we got from our stream contains the // keys we need. - LLEventPumps::instance().obtain(data["pump"]).post(data["data"]); + LLEventPumps::instance().post(data["pump"], data["data"]); } catch (const std::exception& err) { @@ -355,7 +352,7 @@ public: // 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(); + data["reply"] = mListener->getReplyPump().getName(); sendReply(llsd::map("error", stringize(LLError::Log::classname(err), ": ", err.what())), data); @@ -423,7 +420,7 @@ public: LLSD event; event["type"] = "error"; event["error"] = error; - mReplyPump.post(event); + mListener->getReplyPump().post(event); // All the above really accomplished was to buffer the serialized // event in our WritePipe. Have to pump mainloop a couple times to @@ -440,24 +437,10 @@ public: } private: - /// We always want to listen on mReplyPump with wstdin(); under some - /// circumstances we'll also echo other LLEventPumps to the plugin. - LLBoundListener connect(LLEventPump& pump, const std::string& listener) - { - // Serialize any event received on the specified LLEventPump to our - // child's stdin, suitably enriched with the pump name on which it was - // received. - return pump.listen(listener, - [this, name=pump.getName()](const LLSD& data) - { return wstdin(name, data); }); - } - std::string mDesc; LLEventStream mDonePump; - LLEventStream mReplyPump; LLProcessPtr mChild; - LLTempBoundListener - mStdinConnection, mStdoutConnection, mStderrConnection; + LLTempBoundListener mStdoutConnection, mStderrConnection; LLProcess::ReadPipe::size_type mExpect; LLError::RecorderPtr mRecorder; std::unique_ptr<LLLeapListener> mListener; diff --git a/indra/llcommon/llleaplistener.cpp b/indra/llcommon/llleaplistener.cpp index 050d71c327..b81ee66ba9 100644 --- a/indra/llcommon/llleaplistener.cpp +++ b/indra/llcommon/llleaplistener.cpp @@ -54,53 +54,62 @@ return features; } -LLLeapListener::LLLeapListener(const ConnectFunc& connect): +LLLeapListener::LLLeapListener(const std::string_view& caller, const Callback& callback): // Each LEAP plugin has an instance of this listener. Make the command // pump name difficult for other such plugins to guess. LLEventAPI(LLUUID::generateNewID().asString(), "Operations relating to the LLSD Event API Plugin (LEAP) protocol"), - mConnect(connect) + mCaller(caller), + mCallback(callback), + // Troubling thought: what if one plugin intentionally messes with + // another plugin? LLEventPump names are in a single global namespace. + // Try to make that more difficult by generating a UUID for the reply- + // pump name -- so it should NOT need tweaking for uniqueness. + mReplyPump(LLUUID::generateNewID().asString()), + mReplyConn(connect(mReplyPump, mCaller)) { LLSD need_name(LLSDMap("name", LLSD())); add("newpump", - "Instantiate a new LLEventPump named like [\"name\"] and listen to it.\n" - "[\"type\"] == \"LLEventStream\", \"LLEventMailDrop\" et al.\n" - "Events sent through new LLEventPump will be decorated with [\"pump\"]=name.\n" - "Returns actual name in [\"name\"] (may be different if collision).", +R"-(Instantiate a new LLEventPump named like ["name"] and listen to it. +["type"] == "LLEventStream", "LLEventMailDrop" et al. +Events sent through new LLEventPump will be decorated with ["pump"]=name. +Returns actual name in ["name"] (may be different if collision).)-", &LLLeapListener::newpump, need_name); LLSD need_source_listener(LLSDMap("source", LLSD())("listener", LLSD())); add("listen", - "Listen to an existing LLEventPump named [\"source\"], with listener name\n" - "[\"listener\"].\n" - "By default, send events on [\"source\"] to the plugin, decorated\n" - "with [\"pump\"]=[\"source\"].\n" - "If [\"dest\"] specified, send undecorated events on [\"source\"] to the\n" - "LLEventPump named [\"dest\"].\n" - "Returns [\"status\"] boolean indicating whether the connection was made.", +R"-(Listen to an existing LLEventPump named ["source"], with listener name +["listener"]. +If ["tweak"] is specified as true, tweak listener name for uniqueness. +By default, send events on ["source"] to the plugin, decorated +with ["pump"]=["source"]. +If ["dest"] specified, send undecorated events on ["source"] to the +LLEventPump named ["dest"]. +Returns ["status"] boolean indicating whether the connection was made, +plus ["listener"] reporting (possibly tweaked) listener name.)-", &LLLeapListener::listen, need_source_listener); add("stoplistening", - "Disconnect a connection previously established by \"listen\".\n" - "Pass same [\"source\"] and [\"listener\"] arguments.\n" - "Returns [\"status\"] boolean indicating whether such a listener existed.", +R"-(Disconnect a connection previously established by "listen". +Pass same ["source"] and ["listener"] arguments. +Returns ["status"] boolean indicating whether such a listener existed.)-", &LLLeapListener::stoplistening, need_source_listener); add("ping", - "No arguments, just a round-trip sanity check.", +"No arguments, just a round-trip sanity check.", &LLLeapListener::ping); add("getAPIs", - "Enumerate all LLEventAPI instances by name and description.", +"Enumerate all LLEventAPI instances by name and description.", &LLLeapListener::getAPIs); add("getAPI", - "Get name, description, dispatch key and operations for LLEventAPI [\"api\"].", +R"-(Get name, description, dispatch key and operations for LLEventAPI ["api"].)-", &LLLeapListener::getAPI, LLSD().with("api", LLSD())); add("getFeatures", - "Return an LLSD map of feature strings (deltas from baseline LEAP protocol)", +"Return an LLSD map of feature strings (deltas from baseline LEAP protocol)", static_cast<void (LLLeapListener::*)(const LLSD&) const>(&LLLeapListener::getFeatures)); add("getFeature", - "Return the feature value with key [\"feature\"]", +R"-(Return the feature value with key ["feature"])-", &LLLeapListener::getFeature, LLSD().with("feature", LLSD())); } @@ -112,6 +121,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.) + LL_DEBUGS("LLLeapListener") << "~LLLeapListener(\"" << mCaller << "\")" << LL_ENDL; for (ListenersMap::value_type& pair : mListeners) { pair.second.disconnect(); @@ -133,8 +143,7 @@ void LLLeapListener::newpump(const LLSD& request) reply["name"] = name; // Now listen on this new pump with our plugin listener - std::string myname("llleap"); - saveListener(name, myname, mConnect(new_pump, myname)); + saveListener(name, mCaller, connect(new_pump, mCaller)); } catch (const LLEventPumps::BadType& error) { @@ -149,6 +158,11 @@ void LLLeapListener::listen(const LLSD& request) std::string source_name = request["source"]; std::string dest_name = request["dest"]; std::string listener_name = request["listener"]; + if (request["tweak"].asBoolean()) + { + listener_name = LLEventPump::inventName(listener_name); + } + reply["listener"] = listener_name; LLEventPump & source = LLEventPumps::instance().obtain(source_name); @@ -170,7 +184,7 @@ void LLLeapListener::listen(const LLSD& request) { // "dest" unspecified means to direct events on "source" // to our plugin listener. - saveListener(source_name, listener_name, mConnect(source, listener_name)); + saveListener(source_name, listener_name, connect(source, listener_name)); } reply["status"] = true; } @@ -292,10 +306,27 @@ void LLLeapListener::getFeature(const LLSD& request) const } } +LLBoundListener LLLeapListener::connect(LLEventPump& pump, const std::string& listener) +{ + // Connect to source pump with an adapter that calls our callback with the + // pump name as well as the event data. + return pump.listen( + listener, + [callback=mCallback, pump=pump.getName()] + (const LLSD& data) + { return callback(pump, data); }); +} + void LLLeapListener::saveListener(const std::string& pump_name, const std::string& listener_name, const LLBoundListener& listener) { - mListeners.insert(ListenersMap::value_type(ListenersMap::key_type(pump_name, listener_name), - listener)); + // Don't use insert() or emplace() because, if this (pump_name, + // listener_name) pair is already in mListeners, we *want* to overwrite it. + auto& listener_entry{ mListeners[ListenersMap::key_type(pump_name, listener_name)] }; + // If we already stored a connection for this pump and listener name, + // disconnect it before overwriting it. But if this entry was newly + // created, disconnect() will be a no-op. + listener_entry.disconnect(); + listener_entry = listener; } diff --git a/indra/llcommon/llleaplistener.h b/indra/llcommon/llleaplistener.h index cad4543d02..d36d2ff8db 100644 --- a/indra/llcommon/llleaplistener.h +++ b/indra/llcommon/llleaplistener.h @@ -13,10 +13,9 @@ #define LL_LLLEAPLISTENER_H #include "lleventapi.h" +#include <functional> #include <map> #include <string> -#include <boost/function.hpp> -#include <boost/ptr_container/ptr_map.hpp> /// Listener class implementing LLLeap query/control operations. /// See https://jira.lindenlab.com/jira/browse/DEV-31978. @@ -24,18 +23,16 @@ class LLLeapListener: public LLEventAPI { public: /** - * Decouple LLLeap by dependency injection. Certain LLLeapListener - * operations must be able to cause LLLeap to listen on a specified - * LLEventPump with the LLLeap listener that wraps incoming events in an - * outer (pump=, data=) map and forwards them to the plugin. Very well, - * define the signature for a function that will perform that, and make - * our constructor accept such a function. + * Certain LLLeapListener operations listen on a specified LLEventPump. + * Accept a bool(pump, data) callback from our caller for when any such + * event is received. */ - typedef boost::function<LLBoundListener(LLEventPump&, const std::string& listener)> - ConnectFunc; - LLLeapListener(const ConnectFunc& connect); + using Callback = std::function<bool(const std::string& pump, const LLSD& data)>; + LLLeapListener(const std::string_view& caller, const Callback& callback); ~LLLeapListener(); + LLEventPump& getReplyPump() { return mReplyPump; } + static LLSD getFeatures(); private: @@ -48,10 +45,16 @@ private: void getFeatures(const LLSD&) const; void getFeature(const LLSD&) const; + LLBoundListener connect(LLEventPump& pump, const std::string& listener); void saveListener(const std::string& pump_name, const std::string& listener_name, const LLBoundListener& listener); - ConnectFunc mConnect; + // The relative order of these next declarations is important because the + // constructor will initialize in this order. + std::string mCaller; + Callback mCallback; + LLEventStream mReplyPump; + LLTempBoundListener mReplyConn; // In theory, listen() could simply call the relevant LLEventPump's // listen() method, stoplistening() likewise. Lifespan issues make us diff --git a/indra/llcommon/lllivefile.cpp b/indra/llcommon/lllivefile.cpp index 58de61a7e4..b20c91dbe4 100644 --- a/indra/llcommon/lllivefile.cpp +++ b/indra/llcommon/lllivefile.cpp @@ -170,7 +170,7 @@ namespace : LLEventTimer(refresh), mLiveFile(f) { } - bool tick() + bool tick() override { mLiveFile.checkAndReload(); return false; diff --git a/indra/llcommon/llmainthreadtask.h b/indra/llcommon/llmainthreadtask.h index c3ed7fef52..eccf11fcbe 100644 --- a/indra/llcommon/llmainthreadtask.h +++ b/indra/llcommon/llmainthreadtask.h @@ -13,11 +13,8 @@ #if ! defined(LL_LLMAINTHREADTASK_H) #define LL_LLMAINTHREADTASK_H -#include "lleventtimer.h" #include "llthread.h" -#include "llmake.h" -#include <future> -#include <type_traits> // std::result_of +#include "workqueue.h" /** * LLMainThreadTask provides a way to perform some task specifically on the @@ -28,18 +25,17 @@ * Instead of instantiating LLMainThreadTask, pass your invocable to its * static dispatch() method. dispatch() returns the result of calling your * task. (Or, if your task throws an exception, dispatch() throws that - * exception. See std::packaged_task.) + * exception.) * * When you call dispatch() on the main thread (as determined by * on_main_thread() in llthread.h), it simply calls your task and returns the * result. * - * When you call dispatch() on a secondary thread, it instantiates an - * LLEventTimer subclass scheduled immediately. Next time the main loop calls - * LLEventTimer::updateClass(), your task will be run, and LLMainThreadTask - * will fulfill a future with its result. Meanwhile the requesting thread - * blocks on that future. As soon as it is set, the requesting thread wakes up - * with the task result. + * When you call dispatch() on a secondary thread, it posts your task to + * gMainloopWork, the WorkQueue serviced by the main thread, using + * WorkQueue::waitForResult() to block the caller. Next time the main loop + * calls gMainloopWork.runFor(), your task will be run, and waitForResult() + * will return its result. */ class LLMainThreadTask { @@ -59,41 +55,15 @@ public: } else { - // It's essential to construct LLEventTimer subclass instances on - // the heap because, on completion, LLEventTimer deletes them. - // Once we enable C++17, we can use Class Template Argument - // Deduction. Until then, use llmake_heap(). - auto* task = llmake_heap<Task>(std::forward<CALLABLE>(callable)); - auto future = task->mTask.get_future(); - // Now simply block on the future. - return future.get(); + auto queue{ LL::WorkQueue::getInstance("mainloop") }; + // If this needs a null check and a message, please introduce a + // method in the .cpp file so consumers of this header don't drag + // in llerror.h. + // Use waitForResult_() so dispatch() can be used even from the + // calling thread's default coroutine. + return queue->waitForResult_(std::forward<CALLABLE>(callable)); } } - -private: - template <typename CALLABLE> - struct Task: public LLEventTimer - { - Task(CALLABLE&& callable): - // no wait time: call tick() next chance we get - LLEventTimer(0), - mTask(std::forward<CALLABLE>(callable)) - {} - bool tick() override - { - // run the task on the main thread, will populate the future - // obtained by get_future() - mTask(); - // tell LLEventTimer we're done (one shot) - return true; - } - // Given arbitrary CALLABLE, which might be a lambda, how are we - // supposed to obtain its signature for std::packaged_task? It seems - // redundant to have to add an argument list to engage invoke_result_t, then - // add the argument list again to complete the signature. At least we - // only support a nullary CALLABLE. - std::packaged_task<std::invoke_result_t<CALLABLE>()> mTask; - }; }; #endif /* ! defined(LL_LLMAINTHREADTASK_H) */ diff --git a/indra/llcommon/llrefcount.h b/indra/llcommon/llrefcount.h index 3a253d8fa6..707d825e53 100644 --- a/indra/llcommon/llrefcount.h +++ b/indra/llcommon/llrefcount.h @@ -29,6 +29,7 @@ #include <boost/noncopyable.hpp> #include <boost/intrusive_ptr.hpp> #include "llatomic.h" +#include "llerror.h" class LLMutex; diff --git a/indra/llcommon/llrun.h b/indra/llcommon/llrun.h index 70767572ff..adf1f49001 100644 --- a/indra/llcommon/llrun.h +++ b/indra/llcommon/llrun.h @@ -33,6 +33,17 @@ class LLRunnable; +////////////////////////////////////////////////////////////////////////////// +// DEPRECATION WARNING +// LLRunner is one of several mostly redundant ways to schedule future +// callbacks on the main thread. It seems to be unused in the current viewer. +// addRunnable() is only called by LLPumpIO::sleepChain(). +// sleepChain() is only called by LLIOSleeper and LLIOSleep. +// LLIOSleeper is referenced only by tests. +// LLIOSleep is only called by LLDeferredChain. +// LLDeferredChain isn't referenced at all. +////////////////////////////////////////////////////////////////////////////// + /** * @class LLRunner * @brief This class manages a set of LLRunnable objects. diff --git a/indra/llcommon/llsdutil.h b/indra/llcommon/llsdutil.h index 38bbe19ddd..3ce0b1726d 100644 --- a/indra/llcommon/llsdutil.h +++ b/indra/llcommon/llsdutil.h @@ -365,15 +365,14 @@ private: // 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. + // works. // (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_; + mutable std::vector<std::unique_ptr<LLSDParamBase>> converters_; public: LLSDParam(const LLSD& value): value_(value) {} @@ -389,9 +388,9 @@ public: { // capture 'ptr' with the specific subclass type because converters_ // only stores LLSDParamBase pointers - auto ptr{ std::make_shared<LLSDParam<std::decay_t<T>>>(value_) }; + auto ptr{ new LLSDParam<std::decay_t<T>>(value_) }; // keep the new converter alive until we ourselves are destroyed - converters_.push_back(ptr); + converters_.emplace_back(ptr); return *ptr; } }; @@ -474,12 +473,12 @@ public: } }; -namespace llsd -{ - /***************************************************************************** * range-based for-loop helpers for LLSD *****************************************************************************/ +namespace llsd +{ + /// Usage: for (LLSD item : inArray(someLLSDarray)) { ... } class inArray { @@ -525,7 +524,70 @@ private: } // namespace llsd +/***************************************************************************** +* LLSDParam<std::vector<T>> +*****************************************************************************/ +// Given an LLSD array, return a const std::vector<T>&, where T is a type +// supported by LLSDParam. Bonus: if the LLSD value is actually a scalar, +// return a single-element vector containing the converted value. +template <typename T> +class LLSDParam<std::vector<T>>: public LLSDParamBase +{ +public: + LLSDParam(const LLSD& array) + { + // treat undefined "array" as empty vector + if (array.isDefined()) + { + // what if it's a scalar? + if (! array.isArray()) + { + v.push_back(LLSDParam<T>(array)); + } + else // really is an array + { + // reserve space for the array entries + v.reserve(array.size()); + for (const auto& item : llsd::inArray(array)) + { + v.push_back(LLSDParam<T>(item)); + } + } + } + } + + operator const std::vector<T>&() const { return v; } + +private: + std::vector<T> v; +}; + +/***************************************************************************** +* LLSDParam<std::map<std::string, T>> +*****************************************************************************/ +// Given an LLSD map, return a const std::map<std::string, T>&, where T is a +// type supported by LLSDParam. +template <typename T> +class LLSDParam<std::map<std::string, T>>: public LLSDParamBase +{ +public: + LLSDParam(const LLSD& map) + { + for (const auto& pair : llsd::inMap(map)) + { + m[pair.first] = LLSDParam<T>(pair.second); + } + } + + operator const std::map<std::string, T>&() const { return m; } + +private: + std::map<std::string, T> m; +}; +/***************************************************************************** +* deep and shallow clone +*****************************************************************************/ // Creates a deep clone of an LLSD object. Maps, Arrays and binary objects // are duplicated, atomic primitives (Boolean, Integer, Real, etc) simply // use a shared reference. @@ -553,6 +615,60 @@ LLSD shallow(LLSD value, LLSD filter=LLSD()) { return llsd_shallow(value, filter } // namespace llsd +/***************************************************************************** +* toArray(), toMap() +*****************************************************************************/ +namespace llsd +{ + +// For some T convertible to LLSD, given std::vector<T> myVec, +// toArray(myVec) returns an LLSD array whose entries correspond to the +// items in myVec. +// For some U convertible to LLSD, given function U xform(const T&), +// toArray(myVec, xform) returns an LLSD array whose every entry is +// xform(item) of the corresponding item in myVec. +// toArray() actually works with any container<C> usable with range +// 'for', not just std::vector. +// (Once we get C++20 we can use std::identity instead of this default lambda.) +template <typename C, typename FUNC> +LLSD toArray(const C& container, FUNC&& func=[](const auto& arg){ return arg; }) +{ + LLSD array; + for (const auto& item : container) + { + array.append(std::forward<FUNC>(func)(item)); + } + return array; +} + +// For some T convertible to LLSD, given std::map<std::string, T> myMap, +// toMap(myMap) returns an LLSD map whose entries correspond to the +// (key, value) pairs in myMap. +// For some U convertible to LLSD, given function +// std::pair<std::string, U> xform(const std::pair<std::string, T>&), +// toMap(myMap, xform) returns an LLSD map whose every entry is +// xform(pair) of the corresponding (key, value) pair in myMap. +// toMap() actually works with any container usable with range 'for', not +// just std::map. It need not even be an associative container, as long as +// you pass an xform function that returns std::pair<std::string, U>. +// (Once we get C++20 we can use std::identity instead of this default lambda.) +template <typename C, typename FUNC> +LLSD toMap(const C& container, FUNC&& func=[](const auto& arg){ return arg; }) +{ + LLSD map; + for (const auto& pair : container) + { + const auto& [key, value] = std::forward<FUNC>(func)(pair); + map[key] = value; + } + return map; +} + +} // namespace llsd + +/***************************************************************************** +* boost::hash<LLSD> +*****************************************************************************/ // Specialization for generating a hash value from an LLSD block. namespace boost { diff --git a/indra/llcommon/llsingleton.cpp b/indra/llcommon/llsingleton.cpp index 05dc3cde79..2a38d6d896 100644 --- a/indra/llcommon/llsingleton.cpp +++ b/indra/llcommon/llsingleton.cpp @@ -27,11 +27,12 @@ #include "linden_common.h" #include "llsingleton.h" +#include "llcoros.h" +#include "lldependencies.h" #include "llerror.h" #include "llerrorcontrol.h" -#include "lldependencies.h" #include "llexception.h" -#include "llcoros.h" +#include "llmainthreadtask.h" #include <algorithm> #include <iostream> // std::cerr in dire emergency #include <sstream> @@ -270,17 +271,29 @@ void LLSingletonBase::reset_initializing(list_t::size_type size) void LLSingletonBase::MasterList::LockedInitializing::log(const char* verb, const char* name) { - LL_DEBUGS("LLSingleton") << verb << ' ' << demangle(name) << ';'; - if (mList) + LL_DEBUGS("LLSingleton") << verb << ' ' << demangle(name) << ';'; + if (mList) + { + for (list_t::const_reverse_iterator ri(mList->rbegin()), rend(mList->rend()); + ri != rend; ++ri) { - for (list_t::const_reverse_iterator ri(mList->rbegin()), rend(mList->rend()); - ri != rend; ++ri) - { - LLSingletonBase* sb(*ri); - LL_CONT << ' ' << classname(sb); - } + LLSingletonBase* sb(*ri); + LL_CONT << ' ' << classname(sb); } - LL_ENDL; + } + LL_ENDL; +} + +void LLSingletonBase::capture_dependency(LLSingletonBase* sb) +{ + // If we're called very late during application shutdown, the Boost.Fibers + // library may have shut down, and MasterList::mInitializing.get() might + // blow up. But if we're called that late, there's really no point in + // trying to capture this dependency. + if (boost::fibers::context::active()) + { + sb->capture_dependency(); + } } void LLSingletonBase::capture_dependency() @@ -484,3 +497,29 @@ std::string LLSingletonBase::demangle(const char* mangled) { return LLError::Log::demangle(mangled); } + +LLSingletonBase* LLSingletonBase::getInstanceForSecondaryThread( + const std::string& name, + const std::string& method, + const std::function<LLSingletonBase*()>& getInstance) +{ + // Normally it would be the height of folly to reference-bind args into a + // lambda to be executed on some other thread! By the time that thread + // executed the lambda, the references would all be dangling, and Bad + // Things would result. But LLMainThreadTask::dispatch() promises to block + // the calling thread until the passed task has completed. So in this case + // we know the references will remain valid until the lambda has run, so + // we dare to bind references. + return LLMainThreadTask::dispatch( + [&name, &method, &getInstance](){ + // VERY IMPORTANT to call getInstance() on the main thread, + // rather than going straight to constructSingleton()! + // During the time window before mInitState is INITIALIZED, + // multiple requests might be queued. It's essential that, as + // the main thread processes them, only the FIRST such request + // actually constructs the instance -- every subsequent one + // simply returns the existing instance. + loginfos({name, "::", method, " on main thread"}); + return getInstance(); + }); +} diff --git a/indra/llcommon/llsingleton.h b/indra/llcommon/llsingleton.h index b5659e053c..d3417a233e 100644 --- a/indra/llcommon/llsingleton.h +++ b/indra/llcommon/llsingleton.h @@ -25,18 +25,19 @@ #ifndef LLSINGLETON_H #define LLSINGLETON_H +#include <boost/call_traits.hpp> #include <boost/noncopyable.hpp> #include <boost/unordered_set.hpp> +#include <functional> #include <initializer_list> #include <list> #include <typeinfo> #include <vector> #include "mutex.h" #include "lockstatic.h" -#include "llthread.h" // on_main_thread() -#include "llmainthreadtask.h" +#include "apply.h" #include "llprofiler.h" -#include "llerror.h" +#include "llthread.h" // on_main_thread() #ifdef LL_WINDOWS #pragma warning(push) @@ -117,6 +118,8 @@ protected: // If a given call to B::getInstance() happens during either A::A() or // A::initSingleton(), record that A directly depends on B. void capture_dependency(); + // trampoline to non-static member function + static void capture_dependency(LLSingletonBase*); // delegate logging calls to llsingleton.cpp public: @@ -141,6 +144,17 @@ protected: // internal wrapper around calls to cleanupSingleton() void cleanup_(); + // This method is where we dispatch to LLMainThreadTask to acquire the + // subclass LLSingleton instance when the first getInstance() call is from + // a secondary thread. We delegate to the .cpp file to untangle header + // circularity. It accepts a std::function referencing the subclass + // getInstance() method -- which can't be virtual because it's static; we + // don't yet have an instance! For messaging, it also accepts the name of + // the subclass and the subclass method. + static LLSingletonBase* getInstanceForSecondaryThread( + const std::string& name, const std::string& method, + const std::function<LLSingletonBase*()>& getInstance); + // deleteSingleton() isn't -- and shouldn't be -- a virtual method. It's a // class static. However, given only Foo*, deleteAll() does need to be // able to reach Foo::deleteSingleton(). Make LLSingleton (which declares @@ -196,7 +210,7 @@ struct LLSingleton_manage_master } void capture_dependency(LLSingletonBase* sb) { - sb->capture_dependency(); + LLSingletonBase::capture_dependency(sb); } }; @@ -428,6 +442,11 @@ protected: // Remove this instance from the master list. LLSingleton_manage_master<DERIVED_TYPE>().remove(this); + // We should no longer need our LockStatic -- but the fact that we can + // query or even resurrect a deleted LLSingleton means we basically + // have to shrug and leak our SingletonData. It's not large, and this + // only happens at shutdown anyway. +// lk.cleanup(); } public: @@ -563,19 +582,11 @@ public: // Per the comment block above, dispatch to the main thread. loginfos({classname<DERIVED_TYPE>(), "::getInstance() dispatching to main thread"}); - auto instance = LLMainThreadTask::dispatch( - [](){ - // VERY IMPORTANT to call getInstance() on the main thread, - // rather than going straight to constructSingleton()! - // During the time window before mInitState is INITIALIZED, - // multiple requests might be queued. It's essential that, as - // the main thread processes them, only the FIRST such request - // actually constructs the instance -- every subsequent one - // simply returns the existing instance. - loginfos({classname<DERIVED_TYPE>(), - "::getInstance() on main thread"}); - return getInstance(); - }); + auto instance = static_cast<DERIVED_TYPE*>( + getInstanceForSecondaryThread( + classname<DERIVED_TYPE>(), + "getInstance()", + getInstance)); // record the dependency chain tracked on THIS thread, not the main // thread (consider a getInstance() overload with a tag param that // suppresses dep tracking when dispatched to the main thread) @@ -640,8 +651,14 @@ private: // Passes arguments to DERIVED_TYPE's constructor and sets appropriate // states, returning a pointer to the new instance. + // If we just let initParamSingleton_() infer its argument types, the + // compiler has trouble passing int and string literals. Use + // boost::call_traits::param_type to smooth parameter passing. This + // construction requires, though, that each invocation of this method + // explicitly specify template arguments, instead of inferring them. template <typename... Args> - static DERIVED_TYPE* initParamSingleton_(Args&&... args) + static LLSingletonBase* initParamSingleton_( + typename boost::call_traits<Args>::param_type... args) { // In case racing threads both call initParamSingleton() at the same // time, serialize them. One should initialize; the other should see @@ -660,7 +677,7 @@ private: // on the main thread, simply construct instance while holding lock super::logdebugs({super::template classname<DERIVED_TYPE>(), "::initParamSingleton()"}); - super::constructSingleton(lk, std::forward<Args>(args)...); + super::constructSingleton(lk, args...); return lk->mInstance; } else @@ -673,20 +690,19 @@ private: lk.unlock(); super::loginfos({super::template classname<DERIVED_TYPE>(), "::initParamSingleton() dispatching to main thread"}); - // Normally it would be the height of folly to reference-bind - // 'args' into a lambda to be executed on some other thread! By - // the time that thread executed the lambda, the references would - // all be dangling, and Bad Things would result. But - // LLMainThreadTask::dispatch() promises to block until the passed - // task has completed. So in this case we know the references will - // remain valid until the lambda has run, so we dare to bind - // references. - auto instance = LLMainThreadTask::dispatch( - [&](){ - super::loginfos({super::template classname<DERIVED_TYPE>(), - "::initParamSingleton() on main thread"}); - return initParamSingleton_(std::forward<Args>(args)...); - }); + auto instance = static_cast<DERIVED_TYPE*>( + super::getInstanceForSecondaryThread( + super::template classname<DERIVED_TYPE>(), + "initParamSingleton()", + // This lambda does what std::bind() is supposed to do -- + // but when the actual parameter is (e.g.) a string + // literal, type inference makes it fail. Apply param_type + // to each incoming type to make it work. + [args=std::tuple<typename boost::call_traits<Args>::param_type...>(args...)] + () + { + return LL::apply(initParamSingleton_<Args...>, args); + })); super::loginfos({super::template classname<DERIVED_TYPE>(), "::initParamSingleton() returning on requesting thread"}); return instance; @@ -703,7 +719,7 @@ public: template <typename... Args> static DERIVED_TYPE& initParamSingleton(Args&&... args) { - return *initParamSingleton_(std::forward<Args>(args)...); + return *static_cast<DERIVED_TYPE*>(initParamSingleton_<Args...>(args...)); } static DERIVED_TYPE* getInstance() diff --git a/indra/llcommon/llstring.h b/indra/llcommon/llstring.h index db716b1431..e4be1efaed 100644 --- a/indra/llcommon/llstring.h +++ b/indra/llcommon/llstring.h @@ -38,7 +38,9 @@ #include <algorithm> #include <vector> #include <map> +#include <type_traits> #include "llformat.h" +#include "stdtypes.h" #if LL_LINUX #include <wctype.h> @@ -313,6 +315,14 @@ public: static void trim(string_type& string) { trimHead(string); trimTail(string); } static void truncate(string_type& string, size_type count); + // if string startsWith prefix, remove it and return true + static bool removePrefix(string_type& string, const string_type& prefix); + // if string startsWith prefix, return (string without prefix, true), else (string, false) + static std::pair<string_type, bool> withoutPrefix(const string_type& string, const string_type& prefix); + // like removePrefix() + static bool removeSuffix(string_type& string, const string_type& suffix); + static std::pair<string_type, bool> withoutSuffix(const string_type& string, const string_type& suffix); + static void toUpper(string_type& string); static void toLower(string_type& string); @@ -521,11 +531,38 @@ struct ll_convert_impl TO operator()(const FROM& in) const; }; -// Use a function template to get the nice ll_convert<TO>(from_value) API. +/** + * somefunction(ll_convert(data)) + * target = ll_convert(data) + * totype otherfunc(const fromtype& data) + * { + * // ... + * return ll_convert(data); + * } + * all infer both the FROM type and the TO type. + */ +template <typename FROM> +class ll_convert +{ +private: + const FROM& mRef; + +public: + ll_convert(const FROM& ref): mRef(ref) {} + + template <typename TO> + inline operator TO() const + { + return ll_convert_impl<TO, std::decay_t<const FROM>>()(mRef); + } +}; + +// When the TO type must be explicit, use a function template to get +// ll_convert_to<TO>(from_value) API. template<typename TO, typename FROM> -TO ll_convert(const FROM& in) +TO ll_convert_to(const FROM& in) { - return ll_convert_impl<TO, FROM>()(in); + return ll_convert_impl<TO, std::decay_t<const FROM>>()(in); } // degenerate case @@ -579,8 +616,8 @@ inline size_t ll_convert_length<char> (const char* zstr) { return std::strl // and longname(const string&, len) so calls written pre-ll_convert() will // work. Most of these overloads will be unified once we turn on C++17 and can // use std::string_view. -// It also uses aliasmacro to ensure that both ll_convert<OUTSTR>(const char*) -// and ll_convert<OUTSTR>(const string&) will work. +// It also uses aliasmacro to ensure that both ll_convert(const char*) +// and ll_convert(const string&) will work. #define ll_convert_forms(aliasmacro, OUTSTR, INSTR, longname) \ LL_COMMON_API OUTSTR longname(const INSTR::value_type* in, size_t len); \ inline auto longname(const INSTR& in, size_t len) \ @@ -823,7 +860,7 @@ LL_COMMON_API std::string ll_convert_string_to_utf8_string(const std::string& in template<typename STRING> STRING windows_message(unsigned long error) { - return ll_convert<STRING>(windows_message<std::wstring>(error)); + return ll_convert(windows_message<std::wstring>(error)); } /// There's only one real implementation @@ -1467,6 +1504,60 @@ void LLStringUtilBase<T>::trimTail(string_type& string) } } +// if string startsWith prefix, remove it and return true +template<class T> +bool LLStringUtilBase<T>::removePrefix(string_type& string, const string_type& prefix) +{ + bool found{ startsWith(string, prefix) }; + if (found) + { + string.erase(0, prefix.length()); + } + return found; +} + +// if string startsWith prefix, return (string without prefix, true), else (string, false) +template<class T> +std::pair<typename LLStringUtilBase<T>::string_type, bool> +LLStringUtilBase<T>::withoutPrefix(const string_type& string, const string_type& prefix) +{ + bool found{ startsWith(string, prefix) }; + if (! found) + { + return { string, false }; + } + else + { + return { string.substr(prefix.length()), true }; + } +} + +// like removePrefix() +template<class T> +bool LLStringUtilBase<T>::removeSuffix(string_type& string, const string_type& suffix) +{ + bool found{ endsWith(string, suffix) }; + if (found) + { + string.erase(string.length() - suffix.length()); + } + return found; +} + +template<class T> +std::pair<typename LLStringUtilBase<T>::string_type, bool> +LLStringUtilBase<T>::withoutSuffix(const string_type& string, const string_type& suffix) +{ + bool found{ endsWith(string, suffix) }; + if (! found) + { + return { string, false }; + } + else + { + return { string.substr(0, string.length() - suffix.length()), true }; + } +} // Replace line feeds with carriage return-line feed pairs. //static @@ -1835,7 +1926,7 @@ auto LLStringUtilBase<T>::getoptenv(const std::string& key) -> std::optional<str if (found) { // return populated std::optional - return { ll_convert<string_type>(*found) }; + return { ll_convert_to<string_type>(*found) }; } else { diff --git a/indra/llcommon/lockstatic.cpp b/indra/llcommon/lockstatic.cpp new file mode 100755 index 0000000000..f531ef331e --- /dev/null +++ b/indra/llcommon/lockstatic.cpp @@ -0,0 +1,26 @@ +/** + * @file lockstatic.cpp + * @author Nat Goodspeed + * @date 2024-05-23 + * @brief Implementation for lockstatic. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "lockstatic.h" +// STL headers +// std headers +// external library headers +// other Linden headers +#include "llerror.h" +#include "stringize.h" + +void llthread::LockStaticBase::throwDead(const char* mangled) +{ + LLTHROW(Dead(stringize(LLError::Log::demangle(mangled), " called after cleanup()"))); +} diff --git a/indra/llcommon/lockstatic.h b/indra/llcommon/lockstatic.h index 7cc9b7eec0..e83957b1fd 100644 --- a/indra/llcommon/lockstatic.h +++ b/indra/llcommon/lockstatic.h @@ -14,21 +14,36 @@ #define LL_LOCKSTATIC_H #include "mutex.h" // std::unique_lock +#include "llexception.h" +#include <typeinfo> namespace llthread { +class LockStaticBase +{ +public: + // trying to lock Static after cleanup() has been called + struct Dead: public LLException + { + Dead(const std::string& what): LLException(what) {} + }; + +protected: + static void throwDead(const char* mangled); +}; + // Instantiate this template to obtain a pointer to the canonical static // instance of Static while holding a lock on that instance. Use of // Static::mMutex presumes that Static declares some suitable mMutex. template <typename Static> -class LockStatic +class LockStatic: public LockStaticBase { typedef std::unique_lock<decltype(Static::mMutex)> lock_t; public: LockStatic(): mData(getStatic()), - mLock(mData->mMutex) + mLock(getLock(mData)) {} Static* get() const { return mData; } operator Static*() const { return get(); } @@ -40,31 +55,69 @@ public: mData = nullptr; mLock.unlock(); } + // explicit destruction + // We used to store a static instance of Static in getStatic(). The + // trouble with that is that at some point during final termination + // cleanup, the compiler calls ~Static(), destroying the mutex. If some + // later static object's destructor tries to lock our Static, we blow up + // trying to lock a destroyed mutex object. This can happen, for instance, + // if some class's destructor tries to reference an LLSingleton. + // Since a plain dumb pointer has no destructor, the compiler leaves it + // alone, so the referenced heap Static instance can survive until we + // explicitly call this method. + void cleanup() + { + // certainly don't claim to lock after this point! + mData = nullptr; + Static*& ptrref{ getStatic() }; + Static* ptr{ ptrref }; + ptrref = nullptr; + delete ptr; + } protected: Static* mData; lock_t mLock; private: - Static* getStatic() + static lock_t getLock(Static* data) + { + // data can be false if cleanup() has already been called. If so, no + // code in the caller is valid that depends on this instance. We dare + // to throw an exception because trying to lock Static after it's been + // deleted is not part of normal processing. There are callers who + // want to handle this exception, but it should indeed be treated as + // exceptional. + if (! data) + { + throwDead(typeid(LockStatic<Static>).name()); + } + // Usual case: data isn't nullptr, carry on. + return lock_t(data->mMutex); + } + + Static*& getStatic() { - // Static::mMutex must be function-local static rather than class- - // static. Some of our consumers must function properly (therefore - // lock properly) even when the containing module's static variables - // have not yet been runtime-initialized. A mutex requires + // Our Static instance must be function-local static rather than + // class-static. Some of our consumers must function properly + // (therefore lock properly) even when the containing module's static + // variables have not yet been runtime-initialized. A mutex requires // construction. A static class member might not yet have been // constructed. // - // We could store a dumb mutex_t*, notice when it's NULL and allocate a - // heap mutex -- but that's vulnerable to race conditions. And we can't - // defend the dumb pointer with another mutex. + // We could store a dumb mutex_t* class member, notice when it's NULL + // and allocate a heap mutex -- but that's vulnerable to race + // conditions. And we can't defend the dumb pointer with another + // mutex. // // We could store a std::atomic<mutex_t*> -- but a default-constructed // std::atomic<T> does not contain a valid T, even a default-constructed // T! Which means std::atomic, too, requires runtime initialization. // // But a function-local static is guaranteed to be initialized exactly - // once: the first time control reaches that declaration. - static Static sData; - return &sData; + // once: the first time control reaches that declaration. Importantly, + // since a plain dumb pointer has no destructor, the compiler lets our + // heap Static instance survive until someone calls cleanup() (above). + static Static* sData{ new Static }; + return sData; } }; diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp new file mode 100644 index 0000000000..eefb1e62cf --- /dev/null +++ b/indra/llcommon/lua_function.cpp @@ -0,0 +1,1664 @@ +/** + * @file lua_function.cpp + * @author Nat Goodspeed + * @date 2024-02-05 + * @brief Implementation for lua_function. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "lua_function.h" +// STL headers +// std headers +#include <algorithm> +#include <exception> +#include <iomanip> // std::quoted +#include <map> +#include <memory> // std::unique_ptr +#include <typeinfo> +#include <unordered_map> +// external library headers +// other Linden headers +#include "commoncontrol.h" +#include "fsyspath.h" +#include "hexdump.h" +#include "llcoros.h" +#include "lleventcoro.h" +#include "llsd.h" +#include "llsdutil.h" +#include "llstring.h" +#include "lualistener.h" +#include "stringize.h" + +using namespace std::literals; // e.g. std::string_view literals: "this"sv + +const S32 INTERRUPTS_MAX_LIMIT = 100000; +const S32 INTERRUPTS_SUSPEND_LIMIT = 100; + +#define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n))) +#define lua_rawlen lua_objlen + +int DistinctInt::mValues{0}; + +/***************************************************************************** +* lluau namespace +*****************************************************************************/ +namespace +{ + // can't specify free function free() as a unique_ptr deleter + struct freer + { + void operator()(void* ptr){ free(ptr); } + }; +} // anonymous namespace + +namespace lluau +{ + +int dostring(lua_State* L, const std::string& desc, const std::string& text) +{ + auto r = loadstring(L, desc, text); + if (r != LUA_OK) + return r; + + // Push debug.traceback() onto the stack as lua_pcall()'s error + // handler function. On error, lua_pcall() calls the specified error + // handler function with the original error message; the message + // returned by the error handler is then returned by lua_pcall(). + // Luau's debug.traceback() is called with a message to prepend to the + // returned traceback string. Almost as if they'd been designed to + // work together... + lua_getglobal(L, "debug"); + lua_getfield(L, -1, "traceback"); + // ditch "debug" + lua_remove(L, -2); + // stack: compiled chunk, debug.traceback() + lua_insert(L, -2); + // stack: debug.traceback(), compiled chunk + LuaRemover cleanup(L, -2); + + // It's important to pass LUA_MULTRET as the expected number of return + // values: if we pass any fixed number, we discard any returned values + // beyond that number. + return lua_pcall(L, 0, LUA_MULTRET, -2); +} + +int loadstring(lua_State *L, const std::string &desc, const std::string &text) +{ + size_t bytecodeSize = 0; + // The char* returned by luau_compile() must be freed by calling free(). + // Use unique_ptr so the memory will be freed even if luau_load() throws. + std::unique_ptr<char[], freer> bytecode{ + luau_compile(text.data(), text.length(), nullptr, &bytecodeSize)}; + return luau_load(L, desc.data(), bytecode.get(), bytecodeSize, 0); +} + +fsyspath source_path(lua_State* L) +{ + //Luau lua_Debug and lua_getinfo() are different compared to default Lua: + //see https://github.com/luau-lang/luau/blob/80928acb92d1e4b6db16bada6d21b1fb6fa66265/VM/include/lua.h + // In particular: + // passing level=1 gets you info about the deepest function call + // passing level=lua_stackdepth() gets you info about the topmost script + // Empirically, lua_getinfo(level > 1) behaves strangely (including + // crashing the program) unless you iterate from 1 to desired level. + lua_Debug ar{}; + for (int i(0), depth(lua_stackdepth(L)); i <= depth; ++i) + { + lua_getinfo(L, i, "s", &ar); + } + return ar.source; +} + +} // namespace lluau + +/***************************************************************************** +* lua_destroyuserdata(), lua_destroybounduserdata() (see lua_emplace<T>()) +*****************************************************************************/ +int lua_destroyuserdata(lua_State* L) +{ + // stack: lua_emplace() userdata to be destroyed + if (int tag; + lua_isuserdata(L, -1) && + (tag = lua_userdatatag(L, -1)) != 0) + { + auto dtor = lua_getuserdatadtor(L, tag); + // detach this userdata from the destructor with tag 'tag' + lua_setuserdatatag(L, -1, 0); + // now run the real destructor + dtor(L, lua_touserdata(L, -1)); + } + lua_settop(L, 0); + return 0; +} + +int lua_destroybounduserdata(lua_State *L) +{ + // called with no arguments -- push bound upvalue + lluau_checkstack(L, 1); + lua_pushvalue(L, lua_upvalueindex(1)); + return lua_destroyuserdata(L); +} + +/***************************************************************************** +* Lua <=> C++ conversions +*****************************************************************************/ +std::string lua_tostdstring(lua_State* L, int index) +{ + lua_checkdelta(L); + size_t len; + const char* strval{ lua_tolstring(L, index, &len) }; + return { strval, len }; +} + +void lua_pushstdstring(lua_State* L, const std::string& str) +{ + lua_checkdelta(L, 1); + lluau_checkstack(L, 1); + lua_pushlstring(L, str.c_str(), str.length()); +} + +// By analogy with existing lua_tomumble() functions, return an LLSD object +// corresponding to the Lua object at stack index 'index' in state L. +// This function assumes that a Lua caller is fully aware that they're trying +// to call a viewer function. In other words, the caller must specifically +// construct Lua data convertible to LLSD. +// +// For proper error handling, we REQUIRE that the Lua runtime be compiled as +// C++ so errors are raised as C++ exceptions rather than as longjmp() calls: +// http://www.lua.org/manual/5.4/manual.html#4.4 +// "Internally, Lua uses the C longjmp facility to handle errors. (Lua will +// use exceptions if you compile it as C++; search for LUAI_THROW in the +// source code for details.)" +// Some blocks within this function construct temporary C++ objects in the +// expectation that these objects will be properly destroyed even if code +// reached by that block raises a Lua error. +LLSD lua_tollsd(lua_State* L, int index) +{ + lua_checkdelta(L); + switch (lua_type(L, index)) + { + case LUA_TNONE: + // Should LUA_TNONE be an error instead of returning isUndefined()? + case LUA_TNIL: + return {}; + + case LUA_TBOOLEAN: + return bool(lua_toboolean(L, index)); + + case LUA_TNUMBER: + { + // Vanilla Lua supports lua_tointegerx(), which tells the caller + // whether the number at the specified stack index is or is not an + // integer. Apparently the function exists but does not work right in + // Luau: it reports even non-integer numbers as integers. + // Instead, check if integer truncation leaves the number intact. + lua_Number numval{ lua_tonumber(L, index) }; + lua_Integer intval{ narrow(numval) }; + if (lua_Number(intval) == numval) + { + return LLSD::Integer(intval); + } + else + { + return numval; + } + } + + case LUA_TSTRING: + return lua_tostdstring(L, index); + + case LUA_TUSERDATA: + { + LLSD::Binary binary(lua_rawlen(L, index)); + std::memcpy(binary.data(), lua_touserdata(L, index), binary.size()); + return binary; + } + + case LUA_TTABLE: + { + // A Lua table correctly constructed to convert to LLSD will have + // either consecutive integer keys starting at 1, which we represent + // as an LLSD array (with Lua key 1 at C++ index 0), or will have + // all string keys. + // + // In the belief that Lua table traversal skips "holes," that is, it + // doesn't report any key/value pair whose value is nil, we allow a + // table with integer keys >= 1 but with "holes." This produces an + // LLSD array with isUndefined() entries at unspecified keys. There + // would be no other way for a Lua caller to construct an + // isUndefined() LLSD array entry. However, to guard against crazy int + // keys, we forbid gaps larger than a certain size: crazy int keys + // could result in a crazy large contiguous LLSD array. + // + // Possible looseness could include: + // - A mix of integer and string keys could produce an LLSD map in + // which the integer keys are converted to string. (Key conversion + // must be performed in C++, not Lua, to avoid confusing + // lua_next().) + // - However, since in Lua t[0] and t["0"] are distinct table entries, + // do not consider converting numeric string keys to int to return + // an LLSD array. + // But until we get more experience with actual Lua scripts in + // practice, let's say that any deviation is a Lua coding error. + // An important property of the strict definition above is that most + // conforming data blobs can make a round trip across the language + // boundary and still compare equal. A non-conforming data blob would + // lose that property. + // Known exceptions to round trip identity: + // - Empty LLSD map and empty LLSD array convert to empty Lua table. + // But empty Lua table converts to isUndefined() LLSD object. + // - LLSD::Real with integer value returns as LLSD::Integer. + // - LLSD::UUID, LLSD::Date and LLSD::URI all convert to Lua string, + // and so return as LLSD::String. + // - Lua does not store any table key whose value is nil. An LLSD + // array with isUndefined() entries produces a Lua table with + // "holes" in the int key sequence; this converts back to an LLSD + // array containing corresponding isUndefined() entries -- except + // when one or more of the final entries isUndefined(). These are + // simply dropped, producing a shorter LLSD array than the original. + // - For the same reason, any keys in an LLSD map whose value + // isUndefined() are simply discarded in the converted Lua table. + // This converts back to an LLSD map lacking those keys. + // - If it's important to preserve the original length of an LLSD + // array whose final entries are undefined, or the full set of keys + // for an LLSD map some of whose values are undefined, store an + // LLSD::emptyArray() or emptyMap() instead. These will be + // represented in Lua as empty table, which should convert back to + // undefined LLSD. Naturally, though, those won't survive a second + // round trip. + + // This is the most important of the lluau_checkstack() calls because a + // deeply nested Lua structure will enter this case at each level, and + // we'll need another 2 stack slots to traverse each nested table. + lluau_checkstack(L, 2); + // BEFORE we push nil to initialize the lua_next() traversal, convert + // 'index' to absolute! Our caller might have passed a relative index; + // we do, below: lua_tollsd(L, -1). If 'index' is -1, then when we + // push nil, what we find at index -1 is nil, not the table! + index = lua_absindex(L, index); + lua_pushnil(L); // first key + if (! lua_next(L, index)) + { + // it's a table, but the table is empty -- no idea if it should be + // modeled as empty array or empty map -- return isUndefined(), + // which can be consumed as either + return {}; + } + // key is at stack index -2, value at index -1 + // from here until lua_next() returns 0, have to lua_pop(2) if we + // return early + LuaPopper popper(L, 2); + // Remember the type of the first key + auto firstkeytype{ lua_type(L, -2) }; + switch (firstkeytype) + { + case LUA_TNUMBER: + { + // First Lua key is a number: try to convert table to LLSD array. + // This is tricky because we don't know in advance the size of the + // array. The Lua reference manual says that lua_rawlen() is the + // same as the length operator '#'; but the length operator states + // that it might stop at any "hole" in the subject table. + // Moreover, the Lua next() function (and presumably lua_next()) + // traverses a table in unspecified order, even for numeric keys + // (emphasized in the doc). + // Make a preliminary pass over the whole table to validate and to + // collect keys. + std::vector<LLSD::Integer> keys; + // Try to determine the length of the table. If the length + // operator is truthful, avoid allocations while we grow the keys + // vector. Even if it's not, we can still grow the vector, albeit + // a little less efficiently. + keys.reserve(lua_objlen(L, index)); + do + { + auto arraykeytype{ lua_type(L, -2) }; + switch (arraykeytype) + { + case LUA_TNUMBER: + { + int isint; + lua_Integer intkey{ lua_tointegerx(L, -2, &isint) }; + if (! isint) + { + // key isn't an integer - this doesn't fit our LLSD + // array constraints + return lluau::error(L, "Expected integer array key, got %f instead", + lua_tonumber(L, -2)); + } + if (intkey < 1) + { + return lluau::error(L, "array key %d out of bounds", int(intkey)); + } + + keys.push_back(LLSD::Integer(intkey)); + break; + } + + case LUA_TSTRING: + // break out strings specially to report the value + return lluau::error(L, "Cannot convert string array key '%s' to LLSD", + lua_tostring(L, -2)); + + default: + return lluau::error(L, "Cannot convert %s array key to LLSD", + lua_typename(L, arraykeytype)); + } + + // remove value, keep key for next iteration + lua_pop(L, 1); + } while (lua_next(L, index) != 0); + popper.disarm(); + // Table keys are all integers: are they reasonable integers? + // Arbitrary max: may bite us, but more likely to protect us + const size_t array_max{ 10000 }; + if (keys.size() > array_max) + { + return lluau::error(L, "Conversion from Lua to LLSD array limited to %d entries", + int(array_max)); + } + // We know the smallest key is >= 1. Check the largest. We also + // know the vector is NOT empty, else we wouldn't have gotten here. + std::sort(keys.begin(), keys.end()); + LLSD::Integer highkey = *keys.rbegin(); + if ((highkey - LLSD::Integer(keys.size())) > 100) + { + // Looks like we've gone beyond intentional array gaps into + // crazy key territory. + return lluau::error(L, "Gaps in Lua table too large for conversion to LLSD array"); + } + // right away expand the result array to the size we'll need + LLSD result{ LLSD::emptyArray() }; + result[highkey - 1] = LLSD(); + // Traverse the table again, and this time populate result array. + lua_pushnil(L); // first key + while (lua_next(L, index)) + { + // key at stack index -2, value at index -1 + // We've already validated lua_tointegerx() for each key. + auto key{ lua_tointeger(L, -2) }; + // Don't forget to subtract 1 from Lua key for LLSD subscript! + result[LLSD::Integer(key) - 1] = lua_tollsd(L, -1); + // remove value, keep key for next iteration + lua_pop(L, 1); + } + return result; + } + + case LUA_TSTRING: + { + // First Lua key is a string: try to convert table to LLSD map + LLSD result{ LLSD::emptyMap() }; + do + { + auto mapkeytype{ lua_type(L, -2) }; + if (mapkeytype != LUA_TSTRING) + { + return lluau::error(L, "Cannot convert %s map key to LLSD", + lua_typename(L, mapkeytype)); + } + + auto key{ lua_tostdstring(L, -2) }; + result[key] = lua_tollsd(L, -1); + // remove value, keep key for next iteration + lua_pop(L, 1); + } while (lua_next(L, index) != 0); + popper.disarm(); + return result; + } + + default: + // First Lua key isn't number or string: sorry + return lluau::error(L, "Cannot convert %s table key to LLSD", + lua_typename(L, firstkeytype)); + } + } + + default: + // Other Lua entities (e.g. function, C function, light userdata, + // thread, userdata) are not convertible to LLSD, indicating a coding + // error in the caller. + return lluau::error(L, "Cannot convert type %s to LLSD", luaL_typename(L, index)); + } +} + +// By analogy with existing lua_pushmumble() functions, push onto state L's +// stack a Lua object corresponding to the passed LLSD object. +void lua_pushllsd(lua_State* L, const LLSD& data) +{ + lua_checkdelta(L, 1); + // might need 2 slots for array or map + lluau_checkstack(L, 2); + switch (data.type()) + { + case LLSD::TypeUndefined: + lua_pushnil(L); + break; + + case LLSD::TypeBoolean: + lua_pushboolean(L, data.asBoolean()); + break; + + case LLSD::TypeInteger: + lua_pushinteger(L, data.asInteger()); + break; + + case LLSD::TypeReal: + lua_pushnumber(L, data.asReal()); + break; + + case LLSD::TypeBinary: + { + auto binary{ data.asBinary() }; + std::memcpy(lua_newuserdata(L, binary.size()), + binary.data(), binary.size()); + break; + } + + case LLSD::TypeMap: + { + // push a new table with space for our non-array keys + lua_createtable(L, 0, narrow(data.size())); + for (const auto& pair: llsd::inMap(data)) + { + // push value -- so now table is at -2, value at -1 + lua_pushllsd(L, pair.second); + // pop value, assign to table[key] + lua_setfield(L, -2, pair.first.c_str()); + } + break; + } + + case LLSD::TypeArray: + { + // push a new table with space for array entries + lua_createtable(L, narrow(data.size()), 0); + lua_Integer key{ 0 }; + for (const auto& item: llsd::inArray(data)) + { + // push new array value: table at -2, value at -1 + lua_pushllsd(L, item); + // pop value, assign table[key] = value + lua_rawseti(L, -2, ++key); + } + break; + } + + case LLSD::TypeString: + case LLSD::TypeUUID: + case LLSD::TypeDate: + case LLSD::TypeURI: + default: + { + lua_pushstdstring(L, data.asString()); + break; + } + } +} + +/***************************************************************************** +* LuaState class +*****************************************************************************/ +namespace +{ + +// If we find we're running Lua scripts from more than one thread, sLuaStateMap +// should be thread_local. Until then, avoid the overhead. +using LuaStateMap = std::unordered_map<lua_State*, LuaState*>; +static LuaStateMap sLuaStateMap; + +// replace table-at-index[name] with passed func, +// binding the original table-at-index[name] as func's upvalue +void replace_entry(lua_State* L, int index, + const std::string& name, lua_CFunction func); + +// replacement next() function that understands setdtor() proxy args +int lua_proxydrill(lua_State* L); +// replacement pairs() function that supports __iter() metamethod +int lua_metapairs(lua_State* L); +// replacement ipairs() function that supports __index() metamethod +int lua_metaipairs(lua_State* L); +// helper for lua_metaipairs() (actual generator function) +int lua_metaipair(lua_State* L); + +} // anonymous namespace + +LuaState::LuaState(script_finished_fn cb): + mCallback(cb) +{ + /*---------------------------- feature flag ----------------------------*/ + try + { + mFeature = LL::CommonControl::get("Global", "LuaFeature").asBoolean(); + } + catch (const LL::CommonControl::NoListener&) + { + // If this program doesn't have an LLViewerControlListener, + // it's probably a test program; go ahead. + mFeature = true; + } + catch (const LL::CommonControl::ParamError&) + { + // We found LLViewerControlListener, but its settings do not include + // "LuaFeature". Hmm, fishy: that feature flag was introduced at the + // same time as this code. + mFeature = false; + } + // None of the rest of this is necessary if we're not going to run anything. + if (! mFeature) + { + mError = "Lua feature disabled"; + return; + } + /*---------------------------- feature flag ----------------------------*/ + + mState = luaL_newstate(); + // Ensure that we can always find this LuaState instance, given the + // lua_State we just created or any of its coroutines. + sLuaStateMap.emplace(mState, this); + luaL_openlibs(mState); + // publish to this new lua_State all the LL entry points we defined using + // the lua_function() macro + LuaFunction::init(mState); + // Try to make print() write to our log. + lua_register(mState, "print", LuaFunction::get("print_info")); + // We don't want to have to prefix require(). + lua_register(mState, "require", LuaFunction::get("require")); + + // Replace certain key global functions so they understand our + // LL.setdtor() proxy objects. + // (We could also do this for selected library functions as well, + // e.g. the table, string, math libraries... let's see if needed.) + replace_entry(mState, LUA_GLOBALSINDEX, "next", lua_proxydrill); + // Replacing pairs() with lua_metapairs() makes global pairs() honor + // objects with __iter() metamethods. + replace_entry(mState, LUA_GLOBALSINDEX, "pairs", lua_metapairs); + // Replacing ipairs() with lua_metaipairs() makes global ipairs() honor + // objects with __index() metamethods -- as long as the object in question + // has no array entries (int keys) of its own. (If it does, object[i] will + // retrieve table[i] instead of calling __index(table, i).) + replace_entry(mState, LUA_GLOBALSINDEX, "ipairs", lua_metaipairs); +} + +namespace +{ + +void replace_entry(lua_State* L, int index, + const std::string& name, lua_CFunction func) +{ + index = lua_absindex(L, index); + lua_checkdelta(L); + // push the function's name string twice + lua_pushlstring(L, name.data(), name.length()); + lua_pushvalue(L, -1); + // stack: name, name + // look up the existing table entry + lua_rawget(L, index); + // stack: name, original function + // bind original function as the upvalue for func() + lua_pushcclosure(L, func, (name + "()").c_str(), 1); + // stack: name, func-with-bound-original + // table[name] = func-with-bound-original + lua_rawset(L, index); +} + +int lua_metapairs(lua_State* L) +{ +// LuaLog debug(L, "lua_metapairs()"); + // pairs(obj): object is at index 1 + // How many args were we passed? + int args = lua_gettop(L); + // stack: obj, ... + if (luaL_getmetafield(L, 1, "__iter")) + { + // stack: obj, ..., getmetatable(obj).__iter + } + else + { + // Push the original pairs() function, captured as our upvalue. + lua_pushvalue(L, lua_upvalueindex(1)); + // stack: obj, ..., original pairs() + } + lua_insert(L, 1); + // stack: (__iter() or pairs()), obj, ... + // call whichever function(obj, ...) (args args, up to 3 return values) + lua_call(L, args, LUA_MULTRET); + // return as many values as the selected function returned + return lua_gettop(L); +} + +int lua_metaipairs(lua_State* L) +{ +// LuaLog debug(L, "lua_metaipairs()"); + // ipairs(obj): object is at index 1 + // How many args were we passed? + int args = lua_gettop(L); + // stack: obj, ... + if (luaL_getmetafield(L, 1, "__index")) + { + // stack: obj, ..., getmetatable(obj).__index + // discard __index and everything but obj: + // we don't want to call __index(), just check its presence + lua_settop(L, 1); + // stack: obj + lua_pushcfunction(L, lua_metaipair, "lua_metaipair"); + // stack: obj, lua_metaipair + lua_insert(L, 1); + // stack: lua_metaipair, obj + // push explicit 0 so lua_metaipair need not special-case nil + lua_pushinteger(L, 0); + // stack: lua_metaipair, obj, 0 + return 3; + } + else // no __index() metamethod + { + // Although our lua_metaipair() function demonstrably works whether or + // not our object has an __index() metamethod, the code below assumes + // that the Lua engine may have a more efficient implementation for + // built-in ipairs() than our lua_metaipair(). + // Push the original ipairs() function, captured as our upvalue. + lua_pushvalue(L, lua_upvalueindex(1)); + // stack: obj, ..., original ipairs() + // Shift the stack so the original function is first. + lua_insert(L, 1); + // stack: original ipairs(), obj, ... + // Call original ipairs() with all original args, no error checking. + // Don't truncate however many values that function returns. + lua_call(L, args, LUA_MULTRET); + // Return as many values as the original function returned. + return lua_gettop(L); + } +} + +int lua_metaipair(lua_State* L) +{ +// LuaLog debug(L, "lua_metaipair()"); + // called with (obj, previous-index) + // increment previous-index for this call + lua_Integer i = luaL_optinteger(L, 2, 0) + 1; + lua_pop(L, 1); + // stack: obj + lua_pushinteger(L, i); + // stack: obj, i + lua_pushvalue(L, -1); + // stack: obj, i, i + lua_insert(L, 1); + // stack: i, obj, i + lua_gettable(L, -2); + // stack: i, obj, obj[i] (honoring __index()) + lua_remove(L, -2); + // stack: i, obj[i] + if (! lua_isnil(L, -1)) + { + // great, obj[i] isn't nil: return (i, obj[i]) + return 2; + } + // obj[i] is nil. ipairs() is documented to stop at the first hole, + // regardless of #obj. Clear the stack, i.e. return nil. + lua_settop(L, 0); + return 0; +} + +} // anonymous namespace + +LuaState::~LuaState() +{ + /*---------------------------- feature flag ----------------------------*/ + if (mFeature) + /*---------------------------- feature flag ----------------------------*/ + { + // We're just about to destroy this lua_State mState. Did this Lua chunk + // register any atexit() functions? + lluau_checkstack(mState, 3); + // look up Registry.atexit + lua_getfield(mState, LUA_REGISTRYINDEX, "atexit"); + // stack contains Registry.atexit + if (lua_istable(mState, -1)) + { + // We happen to know that Registry.atexit is built by appending array + // entries using table.insert(). That's important because it means + // there are no holes, and therefore lua_objlen() should be correct. + // That's important because we walk the atexit table backwards, to + // destroy last the things we created (passed to LL.atexit()) first. + int len(lua_objlen(mState, -1)); + LL_DEBUGS("Lua") << LLCoros::getName() << ": Registry.atexit is a table with " + << len << " entries" << LL_ENDL; + + // Push debug.traceback() onto the stack as lua_pcall()'s error + // handler function. On error, lua_pcall() calls the specified error + // handler function with the original error message; the message + // returned by the error handler is then returned by lua_pcall(). + // Luau's debug.traceback() is called with a message to prepend to the + // returned traceback string. Almost as if they'd been designed to + // work together... + lua_getglobal(mState, "debug"); + lua_getfield(mState, -1, "traceback"); + // ditch "debug" + lua_remove(mState, -2); + // stack now contains atexit, debug.traceback() + + for (int i(len); i >= 1; --i) + { + lua_pushinteger(mState, i); + // stack contains Registry.atexit, debug.traceback(), i + lua_gettable(mState, -3); + // stack contains Registry.atexit, debug.traceback(), atexit[i] + // Call atexit[i](), no args, no return values. + // Use lua_pcall() because errors in any one atexit() function + // shouldn't cancel the rest of them. Pass debug.traceback() as + // the error handler function. + LL_DEBUGS("Lua") << LLCoros::getName() + << ": calling atexit(" << i << ")" << LL_ENDL; + if (lua_pcall(mState, 0, 0, -2) != LUA_OK) + { + auto error{ lua_tostdstring(mState, -1) }; + LL_WARNS("Lua") << LLCoros::getName() + << ": atexit(" << i << ") error: " << error << LL_ENDL; + // pop error message + lua_pop(mState, 1); + } + LL_DEBUGS("Lua") << LLCoros::getName() << ": atexit(" << i << ") done" << LL_ENDL; + // lua_pcall() has already popped atexit[i]: + // stack contains atexit, debug.traceback() + } + // pop debug.traceback() + lua_pop(mState, 1); + } + // pop Registry.atexit (either table or nil) + lua_pop(mState, 1); + + // with the demise of this LuaState, remove sLuaStateMap entry + sLuaStateMap.erase(mState); + + lua_close(mState); + } + + if (mCallback) + { + // mError potentially set by previous checkLua() call(s) + mCallback(mError); + } +} + +bool LuaState::checkLua(const std::string& desc, int r) +{ + if (r != LUA_OK) + { + mError = lua_tostring(mState, -1); + lua_pop(mState, 1); + + LL_WARNS("Lua") << desc << ": " << mError << LL_ENDL; + return false; + } + return true; +} + +std::pair<int, LLSD> LuaState::expr(const std::string& desc, const std::string& text) +{ + /*---------------------------- feature flag ----------------------------*/ + if (! mFeature) + { + // fake an error + return { -1, stringize("Not running ", desc) }; + } + /*---------------------------- feature flag ----------------------------*/ + + set_interrupts_counter(0); + + lua_callbacks(mState)->interrupt = [](lua_State *L, int gc) + { + // skip if we're interrupting only for garbage collection + if (gc >= 0) + return; + + LLCoros::checkStop(); + LuaState::getParent(L).check_interrupts_counter(); + }; + + LL_INFOS("Lua") << desc << " run" << LL_ENDL; + if (! checkLua(desc, lluau::dostring(mState, desc, text))) + { + LL_WARNS("Lua") << desc << " error: " << mError << LL_ENDL; + return { -1, mError }; + } + + // here we believe there was no error -- did the Lua fragment leave + // anything on the stack? + std::pair<int, LLSD> result{ lua_gettop(mState), {} }; + LL_INFOS("Lua") << desc << " done, " << result.first << " results." << LL_ENDL; + if (result.first) + { + // aha, at least one entry on the stack! + if (result.first == 1) + { + // Don't forget that lua_tollsd() can throw Lua errors. + try + { + result.second = lua_tollsd(mState, 1); + } + catch (const std::exception& error) + { + LL_WARNS("Lua") << desc << " error converting result: " << error.what() << LL_ENDL; + // lua_tollsd() is designed to be called from a lua_function(), + // that is, from a C++ function called by Lua. In case of error, + // it throws a Lua error to be caught by the Lua runtime. expr() + // is a peculiar use case in which our C++ code is calling + // lua_tollsd() after return from the Lua runtime. We must catch + // the exception thrown for a Lua error, else it will propagate + // out to the main coroutine and terminate the viewer -- but since + // we instead of the Lua runtime catch it, our lua_State retains + // its internal error status. Any subsequent lua_pcall() calls + // with this lua_State will report error regardless of whether the + // chunk runs successfully. + return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) }; + } + } + else + { + // multiple entries on the stack + int index; + try + { + for (index = 1; index <= result.first; ++index) + { + result.second.append(lua_tollsd(mState, index)); + } + } + catch (const std::exception& error) + { + LL_WARNS("Lua") << desc << " error converting result " << index << ": " + << error.what() << LL_ENDL; + // see above comments regarding lua_State's error status + return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) }; + } + } + } + // pop everything + lua_settop(mState, 0); + return result; +} + +// We think we don't need mFeature tests in the rest of these LuaState methods +// because, if expr() isn't running code, nobody should be calling any of them. + +LuaListener& LuaState::obtainListener(lua_State* L) +{ + lluau_checkstack(L, 2); + lua_getfield(L, LUA_REGISTRYINDEX, "LuaListener"); + // compare lua_type() because lua_isuserdata() also accepts light userdata + if (lua_type(L, -1) != LUA_TUSERDATA) + { + llassert(lua_type(L, -1) == LUA_TNIL); + lua_pop(L, 1); + // push a userdata containing new LuaListener, binding L + lua_emplace<LuaListener>(L, L); + // duplicate the top stack entry so we can store one copy + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, "LuaListener"); + } + // At this point, one way or the other, the stack top should be (a Lua + // userdata containing) our LuaListener. + LuaListener* listener{ lua_toclass<LuaListener>(L, -1) }; + // Since our LuaListener instance is stored in the Registry, it won't be + // garbage collected: it will be destroyed only when lua_close() clears + // out the Registry. That's why we dare pop the userdata value off the + // stack while still depending on a pointer into its data. + lua_pop(L, 1); + return *listener; +} + +LuaState& LuaState::getParent(lua_State* L) +{ + // Look up the LuaState instance associated with the *script*, not the + // specific Lua *coroutine*. In other words, first find this lua_State's + // main thread. + auto found{ sLuaStateMap.find(lua_mainthread(L)) }; + // Our constructor creates the map entry, our destructor deletes it. As + // long as the LuaState exists, we should be able to find it. And we + // SHOULD only be talking to a lua_State managed by a LuaState instance. + llassert(found != sLuaStateMap.end()); + return *found->second; +} + +void LuaState::set_interrupts_counter(S32 counter) +{ + mInterrupts = counter; +} + +void LuaState::check_interrupts_counter() +{ + // The official way to manage data associated with a lua_State is to store + // it *as* Lua data within the lua_State. But this method is called by the + // Lua engine via lua_callbacks(L)->interrupt, and empirically we've hit + // mysterious Lua data stack overflows trying to use stack-based Lua data + // access functions in that situation. It seems the Lua engine is capable + // of interrupting itself at a moment when re-entry is not valid. So only + // touch data in this LuaState. + ++mInterrupts; + if (mInterrupts > INTERRUPTS_MAX_LIMIT) + { + lluau::error(mState, "Possible infinite loop, terminated."); + } + else if (mInterrupts % INTERRUPTS_SUSPEND_LIMIT == 0) + { + LL_DEBUGS("Lua") << LLCoros::getName() << " suspending at " << mInterrupts + << " interrupts" << LL_ENDL; + llcoro::suspend(); + } +} + +/***************************************************************************** +* atexit() +*****************************************************************************/ +lua_function(atexit, "atexit(function): " + "register Lua function to be called at script termination") +{ + lua_checkdelta(L, -1); + lluau_checkstack(L, 4); + // look up the global name "table" + lua_getglobal(L, "table"); + // stack contains function, table + // look up table.insert + lua_getfield(L, -1, "insert"); + // stack contains function, table, table.insert + // ditch table + lua_replace(L, -2); + // stack contains function, table.insert + // find or create the "atexit" table in the Registry + luaL_newmetatable(L, "atexit"); + // stack contains function, table.insert, Registry.atexit + // we were called with a Lua function to append to that Registry.atexit + // table -- push function + lua_pushvalue(L, 1); // or -3 + // stack contains function, table.insert, Registry.atexit, function + // call table.insert(Registry.atexit, function) + // don't use pcall(): if there's an error, let it propagate + lua_call(L, 2, 0); + // stack contains function -- pop everything + lua_settop(L, 0); + return 0; +} + +/***************************************************************************** +* LuaPopper class +*****************************************************************************/ +LuaPopper::~LuaPopper() +{ + if (mCount) + { + lua_pop(mState, mCount); + } +} + +/***************************************************************************** +* LuaFunction class +*****************************************************************************/ +LuaFunction::LuaFunction(const std::string_view& name, lua_CFunction function, + const std::string_view& helptext) +{ + const auto& [registry, lookup] = getState(); + registry.emplace(name, Registry::mapped_type{ function, helptext }); + lookup.emplace(function, name); +} + +void LuaFunction::init(lua_State* L) +{ + const auto& [registry, lookup] = getRState(); + lluau_checkstack(L, 2); + // create LL table -- + // it happens that we know exactly how many non-array members we want + lua_createtable(L, 0, int(narrow(lookup.size()))); + int idx = lua_gettop(L); + for (const auto& [name, pair]: registry) + { + const auto& [funcptr, helptext] = pair; + // store funcptr in LL table with saved name + lua_pushcfunction(L, funcptr, name.c_str()); + lua_setfield(L, idx, name.c_str()); + } + // store LL in new lua_State's globals + lua_setglobal(L, "LL"); +} + +lua_CFunction LuaFunction::get(const std::string& key) +{ + // use find() instead of subscripting to avoid creating an entry for + // unknown key + const auto& [registry, lookup] = getState(); + auto found{ registry.find(key) }; + return (found == registry.end())? nullptr : found->second.first; +} + +std::pair<LuaFunction::Registry&, LuaFunction::Lookup&> LuaFunction::getState() +{ + // use function-local statics to ensure they're initialized + static Registry registry; + static Lookup lookup; + return { registry, lookup }; +} + +/***************************************************************************** +* source_path() +*****************************************************************************/ +lua_function(source_path, "source_path(): return the source path of the running Lua script") +{ + lua_checkdelta(L, 1); + lluau_checkstack(L, 1); + lua_pushstdstring(L, lluau::source_path(L).u8string()); + return 1; +} + +/***************************************************************************** +* source_dir() +*****************************************************************************/ +lua_function(source_dir, "source_dir(): return the source directory of the running Lua script") +{ + lua_checkdelta(L, 1); + lluau_checkstack(L, 1); + lua_pushstdstring(L, lluau::source_path(L).parent_path().u8string()); + return 1; +} + +/***************************************************************************** +* abspath() +*****************************************************************************/ +lua_function(abspath, "abspath(path): " + "for given filesystem path relative to running script, return absolute path") +{ + lua_checkdelta(L); + auto path{ lua_tostdstring(L, 1) }; + lua_pop(L, 1); + lua_pushstdstring(L, (lluau::source_path(L).parent_path() / path).u8string()); + return 1; +} + +/***************************************************************************** +* check_stop() +*****************************************************************************/ +lua_function(check_stop, "check_stop(): ensure that a Lua script responds to viewer shutdown") +{ + lua_checkdelta(L); + LLCoros::checkStop(); + return 0; +} + +/***************************************************************************** +* help() +*****************************************************************************/ +lua_function(help, + "LL.help(): list viewer's Lua functions\n" + "LL.help(function): show help string for specific function") +{ + auto& luapump{ LLEventPumps::instance().obtain("lua output") }; + const auto& [registry, lookup]{ LuaFunction::getRState() }; + if (! lua_gettop(L)) + { + // no arguments passed: list all lua_functions + for (const auto& [name, pair] : registry) + { + const auto& [fptr, helptext] = pair; + luapump.post("LL." + helptext); + } + } + else + { + // arguments passed: list each of the specified lua_functions + for (int idx = 1, top = lua_gettop(L); idx <= top; ++idx) + { + std::string arg{ stringize("<unknown ", lua_typename(L, lua_type(L, idx)), ">") }; + if (lua_type(L, idx) == LUA_TSTRING) + { + arg = lua_tostdstring(L, idx); + LLStringUtil::removePrefix(arg, "LL."); + } + else if (lua_type(L, idx) == LUA_TFUNCTION) + { + // Caller passed the actual function instead of its string + // name. A Lua function is an anonymous callable object; it + // has a name only by assigment. You can't ask Lua for a + // function's name, which is why our constructor maintains a + // reverse Lookup map. + auto function{ lua_tocfunction(L, idx) }; + if (auto found = lookup.find(function); found != lookup.end()) + { + // okay, pass found name to lookup below + arg = found->second; + } + } + + if (auto found = registry.find(arg); found != registry.end()) + { + luapump.post("LL." + found->second.second); + } + else + { + luapump.post(arg + ": NOT FOUND"); + } + } + // pop all arguments + lua_settop(L, 0); + } + return 0; // void return +} + +/***************************************************************************** +* leaphelp() +*****************************************************************************/ +lua_function( + leaphelp, + "LL.leaphelp(): list viewer's LEAP APIs\n" + "LL.leaphelp(api): show help for specific api string name") +{ + LLSD request; + int top{ lua_gettop(L) }; + if (top) + { + request = llsd::map("op", "getAPI", "api", lua_tostdstring(L, 1)); + } + else + { + request = llsd::map("op", "getAPIs"); + } + // pop all args + lua_settop(L, 0); + + auto& outpump{ LLEventPumps::instance().obtain("lua output") }; + auto& listener{ LuaState::obtainListener(L) }; + LLEventStream replyPump("leaphelp", true); + // ask the LuaListener's LeapListener and suspend calling coroutine until reply + auto reply{ llcoro::postAndSuspend(request, listener.getCommandName(), replyPump, "reply") }; + reply.erase("reqid"); + + if (auto error = reply["error"]; error.isString()) + { + outpump.post(error.asString()); + return 0; + } + + if (top) + { + // caller wants a specific API + outpump.post(stringize(reply["name"].asString(), ":\n", reply["desc"].asString())); + for (const auto& opmap : llsd::inArray(reply["ops"])) + { + std::ostringstream reqstr; + auto req{ opmap["required"] }; + if (req.isArray()) + { + const char* sep = " (requires "; + for (const auto& [reqkey, reqval] : llsd::inMap(req)) + { + reqstr << sep << reqkey; + sep = ", "; + } + reqstr << ")"; + } + outpump.post(stringize("---- ", reply["key"].asString(), " == '", + opmap["name"].asString(), "'", reqstr.str(), ":\n", + opmap["desc"].asString())); + } + } + else + { + // caller wants a list of APIs + for (const auto& [name, data] : llsd::inMap(reply)) + { + outpump.post(stringize("==== ", name, ":\n", data["desc"].asString())); + } + } + return 0; // void return +} + +/***************************************************************************** +* setdtor +*****************************************************************************/ +namespace { + +// proxy userdata object returned by setdtor() +struct setdtor_refs +{ + lua_State* L; + std::string desc; + // You can't directly store a Lua object in a C++ object, but you can + // create a Lua "reference" by storing the object in the Lua Registry and + // capturing its Registry index. + int objref; + int dtorref; + + setdtor_refs(lua_State* L, const std::string& desc, int objref, int dtorref): + L(L), + desc(desc), + objref(objref), + dtorref(dtorref) + {} + setdtor_refs(const setdtor_refs&) = delete; + setdtor_refs& operator=(const setdtor_refs&) = delete; + ~setdtor_refs(); + + static void push_metatable(lua_State* L); + static std::string binop(const std::string& name, const std::string& op); + static int meta__index(lua_State* L); +}; + +} // anonymous namespace + +lua_function( + setdtor, + "setdtor(desc, obj, dtorfunc) => proxy object referencing obj and dtorfunc.\n" + "When the returned proxy object is garbage-collected, or when the script\n" + "ends, call dtorfunc(obj). String desc is logged in the error message, if any.\n" + "Use the returned proxy object (or proxy._target) like obj.\n" + "obj won't be destroyed as long as the proxy exists; it's the proxy object's\n" + "lifespan that determines when dtorfunc(obj) will be called.") +{ + if (lua_gettop(L) != 3) + { + return lluau::error(L, "setdtor(desc, obj, dtor) requires exactly 3 arguments"); + } + // called with (desc, obj, dtor), returns proxy object + lua_checkdelta(L, -2); +// lluau_checkstack(L, 0); // might get up to 3 stack entries + auto desc{ lua_tostdstring(L, 1) }; + // Get Lua "references" for each of the object and the dtor function. + int objref = lua_ref(L, 2); + int dtorref = lua_ref(L, 3); + // Having captured each of our parameters, discard them. + lua_settop(L, 0); + // Push our setdtor_refs userdata. Not only do we want to push it on L's + // stack, but setdtor_refs's constructor itself requires L. + lua_emplace<setdtor_refs>(L, L, desc, objref, dtorref); + // stack: proxy (i.e. setdtor_refs userdata) + // have to set its metatable + lua_getfield(L, LUA_REGISTRYINDEX, "setdtor_meta"); + // stack: proxy, setdtor_meta (which might be nil) + if (lua_isnil(L, -1)) + { + // discard nil + lua_pop(L, 1); + // compile and push our forwarding metatable + setdtor_refs::push_metatable(L); + // stack: proxy, metatable + // duplicate metatable to save it + lua_pushvalue(L, -1); + // stack: proxy, metatable, metable + // save metatable for future calls + lua_setfield(L, LUA_REGISTRYINDEX, "setdtor_meta"); + // stack: proxy, metatable + } + // stack: proxy, metatable + lua_setmetatable(L, -2); + // stack: proxy + // Because ~setdtor_refs() necessarily uses the Lua stack, the Registry et + // al., we can't let a setdtor_refs instance be destroyed by lua_close(): + // the Lua environment will already be partially shut down. To destroy + // this new setdtor_refs instance BEFORE lua_close(), bind it with + // lua_destroybounduserdata() and register it with LL.atexit(). + // push (the entry point for) LL.atexit() + lua_pushcfunction(L, atexit_luasub::call, "LL.atexit()"); + // stack: proxy, atexit() + lua_pushvalue(L, -2); + // stack: proxy, atexit(), proxy + int tag = lua_userdatatag(L, -1); + // We don't have a lookup table to get from an int Lua userdata tag to the + // corresponding C++ typeinfo name string. We'll introduce one if we need + // it for debugging. But for this particular call, we happen to know it's + // always a setdtor_refs object. + lua_pushcclosure(L, lua_destroybounduserdata, + stringize("lua_destroybounduserdata<", tag, ">()").c_str(), + 1); + // stack: proxy, atexit(), lua_destroybounduserdata + // call atexit(): one argument, no results, let error propagate + lua_call(L, 1, 0); + // stack: proxy + return 1; +} + +namespace { + +void setdtor_refs::push_metatable(lua_State* L) +{ + lua_checkdelta(L, 1); + lluau_checkstack(L, 1); + // Ideally we want a metatable that forwards every operation on our + // setdtor_refs userdata proxy object to the original object. But the + // published C API doesn't include (e.g.) arithmetic operations on Lua + // objects, so in fact it's easier to express the desired metatable in Lua + // than in C++. We could make setdtor() depend on an external Lua module, + // but it seems less fragile to embed the Lua source code right here. + static const std::string setdtor_meta = stringize(R"-( + -- This metatable literal doesn't define __index() because that's + -- implemented in C++. We cannot, in Lua, peek into the setdtor_refs + -- userdata object to obtain objref, nor can we fetch Registry[objref]. + -- So our C++ __index() metamethod recognizes access to '_target' as a + -- reference to Registry[objref]. + -- The rest are defined per https://www.lua.org/manual/5.1/manual.html#2.8. + -- Luau supports destructors instead of __gc metamethod -- we rely on that! + -- We don't set __mode because our proxy is not a table. Real references + -- are stored in the wrapped table, so ITS __mode is what counts. + -- Initial definition of meta omits binary metamethods so they can bind the + -- metatable itself, as explained for binop() below. + local meta = { + __unm = function(arg) + return -arg._target + end, + __len = function(arg) + return #arg._target + end, + -- Comparison metamethods __eq(), __lt() and __le() are only called + -- when both operands have the same metamethod. For our purposes, that + -- means both operands are setdtor_refs userdata objects. + __eq = function(lhs, rhs) + return (lhs._target == rhs._target) + end, + __lt = function(lhs, rhs) + return (lhs._target < rhs._target) + end, + __le = function(lhs, rhs) + return (lhs._target <= rhs._target) + end, + __newindex = function(t, key, value) + assert(key ~= '_target', + "Don't try to replace a setdtor() proxy's _target") + t._target[key] = value + end, + __call = function(func, ...) + return func._target(...) + end, + __tostring = function(arg) + -- don't fret about arg._target's __tostring metamethod, + -- if any, because built-in tostring() deals with that + return tostring(arg._target) + end, + __iter = function(arg) + local iter = (getmetatable(arg._target) or {}).__iter + if iter then + return iter(arg._target) + else + return next, arg._target + end + end + } +)-", + binop("add", "+"), + binop("sub", "-"), + binop("mul", "*"), + binop("div", "/"), + binop("idiv", "//"), + binop("mod", "%"), + binop("pow", "^"), + binop("concat", ".."), +R"-( + return meta +)-"); + // only needed for debugging binop() +// LL_DEBUGS("Lua") << setdtor_meta << LL_ENDL; + + if (lluau::dostring(L, LL_PRETTY_FUNCTION, setdtor_meta) != LUA_OK) + { + // stack: error message string + lua_error(L); + } + llassert(lua_gettop(L) > 0); + llassert(lua_type(L, -1) == LUA_TTABLE); + // stack: Lua metatable compiled from setdtor_meta source + // Inject our C++ __index metamethod. + lua_rawsetfield(L, -1, "__index"sv, &setdtor_refs::meta__index); +} + +// In the definition of setdtor_meta above, binary arithmethic and +// concatenation metamethods are a little funny in that we don't know a +// priori which operand is the userdata with our metatable: the metamethod +// can be invoked either way. So every such metamethod must check, which +// leads to lots of redundancy. Hence this helper function. Call it a Lua +// macro. +std::string setdtor_refs::binop(const std::string& name, const std::string& op) +{ + return stringize( + " meta.__", name, " = function(lhs, rhs)\n" + " if getmetatable(lhs) == meta then\n" + " return lhs._target ", op, " rhs\n" + " else\n" + " return lhs ", op, " rhs._target\n" + " end\n" + " end\n"); +} + +// setdtor_refs __index() metamethod +int setdtor_refs::meta__index(lua_State* L) +{ + // called with (setdtor_refs userdata, key), returns retrieved object + lua_checkdelta(L, -1); + lluau_checkstack(L, 2); + // stack: proxy, key + // get ptr to the C++ struct data + auto ptr = lua_toclass<setdtor_refs>(L, -2); + // meta__index() should NEVER be called with anything but setdtor_refs! + llassert(ptr); + // push the wrapped object + lua_getref(L, ptr->objref); + // stack: proxy, key, _target + // replace userdata with _target + lua_replace(L, -3); + // stack: _target, key + // Duplicate key because lua_tostring() converts number to string: + // if the key is (e.g.) 1, don't try to retrieve _target["1"]! + lua_pushvalue(L, -1); + // stack: _target, key, key + // recognize the special _target field + if (lua_tostdstring(L, -1) == "_target") + { + // okay, ditch both copies of "_target" string key + lua_pop(L, 2); + // stack: _target + } + else // any key but _target + { + // ditch stringized key + lua_pop(L, 1); + // stack: _target, key + // replace key with _target[key], invoking metamethod if any + lua_gettable(L, -2); + // stack: _target, _target[key] + // discard _target + lua_remove(L, -2); + // stack: _target[key] + } + return 1; +} + +// replacement for global next(): +// its lua_upvalueindex(1) is the original function it's replacing +int lua_proxydrill(lua_State* L) +{ + // Accept however many arguments the original function normally accepts. + // If our first arg is a userdata, check if it's a setdtor_refs proxy. + // Drill through as many levels of proxy wrapper as needed. + while (const setdtor_refs* ptr = lua_toclass<setdtor_refs>(L, 1)) + { + // push original object + lua_getref(L, ptr->objref); + // replace first argument with that + lua_replace(L, 1); + } + // We've reached a first argument that's not a setdtor() proxy. + // How many arguments were we passed, anyway? + int args = lua_gettop(L); + // Push the original function, captured as our upvalue. + lua_pushvalue(L, lua_upvalueindex(1)); + // Shift the stack so the original function is first. + lua_insert(L, 1); + // Call the original function with all original args, no error checking. + // Don't truncate however many values that function returns. + lua_call(L, args, LUA_MULTRET); + // Return as many values as the original function returned. + return lua_gettop(L); +} + +// When Lua destroys a setdtor_refs userdata object, either from garbage +// collection or from LL.atexit(lua_destroybounduserdata), it's time to keep +// its promise to call the specified Lua destructor function with the +// specified Lua object. Of course we must also delete the captured +// "references" to both objects. +setdtor_refs::~setdtor_refs() +{ + lua_checkdelta(L); + lluau_checkstack(L, 2); + // push Registry[dtorref] + lua_getref(L, dtorref); + // push Registry[objref] + lua_getref(L, objref); + // free Registry[dtorref] + lua_unref(L, dtorref); + // free Registry[objref] + lua_unref(L, objref); + // call dtor(obj): one arg, no result, no error function + int rc = lua_pcall(L, 1, 0, 0); + if (rc != LUA_OK) + { + // TODO: we don't really want to propagate the error here. + // If this setdtor_refs instance is being destroyed by + // LL.atexit(), we want to continue cleanup. If it's being + // garbage-collected, the call is completely unpredictable from + // the consuming script's point of view. But what to do about this + // error?? For now, just log it. + LL_WARNS("Lua") << LLCoros::getName() + << ": setdtor(" << std::quoted(desc) << ") error: " + << lua_tostring(L, -1) << LL_ENDL; + lua_pop(L, 1); + } +} + +} // anonymous namespace + +/***************************************************************************** +* lua_what +*****************************************************************************/ +std::ostream& operator<<(std::ostream& out, const lua_what& self) +{ + switch (lua_type(self.L, self.index)) + { + case LUA_TNONE: + // distinguish acceptable but non-valid index + out << "none"; + break; + + case LUA_TNIL: + out << "nil"; + break; + + case LUA_TBOOLEAN: + { + auto oldflags { out.flags() }; + out << std::boolalpha << lua_toboolean(self.L, self.index); + out.flags(oldflags); + break; + } + + case LUA_TNUMBER: + out << lua_tonumber(self.L, self.index); + break; + + case LUA_TSTRING: + out << std::quoted(lua_tostdstring(self.L, self.index)); + break; + + case LUA_TUSERDATA: + { + const S32 maxlen = 20; + S32 binlen{ lua_rawlen(self.L, self.index) }; + LLSD::Binary binary(std::min(maxlen, binlen)); + std::memcpy(binary.data(), lua_touserdata(self.L, self.index), binary.size()); + out << LL::hexdump(binary); + if (binlen > maxlen) + { + out << "...(" << (binlen - maxlen) << " more)"; + } + break; + } + + case LUA_TLIGHTUSERDATA: + out << lua_touserdata(self.L, self.index); + break; + + case LUA_TFUNCTION: + { + // Try for the function's name, at the cost of a few more stack + // entries. + lua_checkdelta(self.L); + lluau_checkstack(self.L, 3); + lua_getglobal(self.L, "debug"); + // stack: ..., debug + lua_getfield(self.L, -1, "info"); + // stack: ..., debug, debug.info + lua_remove(self.L, -2); + // stack: ..., debug.info + lua_pushvalue(self.L, self.index); + // stack: ..., debug.info, this function + lua_pushstring(self.L, "n"); + // stack: ..., debug.info, this function, "n" + // 2 arguments, 1 return value (or error message), no error handler + lua_pcall(self.L, 2, 1, 0); + // stack: ..., function name (or error) from debug.info() + out << "function " << lua_tostdstring(self.L, -1); + lua_pop(self.L, 1); + // stack: ... + break; + } + + default: + // anything else, don't bother trying to report value, just type + out << lua_typename(self.L, lua_type(self.L, self.index)); + break; + } + return out; +} + +/***************************************************************************** +* lua_stack +*****************************************************************************/ +std::ostream& operator<<(std::ostream& out, const lua_stack& self) +{ + out << "stack: ["; + const char* sep = ""; + for (int index = 1; index <= lua_gettop(self.L); ++index) + { + out << sep << lua_what(self.L, index); + sep = ", "; + } + out << ']'; + return out; +} + +/***************************************************************************** +* LuaStackDelta +*****************************************************************************/ +LuaStackDelta::LuaStackDelta(lua_State* L, const std::string& where, int delta): + L(L), + mWhere(where), + mDepth(lua_gettop(L)), + mDelta(delta) +{} + +LuaStackDelta::~LuaStackDelta() +{ + auto depth{ lua_gettop(L) }; + // If we're unwinding the stack due to an exception, then of course we + // can't expect the logic in the block containing this LuaStackDelta + // instance to keep its contract wrt the Lua data stack. + if (std::uncaught_exceptions() == 0 && mDepth + mDelta != depth) + { + LL_ERRS("Lua") << LLCoros::getName() << ": " << mWhere + << ": Lua stack went from " << mDepth << " to " << depth; + if (mDelta) + { + LL_CONT << ", rather than expected " << (mDepth + mDelta) << " (" << mDelta << ")"; + } + LL_ENDL; + } +} diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h new file mode 100644 index 0000000000..967d8eaba1 --- /dev/null +++ b/indra/llcommon/lua_function.h @@ -0,0 +1,616 @@ +/** + * @file lua_function.h + * @author Nat Goodspeed + * @date 2024-02-05 + * @brief Definitions useful for coding a new Luau entry point into C++ + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LUA_FUNCTION_H) +#define LL_LUA_FUNCTION_H + +#include "luau/luacode.h" +#include "luau/lua.h" +#include "luau/luaconf.h" +#include "luau/lualib.h" +#include "fsyspath.h" +#include "llerror.h" +#include "llsd.h" +#include "stringize.h" +#include <exception> // std::uncaught_exceptions() +#include <memory> // std::shared_ptr +#include <typeindex> +#include <typeinfo> +#include <unordered_map> +#include <utility> // std::pair + +class LuaListener; + +/***************************************************************************** +* lluau namespace utility functions +*****************************************************************************/ +namespace lluau +{ + // luau defines luaL_error() as void, but we want to use the Lua idiom of + // 'return error(...)'. Wrap luaL_error() in an int function. +#if __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wformat-security" +#endif // __clang__ + template<typename... Args> + int error(lua_State* L, const char* format, Args&&... args) + { + luaL_error(L, format, std::forward<Args>(args)...); +#ifndef LL_MSVC + return 0; +#endif + } +#if __clang__ +#pragma clang diagnostic pop +#endif // __clang__ + + // luau removed lua_dostring(), but since we perform the equivalent luau + // sequence in multiple places, encapsulate it. desc and text are strings + // rather than string_views because dostring() needs pointers to nul- + // terminated char arrays. + int dostring(lua_State* L, const std::string& desc, const std::string& text); + int loadstring(lua_State* L, const std::string& desc, const std::string& text); + + fsyspath source_path(lua_State* L); +} // namespace lluau + +// must be a macro because LL_PRETTY_FUNCTION is context-sensitive +#define lluau_checkstack(L, n) luaL_checkstack((L), (n), LL_PRETTY_FUNCTION) + +std::string lua_tostdstring(lua_State* L, int index); +void lua_pushstdstring(lua_State* L, const std::string& str); +LLSD lua_tollsd(lua_State* L, int index); +void lua_pushllsd(lua_State* L, const LLSD& data); + +/***************************************************************************** +* LuaState +*****************************************************************************/ +/** + * RAII class to manage the lifespan of a lua_State + */ +class LuaState +{ +public: + typedef std::function<void(std::string msg)> script_finished_fn; + + LuaState(script_finished_fn cb={}); + + LuaState(const LuaState&) = delete; + LuaState& operator=(const LuaState&) = delete; + + ~LuaState(); + + bool checkLua(const std::string& desc, int r); + + // expr() is for when we want to capture any results left on the stack + // by a Lua expression, possibly including multiple return values. + // int < 0 means error, and LLSD::asString() is the error message. + // int == 0 with LLSD::isUndefined() means the Lua expression returned no + // results. + // int == 1 means the Lua expression returned one result. + // int > 1 with LLSD::isArray() means the Lua expression returned + // multiple results, represented as the entries of the array. + std::pair<int, LLSD> expr(const std::string& desc, const std::string& text); + + operator lua_State*() const { return mState; } + + // Find or create LuaListener for this LuaState. + LuaListener& obtainListener() { return obtainListener(mState); } + // Find or create LuaListener for passed lua_State. + static LuaListener& obtainListener(lua_State* L); + + // Given lua_State* L, return the LuaState object managing (the main Lua + // thread for) L. + static LuaState& getParent(lua_State* L); + + void set_interrupts_counter(S32 counter); + void check_interrupts_counter(); + +private: + /*---------------------------- feature flag ----------------------------*/ + bool mFeature{ false }; + /*---------------------------- feature flag ----------------------------*/ + script_finished_fn mCallback; + lua_State* mState{ nullptr }; + std::string mError; + S32 mInterrupts{ 0 }; +}; + +/***************************************************************************** +* LuaPopper +*****************************************************************************/ +/** + * LuaPopper is an RAII class whose role is to pop some number of entries + * from the Lua stack if the calling function exits early. + */ +class LuaPopper +{ +public: + LuaPopper(lua_State* L, int count): + mState(L), + mCount(count) + {} + + LuaPopper(const LuaPopper&) = delete; + LuaPopper& operator=(const LuaPopper&) = delete; + + ~LuaPopper(); + + void disarm() { set(0); } + void set(int count) { mCount = count; } + +private: + lua_State* mState; + int mCount; +}; + +/***************************************************************************** +* LuaRemover +*****************************************************************************/ +/** + * Remove a particular stack index on exit from enclosing scope. + * If you pass a negative index (meaning relative to the current stack top), + * converts to an absolute index. The point of LuaRemover is to remove the + * entry at the specified index regardless of subsequent pushes to the stack. + */ +class LuaRemover +{ +public: + LuaRemover(lua_State* L, int index): + mState(L), + mIndex(lua_absindex(L, index)) + {} + LuaRemover(const LuaRemover&) = delete; + LuaRemover& operator=(const LuaRemover&) = delete; + ~LuaRemover() + { + lua_remove(mState, mIndex); + } + +private: + lua_State* mState; + int mIndex; +}; + +/***************************************************************************** +* LuaStackDelta +*****************************************************************************/ +/** + * Instantiate LuaStackDelta in a block to compare the Lua data stack depth on + * entry (LuaStackDelta construction) and exit. Optionally, pass the expected + * depth increment. (But be aware that LuaStackDelta cannot observe the effect + * of a LuaPopper or LuaRemover declared previously in the same block.) + */ +class LuaStackDelta +{ +public: + LuaStackDelta(lua_State* L, const std::string& where, int delta=0); + LuaStackDelta(const LuaStackDelta&) = delete; + LuaStackDelta& operator=(const LuaStackDelta&) = delete; + + ~LuaStackDelta(); + +private: + lua_State* L; + std::string mWhere; + int mDepth, mDelta; +}; + +#define lua_checkdelta(L, ...) LuaStackDelta delta(L, LL_PRETTY_FUNCTION, ##__VA_ARGS__) + +/***************************************************************************** +* lua_push() wrappers for generic code +*****************************************************************************/ +inline +void lua_push(lua_State* L, bool b) +{ + lua_pushboolean(L, int(b)); +} + +inline +void lua_push(lua_State* L, lua_CFunction fn) +{ + lua_pushcfunction(L, fn, ""); +} + +inline +void lua_push(lua_State* L, lua_Integer n) +{ + lua_pushinteger(L, n); +} + +inline +void lua_push(lua_State* L, void* p) +{ + lua_pushlightuserdata(L, p); +} + +inline +void lua_push(lua_State* L, const LLSD& data) +{ + lua_pushllsd(L, data); +} + +inline +void lua_push(lua_State* L, const char* s, size_t len) +{ + lua_pushlstring(L, s, len); +} + +inline +void lua_push(lua_State* L) +{ + lua_pushnil(L); +} + +inline +void lua_push(lua_State* L, lua_Number n) +{ + lua_pushnumber(L, n); +} + +inline +void lua_push(lua_State* L, const std::string& s) +{ + lua_pushstdstring(L, s); +} + +inline +void lua_push(lua_State* L, const char* s) +{ + lua_pushstring(L, s); +} + +/***************************************************************************** +* lua_to() wrappers for generic code +*****************************************************************************/ +template <typename T> +auto lua_to(lua_State* L, int index); + +template <> +inline +auto lua_to<bool>(lua_State* L, int index) +{ + return lua_toboolean(L, index); +} + +template <> +inline +auto lua_to<lua_CFunction>(lua_State* L, int index) +{ + return lua_tocfunction(L, index); +} + +template <> +inline +auto lua_to<lua_Integer>(lua_State* L, int index) +{ + return lua_tointeger(L, index); +} + +template <> +inline +auto lua_to<LLSD>(lua_State* L, int index) +{ + return lua_tollsd(L, index); +} + +template <> +inline +auto lua_to<lua_Number>(lua_State* L, int index) +{ + return lua_tonumber(L, index); +} + +template <> +inline +auto lua_to<std::string>(lua_State* L, int index) +{ + return lua_tostdstring(L, index); +} + +template <> +inline +auto lua_to<void*>(lua_State* L, int index) +{ + return lua_touserdata(L, index); +} + +/***************************************************************************** +* field operations +*****************************************************************************/ +// return to C++, from table at index, the value of field k +template <typename T> +auto lua_getfieldv(lua_State* L, int index, const char* k) +{ + lua_checkdelta(L); + lluau_checkstack(L, 1); + lua_getfield(L, index, k); + LuaPopper pop(L, 1); + return lua_to<T>(L, -1); +} + +// set in table at index, as field k, the specified C++ value +template <typename T> +auto lua_setfieldv(lua_State* L, int index, const char* k, const T& value) +{ + index = lua_absindex(L, index); + lua_checkdelta(L); + lluau_checkstack(L, 1); + lua_push(L, value); + lua_setfield(L, index, k); +} + +// return to C++, from table at index, the value of field k (without metamethods) +template <typename T> +auto lua_rawgetfield(lua_State* L, int index, const std::string_view& k) +{ + index = lua_absindex(L, index); + lua_checkdelta(L); + lluau_checkstack(L, 1); + lua_pushlstring(L, k.data(), k.length()); + lua_rawget(L, index); + LuaPopper pop(L, 1); + return lua_to<T>(L, -1); +} + +// set in table at index, as field k, the specified C++ value (without metamethods) +template <typename T> +void lua_rawsetfield(lua_State* L, int index, const std::string_view& k, const T& value) +{ + index = lua_absindex(L, index); + lua_checkdelta(L); + lluau_checkstack(L, 2); + lua_pushlstring(L, k.data(), k.length()); + lua_push(L, value); + lua_rawset(L, index); +} + +/***************************************************************************** +* lua_function (and helper class LuaFunction) +*****************************************************************************/ +/** + * LuaFunction is a base class containing a static registry of its static + * subclass call() methods. call() is NOT virtual: instead, each subclass + * constructor passes a pointer to its distinct call() method to the base- + * class constructor, along with a name by which to register that method. + * + * The init() method walks the registry and registers each such name with the + * passed lua_State. + */ +class LuaFunction +{ +public: + LuaFunction(const std::string_view& name, lua_CFunction function, + const std::string_view& helptext); + + static void init(lua_State* L); + + static lua_CFunction get(const std::string& key); + +protected: + using Registry = std::map<std::string, std::pair<lua_CFunction, std::string>>; + using Lookup = std::map<lua_CFunction, std::string>; + static std::pair<const Registry&, const Lookup&> getRState() { return getState(); } + +private: + static std::pair<Registry&, Lookup&> getState(); +}; + +/** + * lua_function(name, helptext) is a macro to facilitate defining C++ functions + * available to Lua. It defines a subclass of LuaFunction and declares a + * static instance of that subclass, thereby forcing the compiler to call its + * constructor at module initialization time. The constructor passes the + * stringized instance name to its LuaFunction base-class constructor, along + * with a pointer to the static subclass call() method. It then emits the + * call() method definition header, to be followed by a method body enclosed + * in curly braces as usual. + */ +#define lua_function(name, helptext) \ +static struct name##_luasub : public LuaFunction \ +{ \ + name##_luasub(): LuaFunction(#name, &call, helptext) {} \ + static int call(lua_State* L); \ +} name##_lua; \ +int name##_luasub::call(lua_State* L) +// { +// ... supply method body here, referencing 'L' ... +// } + +/***************************************************************************** +* lua_emplace<T>(), lua_toclass<T>() +*****************************************************************************/ +// Every instance of DistinctInt has a different int value, barring int +// wraparound. +class DistinctInt +{ +public: + DistinctInt(): mValue(++mValues) {} + int get() const { return mValue; } + operator int() const { return mValue; } +private: + static int mValues; + int mValue; +}; + +namespace { + +template <typename T> +struct TypeTag +{ + // For (std::is_same<T, U>), &TypeTag<T>::value == &TypeTag<U>::value. + // For (! std::is_same<T, U>), &TypeTag<T>::value != &TypeTag<U>::value. + // And every distinct instance of DistinctInt has a distinct value. + // Therefore, TypeTag<T>::value is an int uniquely associated with each + // distinct T. + static DistinctInt value; +}; + +template <typename T> +DistinctInt TypeTag<T>::value; + +} // anonymous namespace + +/** + * On the stack belonging to the passed lua_State, push a Lua userdata object + * containing a newly-constructed C++ object T(args...). The userdata has a + * Luau destructor guaranteeing that the new T instance is destroyed when the + * userdata is garbage-collected, no later than when the LuaState is + * destroyed. It may be destroyed explicitly by calling lua_destroyuserdata(). + * + * Usage: + * lua_emplace<T>(L, T constructor args...); + * // L's Lua stack top is now a userdata containing T + */ +template <class T, typename... ARGS> +void lua_emplace(lua_State* L, ARGS&&... args) +{ + lua_checkdelta(L, 1); + lluau_checkstack(L, 1); + int tag{ TypeTag<T>::value }; + if (! lua_getuserdatadtor(L, tag)) + { + // We haven't yet told THIS lua_State the destructor to use for this tag. + lua_setuserdatadtor( + L, tag, + [](lua_State*, void* ptr) + { + // destroy the contained T instance + static_cast<T*>(ptr)->~T(); + }); + } + auto ptr = lua_newuserdatatagged(L, sizeof(T), tag); + // stack is uninitialized userdata + // For now, assume (but verify) that lua_newuserdata() returns a + // conservatively-aligned ptr. If that turns out not to be the case, we + // might have to discard the new userdata, overallocate its successor and + // perform manual alignment -- but only if we must. + llassert((uintptr_t(ptr) % alignof(T)) == 0); + // Construct our T there using placement new + new (ptr) T(std::forward<ARGS>(args)...); + // stack is now initialized userdata containing our T instance -- return + // that +} + +/** + * If the value at the passed acceptable index is a full userdata created by + * lua_emplace<T>(), return a pointer to the contained T instance. Otherwise + * (index is not a full userdata; userdata is not of type T) return nullptr. + */ +template <class T> +T* lua_toclass(lua_State* L, int index) +{ + lua_checkdelta(L); + // get void* pointer to userdata (if that's what it is) + void* ptr{ lua_touserdatatagged(L, index, TypeTag<T>::value) }; + // Derive the T* from ptr. If in future lua_emplace() must manually + // align our T* within the Lua-provided void*, adjust accordingly. + return static_cast<T*>(ptr); +} + +/** + * Call lua_destroyuserdata() with the doomed userdata on the stack top. + * It must have been created by lua_emplace(). + */ +int lua_destroyuserdata(lua_State* L); + +/** + * Call lua_pushcclosure(L, lua_destroybounduserdata, 1) with the target + * userdata on the stack top. When the resulting C closure is called with no + * arguments, the bound userdata is destroyed by lua_destroyuserdata(). + */ +int lua_destroybounduserdata(lua_State *L); + +/***************************************************************************** +* lua_what() +*****************************************************************************/ +// Usage: std::cout << lua_what(L, stackindex) << ...; +// Reports on the Lua value found at the passed stackindex. +// If cast to std::string, returns the corresponding string value. +class lua_what +{ +public: + lua_what(lua_State* state, int idx): + L(state), + index(idx) + {} + + friend std::ostream& operator<<(std::ostream& out, const lua_what& self); + + operator std::string() const { return stringize(*this); } + +private: + lua_State* L; + int index; +}; + +/***************************************************************************** +* lua_stack() +*****************************************************************************/ +// Usage: std::cout << lua_stack(L) << ...; +// Reports on the contents of the Lua stack. +// If cast to std::string, returns the corresponding string value. +class lua_stack +{ +public: + lua_stack(lua_State* state): + L(state) + {} + + friend std::ostream& operator<<(std::ostream& out, const lua_stack& self); + + operator std::string() const { return stringize(*this); } + +private: + lua_State* L; +}; + +/***************************************************************************** +* LuaLog +*****************************************************************************/ +// adapted from indra/test/debug.h +// can't generalize Debug::operator() target because it's a variadic template +class LuaLog +{ +public: + template <typename... ARGS> + LuaLog(lua_State* L, ARGS&&... args): + L(L), + mBlock(stringize(std::forward<ARGS>(args)...)) + { + (*this)("entry ", lua_stack(L)); + } + + // non-copyable + LuaLog(const LuaLog&) = delete; + LuaLog& operator=(const LuaLog&) = delete; + + ~LuaLog() + { + auto exceptional{ std::uncaught_exceptions()? "exceptional " : "" }; + (*this)(exceptional, "exit ", lua_stack(L)); + } + + template <typename... ARGS> + void operator()(ARGS&&... args) + { + LL_DEBUGS("Lua") << mBlock << ' '; + stream_to(LL_CONT, std::forward<ARGS>(args)...); + LL_ENDL; + } + +private: + lua_State* L; + const std::string mBlock; +}; + +#endif /* ! defined(LL_LUA_FUNCTION_H) */ diff --git a/indra/llcommon/lualistener.cpp b/indra/llcommon/lualistener.cpp new file mode 100644 index 0000000000..94085c6798 --- /dev/null +++ b/indra/llcommon/lualistener.cpp @@ -0,0 +1,102 @@ +/** + * @file lualistener.cpp + * @author Nat Goodspeed + * @date 2024-02-06 + * @brief Implementation for lualistener. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "lualistener.h" +// STL headers +// std headers +#include <iomanip> // std::quoted() +// external library headers +#include "luau/lua.h" +// other Linden headers +#include "llerror.h" +#include "llleaplistener.h" +#include "lua_function.h" + +const int MAX_QSIZE = 1000; + +std::ostream& operator<<(std::ostream& out, const LuaListener& self) +{ + return out << "LuaListener(" << std::quoted(self.mCoroName) << ", " + << self.getReplyName() << ", " << self.getCommandName() << ")"; +} + +LuaListener::LuaListener(lua_State* L): + mCoroName(LLCoros::getName()), + mListener(new LLLeapListener( + "LuaListener", + [this](const std::string& pump, const LLSD& data) + { return queueEvent(pump, data); })), + // Listen for shutdown events. + mShutdownConnection( + LLCoros::getStopListener( + "LuaState", + mCoroName, + [this](const LLSD&) + { + // If a Lua script is still blocked in getNext() during + // viewer shutdown, close the queue to wake up getNext(). + mQueue.close(); + })) +{ + LL_DEBUGS("Lua") << "LuaListener(" << std::quoted(mCoroName) << ")" << LL_ENDL; +} + +LuaListener::~LuaListener() +{ + LL_DEBUGS("Lua") << "~LuaListener(" << std::quoted(mCoroName) << ")" << LL_ENDL; +} + +std::string LuaListener::getReplyName() const +{ + return mListener->getReplyPump().getName(); +} + +std::string LuaListener::getCommandName() const +{ + return mListener->getPumpName(); +} + +bool LuaListener::queueEvent(const std::string& pump, const LLSD& data) +{ + // Our Lua script might be stalled, or just fail to retrieve events. Don't + // grow this queue indefinitely. But don't set MAX_QSIZE as the queue + // capacity or we'd block the post() call trying to propagate this event! + if (auto size = mQueue.size(); size > MAX_QSIZE) + { + LL_WARNS("Lua") << "LuaListener queue for " << mCoroName + << " exceeds " << MAX_QSIZE << ": " << size + << " -- discarding event" << LL_ENDL; + } + else + { + mQueue.push(decltype(mQueue)::value_type(pump, data)); + } + return false; +} + +LuaListener::PumpData LuaListener::getNext() +{ + try + { + LLCoros::TempStatus status("get_event_next()"); + return mQueue.pop(); + } + catch (const LLThreadSafeQueueInterrupt&) + { + // mQueue has been closed. The only way that happens is when we detect + // viewer shutdown. Terminate the calling Lua coroutine. + LLCoros::checkStop(); + return {}; + } +} diff --git a/indra/llcommon/lualistener.h b/indra/llcommon/lualistener.h new file mode 100644 index 0000000000..4c0a2a5c87 --- /dev/null +++ b/indra/llcommon/lualistener.h @@ -0,0 +1,65 @@ +/** + * @file lualistener.h + * @author Nat Goodspeed + * @date 2024-02-06 + * @brief Define LuaListener class + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LUALISTENER_H) +#define LL_LUALISTENER_H + +#include "llevents.h" // LLTempBoundListener +#include "llsd.h" +#include "llthreadsafequeue.h" +#include <iosfwd> // std::ostream +#include <memory> // std::unique_ptr +#include <string> +#include <utility> // std::pair + +struct lua_State; +class LLLeapListener; + +/** + * LuaListener is based on LLLeap. It serves an analogous function. + * + * Like LLLeap, each LuaListener instance also has an associated + * LLLeapListener to respond to LLEventPump management commands. + */ +class LuaListener +{ +public: + LuaListener(lua_State* L); + + LuaListener(const LuaListener&) = delete; + LuaListener& operator=(const LuaListener&) = delete; + + ~LuaListener(); + + std::string getReplyName() const; + std::string getCommandName() const; + + /** + * LuaListener enqueues reply events from its LLLeapListener on mQueue. + * Call getNext() to retrieve the next such event. Blocks the calling + * coroutine if the queue is empty. + */ + using PumpData = std::pair<std::string, LLSD>; + PumpData getNext(); + + friend std::ostream& operator<<(std::ostream& out, const LuaListener& self); + +private: + bool queueEvent(const std::string& pump, const LLSD& data); + + LLThreadSafeQueue<PumpData> mQueue; + + std::string mCoroName; + std::unique_ptr<LLLeapListener> mListener; + LLTempBoundListener mShutdownConnection; +}; + +#endif /* ! defined(LL_LUALISTENER_H) */ diff --git a/indra/llcommon/resultset.cpp b/indra/llcommon/resultset.cpp new file mode 100644 index 0000000000..4d7b00eabd --- /dev/null +++ b/indra/llcommon/resultset.cpp @@ -0,0 +1,96 @@ +/** + * @file resultset.cpp + * @author Nat Goodspeed + * @date 2024-09-03 + * @brief Implementation for resultset. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "resultset.h" +// STL headers +// std headers +// external library headers +// other Linden headers +#include "llerror.h" +#include "llsdutil.h" + +namespace LL +{ + +LLSD ResultSet::getKeyLength() const +{ + return llsd::array(getKey(), getLength()); +} + +std::pair<LLSD, int> ResultSet::getSliceStart(int index, int count) const +{ + // only call getLength() once + auto length = getLength(); + // Adjust bounds [start, end) to overlap the actual result set from + // [0, getLength()). Permit negative index; e.g. with a result set + // containing 5 entries, getSlice(-2, 5) will adjust start to 0 and + // end to 3. + int start = llclamp(index, 0, length); + int end = llclamp(index + count, 0, length); + LLSD result{ LLSD::emptyArray() }; + // beware of count <= 0, or an [index, count) range that doesn't even + // overlap [0, length) at all + if (end > start) + { + // Right away expand the result array to the size we'll need. + // (end - start) is that size; (end - start - 1) is the index of the + // last entry in result. + result[end - start - 1] = LLSD(); + for (int i = 0; (start + i) < end; ++i) + { + // For this to be a slice, set result[0] = getSingle(start), etc. + result[i] = getSingle(start + i); + } + } + return { result, start }; +} + +LLSD ResultSet::getSlice(int index, int count) const +{ + return getSliceStart(index, count).first; +} + +/*==========================================================================*| +LLSD ResultSet::getSingle(int index) const +{ + if (0 <= index && index < getLength()) + { + return getSingle_(index); + } + else + { + return {}; + } +} +|*==========================================================================*/ + +ResultSet::ResultSet(const std::string& name): + mName(name) +{ + LL_DEBUGS("Lua") << *this << LL_ENDL; +} + +ResultSet::~ResultSet() +{ + // We want to be able to observe that the consuming script eventually + // destroys each of these ResultSets. + LL_DEBUGS("Lua") << "~" << *this << LL_ENDL; +} + +} // namespace LL + +std::ostream& operator<<(std::ostream& out, const LL::ResultSet& self) +{ + return out << "ResultSet(" << self.mName << ", " << self.getKey() << ")"; +} diff --git a/indra/llcommon/resultset.h b/indra/llcommon/resultset.h new file mode 100644 index 0000000000..90d52b6fe4 --- /dev/null +++ b/indra/llcommon/resultset.h @@ -0,0 +1,61 @@ +/** + * @file resultset.h + * @author Nat Goodspeed + * @date 2024-09-03 + * @brief ResultSet is an abstract base class to allow scripted access to + * potentially large collections representable as LLSD arrays. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_RESULTSET_H) +#define LL_RESULTSET_H + +#include "llinttracker.h" +#include "llsd.h" +#include <iosfwd> // std::ostream +#include <utility> // std::pair + +namespace LL +{ + +// This abstract base class defines an interface by which a large collection +// of items representable as an LLSD array can be retrieved in slices. It isa +// LLIntTracker so we can pass its unique int key to a consuming script via +// LLSD. +struct ResultSet: public LLIntTracker<ResultSet> +{ + // Get the length of the result set. Indexes are 0-relative. + virtual int getLength() const = 0; + // Get conventional LLSD { key, length } pair. + LLSD getKeyLength() const; + // Retrieve LLSD corresponding to a single entry from the result set, + // once we're sure the index is valid. + virtual LLSD getSingle(int index) const = 0; + // Retrieve LLSD corresponding to a "slice" of the result set: a + // contiguous sub-array starting at index. The returned LLSD array might + // be shorter than count entries if count > MAX_ITEM_LIMIT, or if the + // specified slice contains the end of the result set. + LLSD getSlice(int index, int count) const; + // Like getSlice(), but also return adjusted start position. + std::pair<LLSD, int> getSliceStart(int index, int count) const; +/*==========================================================================*| + // Retrieve LLSD corresponding to a single entry from the result set, + // with index validation. + LLSD getSingle(int index) const; +|*==========================================================================*/ + + /*---------------- the rest is solely for debug logging ----------------*/ + std::string mName; + + ResultSet(const std::string& name); + virtual ~ResultSet(); +}; + +} // namespace LL + +std::ostream& operator<<(std::ostream& out, const LL::ResultSet& self); + +#endif /* ! defined(LL_RESULTSET_H) */ diff --git a/indra/llcommon/scope_exit.h b/indra/llcommon/scope_exit.h new file mode 100644 index 0000000000..eb4df4f74d --- /dev/null +++ b/indra/llcommon/scope_exit.h @@ -0,0 +1,34 @@ +/** + * @file scope_exit.h + * @author Nat Goodspeed + * @date 2024-08-15 + * @brief Cheap imitation of std::experimental::scope_exit + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_SCOPE_EXIT_H) +#define LL_SCOPE_EXIT_H + +#include <functional> + +namespace LL +{ + +class scope_exit +{ +public: + scope_exit(const std::function<void()>& func): mFunc(func) {} + scope_exit(const scope_exit&) = delete; + scope_exit& operator=(const scope_exit&) = delete; + ~scope_exit() { mFunc(); } + +private: + std::function<void()> mFunc; +}; + +} // namespace LL + +#endif /* ! defined(LL_SCOPE_EXIT_H) */ diff --git a/indra/llcommon/stringize.h b/indra/llcommon/stringize.h index 536a18abc1..2730728637 100644 --- a/indra/llcommon/stringize.h +++ b/indra/llcommon/stringize.h @@ -88,13 +88,13 @@ struct gstringize_impl }; // partially specialize for a single STRING argument - -// note that ll_convert<T>(T) already handles the trivial case +// note that ll_convert_to<T>(T) already handles the trivial case template <typename OUTCHAR, typename INCHAR> struct gstringize_impl<OUTCHAR, std::basic_string<INCHAR>> { auto operator()(const std::basic_string<INCHAR>& arg) { - return ll_convert<std::basic_string<OUTCHAR>>(arg); + return ll_convert_to<std::basic_string<OUTCHAR>>(arg); } }; @@ -105,7 +105,7 @@ struct gstringize_impl<OUTCHAR, INCHAR*> { auto operator()(const INCHAR* arg) { - return ll_convert<std::basic_string<OUTCHAR>>(arg); + return ll_convert_to<std::basic_string<OUTCHAR>>(arg); } }; @@ -190,16 +190,4 @@ void destringize_f(std::basic_string<CHARTYPE> const & str, Functor const & f) f(in); } -/** - * DESTRINGIZE(str, item1 >> item2 >> item3 ...) effectively expands to the - * following: - * @code - * std::istringstream in(str); - * in >> item1 >> item2 >> item3 ... ; - * @endcode - */ -#define DESTRINGIZE(STR, EXPRESSION) (destringize_f((STR), [&](auto& in){in >> EXPRESSION;})) -// legacy name, just use DESTRINGIZE() going forward -#define DEWSTRINGIZE(STR, EXPRESSION) DESTRINGIZE(STR, EXPRESSION) - #endif /* ! defined(LL_STRINGIZE_H) */ diff --git a/indra/llcommon/tempset.h b/indra/llcommon/tempset.h new file mode 100755 index 0000000000..773f19f756 --- /dev/null +++ b/indra/llcommon/tempset.h @@ -0,0 +1,41 @@ +/** + * @file tempset.h + * @author Nat Goodspeed + * @date 2024-06-12 + * @brief Temporarily override a variable for scope duration, then restore + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_TEMPSET_H) +#define LL_TEMPSET_H + +// RAII class to set specified variable to specified value +// only for the duration of containing scope +template <typename VAR, typename VALUE> +class TempSet +{ +public: + TempSet(VAR& var, const VALUE& value): + mVar(var), + mOldValue(mVar) + { + mVar = value; + } + + TempSet(const TempSet&) = delete; + TempSet& operator=(const TempSet&) = delete; + + ~TempSet() + { + mVar = mOldValue; + } + +private: + VAR& mVar; + VAR mOldValue; +}; + +#endif /* ! defined(LL_TEMPSET_H) */ diff --git a/indra/llcommon/tests/StringVec.h b/indra/llcommon/tests/StringVec.h index 4311cba992..3f8665affb 100644 --- a/indra/llcommon/tests/StringVec.h +++ b/indra/llcommon/tests/StringVec.h @@ -18,6 +18,16 @@ typedef std::vector<std::string> StringVec; +#if defined(LL_LLTUT_H) +// Modern compilers require us to define operator<<(std::ostream&, StringVec) +// before the definition of the ensure() template that engages it. The error +// stating that the compiler can't find a viable operator<<() is so perplexing +// that even though I've obviously hit it a couple times before, a new +// instance still caused much head-scratching. This warning is intended to +// demystify any inadvertent future recurrence. +#warning "StringVec.h must be #included BEFORE lltut.h for ensure() to work" +#endif + std::ostream& operator<<(std::ostream& out, const StringVec& strings) { out << '('; diff --git a/indra/llcommon/tests/llcond_test.cpp b/indra/llcommon/tests/llcond_test.cpp index f2a302ed13..7d1ac6edb9 100644 --- a/indra/llcommon/tests/llcond_test.cpp +++ b/indra/llcommon/tests/llcond_test.cpp @@ -19,6 +19,7 @@ // other Linden headers #include "../test/lltut.h" #include "llcoros.h" +#include "lldefs.h" // llless() /***************************************************************************** * TUT @@ -64,4 +65,78 @@ namespace tut cond.set_all(2); cond.wait_equal(3); } + + template <typename T0, typename T1> + struct compare + { + const char* desc; + T0 lhs; + T1 rhs; + bool expect; + + void test() const + { + // fails +// ensure_equals(desc, (lhs < rhs), expect); + ensure_equals(desc, llless(lhs, rhs), expect); + } + }; + + template<> template<> + void object::test<3>() + { + set_test_name("comparison"); + // Try to construct signed and unsigned variables such that the + // compiler can't optimize away the code to compare at runtime. + std::istringstream input("-1 10 20 10 20"); + int minus1, s10, s20; + input >> minus1 >> s10 >> s20; + unsigned u10, u20; + input >> u10 >> u20; + ensure_equals("minus1 wrong", minus1, -1); + ensure_equals("s10 wrong", s10, 10); + ensure_equals("s20 wrong", s20, 20); + ensure_equals("u10 wrong", u10, 10); + ensure_equals("u20 wrong", u20, 20); + // signed < signed should always work! + compare<int, int> ss[] = + { {"minus1 < s10", minus1, s10, true}, + {"s10 < s10", s10, s10, false}, + {"s20 < s10", s20, s20, false} + }; + for (const auto& cmp : ss) + { + cmp.test(); + } + // unsigned < unsigned should always work! + compare<unsigned, unsigned> uu[] = + { {"u10 < u20", u10, u20, true}, + {"u20 < u20", u20, u20, false}, + {"u20 < u10", u20, u10, false} + }; + for (const auto& cmp : uu) + { + cmp.test(); + } + // signed < unsigned ?? + compare<int, unsigned> su[] = + { {"minus1 < u10", minus1, u10, true}, + {"s10 < u10", s10, u10, false}, + {"s20 < u10", s20, u10, false} + }; + for (const auto& cmp : su) + { + cmp.test(); + } + // unsigned < signed ?? + compare<unsigned, int> us[] = + { {"u10 < minus1", u10, minus1, false}, + {"u10 < s10", u10, s10, false}, + {"u10 < s20", u10, s20, true} + }; + for (const auto& cmp : us) + { + cmp.test(); + } + } } // namespace tut diff --git a/indra/llcommon/tests/llerror_test.cpp b/indra/llcommon/tests/llerror_test.cpp index 3ec429530c..d597e90ba0 100644 --- a/indra/llcommon/tests/llerror_test.cpp +++ b/indra/llcommon/tests/llerror_test.cpp @@ -112,10 +112,10 @@ namespace tut mMessages.push_back(message); } - int countMessages() { return (int) mMessages.size(); } + int countMessages() const { return (int) mMessages.size(); } void clearMessages() { mMessages.clear(); } - std::string message(int n) + std::string message(int n) const { std::ostringstream test_name; test_name << "testing message " << n << ", not enough messages"; @@ -124,6 +124,16 @@ namespace tut return mMessages[n]; } + void reportMessages() const + { + std::cerr << '\n'; + int n = 0; + for (const auto& msg : mMessages) + { + std::cerr << std::setw(2) << n++ << ": " << msg.substr(0, 100) << '\n'; + } + } + private: typedef std::vector<std::string> MessageVector; MessageVector mMessages; @@ -134,6 +144,8 @@ namespace tut LLError::RecorderPtr mRecorder; LLError::SettingsStoragePtr mPriorErrorSettings; + auto recorder() { return std::dynamic_pointer_cast<TestRecorder>(mRecorder); } + ErrorTestData(): mRecorder(new TestRecorder()) { @@ -153,27 +165,32 @@ namespace tut int countMessages() { - return std::dynamic_pointer_cast<TestRecorder>(mRecorder)->countMessages(); + return recorder()->countMessages(); } void clearMessages() { - std::dynamic_pointer_cast<TestRecorder>(mRecorder)->clearMessages(); + recorder()->clearMessages(); } void setWantsTime(bool t) - { - std::dynamic_pointer_cast<TestRecorder>(mRecorder)->showTime(t); - } + { + recorder()->showTime(t); + } void setWantsMultiline(bool t) - { - std::dynamic_pointer_cast<TestRecorder>(mRecorder)->showMultiline(t); - } + { + recorder()->showMultiline(t); + } std::string message(int n) { - return std::dynamic_pointer_cast<TestRecorder>(mRecorder)->message(n); + return recorder()->message(n); + } + + void reportMessages() + { + recorder()->reportMessages(); } void ensure_message_count(int expectedCount) @@ -204,7 +221,7 @@ namespace tut on_field++; } // except function, which may have embedded spaces so ends with " : " - else if ( ( on_field == FUNCTION_FIELD ) + else if (( on_field == FUNCTION_FIELD ) && ( ':' == msg[scan+1] && ' ' == msg[scan+2] ) ) { @@ -233,19 +250,35 @@ namespace tut } void ensure_message_field_equals(int msgnum, LogFieldIndex fieldnum, const std::string& expectedText) - { - std::ostringstream test_name; - test_name << "testing message " << msgnum << " field " << FieldName[fieldnum] << "\n message: \"" << message(msgnum) << "\"\n "; + { + std::ostringstream test_name; + test_name << "testing message " << msgnum << " field " << FieldName[fieldnum] << "\n message: \"" << message(msgnum).substr(0, 100) << "\"\n "; - ensure_equals(test_name.str(), message_field(msgnum, fieldnum), expectedText); - } + try + { + ensure_equals(test_name.str(), message_field(msgnum, fieldnum), expectedText); + } + catch (const failure&) + { + reportMessages(); + throw; + } + } void ensure_message_does_not_contain(int n, const std::string& expectedText) { std::ostringstream test_name; test_name << "testing message " << n; - ensure_does_not_contain(test_name.str(), message(n), expectedText); + try + { + ensure_does_not_contain(test_name.str(), message(n), expectedText); + } + catch (const failure&) + { + reportMessages(); + throw; + } } }; @@ -297,29 +330,33 @@ namespace tut ensure_message_field_equals(3, MSG_FIELD, "four"); ensure_message_field_equals(3, LEVEL_FIELD, "ERROR"); ensure_message_field_equals(3, TAGS_FIELD, "#WriteTag#"); - ensure_message_count(4); + // LL_ERRS() produces 2 recordMessage() calls + ensure_message_count(5); LLError::setDefaultLevel(LLError::LEVEL_INFO); writeSome(); - ensure_message_field_equals(4, MSG_FIELD, "two"); - ensure_message_field_equals(5, MSG_FIELD, "three"); - ensure_message_field_equals(6, MSG_FIELD, "four"); - ensure_message_count(7); + ensure_message_field_equals(5, MSG_FIELD, "two"); + ensure_message_field_equals(6, MSG_FIELD, "three"); + ensure_message_field_equals(7, MSG_FIELD, "four"); + // LL_ERRS() produces 2 recordMessage() calls + ensure_message_count(9); LLError::setDefaultLevel(LLError::LEVEL_WARN); writeSome(); - ensure_message_field_equals(7, MSG_FIELD, "three"); - ensure_message_field_equals(8, MSG_FIELD, "four"); - ensure_message_count(9); + ensure_message_field_equals(9, MSG_FIELD, "three"); + ensure_message_field_equals(10, MSG_FIELD, "four"); + // LL_ERRS() produces 2 recordMessage() calls + ensure_message_count(12); LLError::setDefaultLevel(LLError::LEVEL_ERROR); writeSome(); - ensure_message_field_equals(9, MSG_FIELD, "four"); - ensure_message_count(10); + ensure_message_field_equals(12, MSG_FIELD, "four"); + // LL_ERRS() produces 2 recordMessage() calls + ensure_message_count(14); LLError::setDefaultLevel(LLError::LEVEL_NONE); writeSome(); - ensure_message_count(10); + ensure_message_count(14); } template<> template<> @@ -331,7 +368,8 @@ namespace tut ensure_message_field_equals(1, LEVEL_FIELD, "INFO"); ensure_message_field_equals(2, LEVEL_FIELD, "WARNING"); ensure_message_field_equals(3, LEVEL_FIELD, "ERROR"); - ensure_message_count(4); + // LL_ERRS() produces 2 recordMessage() calls + ensure_message_count(5); } template<> template<> @@ -627,7 +665,8 @@ namespace tut ensure_message_field_equals(0, LOCATION_FIELD, location); ensure_message_field_equals(0, MSG_FIELD, "die"); - ensure_message_count(1); + // LL_ERRS() produces 2 recordMessage() calls + ensure_message_count(2); ensure("fatal callback called", fatalWasCalled); } @@ -751,10 +790,12 @@ namespace tut ensure_message_field_equals(0, MSG_FIELD, "aim west"); ensure_message_field_equals(1, MSG_FIELD, "ate eels"); - ensure_message_field_equals(2, MSG_FIELD, "buy iron"); - ensure_message_field_equals(3, MSG_FIELD, "bad word"); - ensure_message_field_equals(4, MSG_FIELD, "big easy"); - ensure_message_count(5); + // LL_ERRS() produces 2 recordMessage() calls + ensure_message_field_equals(3, MSG_FIELD, "buy iron"); + ensure_message_field_equals(4, MSG_FIELD, "bad word"); + ensure_message_field_equals(5, MSG_FIELD, "big easy"); + // LL_ERRS() produces 2 recordMessage() calls + ensure_message_count(7); } template<> template<> @@ -868,9 +909,11 @@ namespace tut TestBeta::doAll(); ensure_message_field_equals(3, MSG_FIELD, "aim west"); ensure_message_field_equals(4, MSG_FIELD, "ate eels"); - ensure_message_field_equals(5, MSG_FIELD, "bad word"); - ensure_message_field_equals(6, MSG_FIELD, "big easy"); - ensure_message_count(7); + // LL_ERRS() produces 2 recordMessage() calls + ensure_message_field_equals(6, MSG_FIELD, "bad word"); + ensure_message_field_equals(7, MSG_FIELD, "big easy"); + // LL_ERRS() produces 2 recordMessage() calls + ensure_message_count(9); } } diff --git a/indra/llcommon/tests/lleventcoro_test.cpp b/indra/llcommon/tests/lleventcoro_test.cpp index ab174a8bde..0e352bce0f 100644 --- a/indra/llcommon/tests/lleventcoro_test.cpp +++ b/indra/llcommon/tests/lleventcoro_test.cpp @@ -111,14 +111,13 @@ namespace tut void test_data::explicit_wait(std::shared_ptr<LLCoros::Promise<std::string>>& cbp) { - BEGIN - { - mSync.bump(); - // The point of this test is to verify / illustrate suspending a - // coroutine for something other than an LLEventPump. In other - // words, this shows how to adapt to any async operation that - // provides a callback-style notification (and prove that it - // works). + DEBUG; + mSync.bump(); + // The point of this test is to verify / illustrate suspending a + // coroutine for something other than an LLEventPump. In other + // words, this shows how to adapt to any async operation that + // provides a callback-style notification (and prove that it + // works). // Perhaps we would send a request to a remote server and arrange // for cbp->set_value() to be called on response. @@ -128,13 +127,11 @@ namespace tut cbp = std::make_shared<LLCoros::Promise<std::string>>(); LLCoros::Future<std::string> future = LLCoros::getFuture(*cbp); - // calling get() on the future causes us to suspend - debug("about to suspend"); - stringdata = future.get(); - mSync.bump(); - ensure_equals("Got it", stringdata, "received"); - } - END + // calling get() on the future causes us to suspend + debug("about to suspend"); + stringdata = future.get(); + mSync.bump(); + ensure_equals("Got it", stringdata, "received"); } template<> template<> @@ -161,13 +158,9 @@ namespace tut void test_data::waitForEventOn1() { - BEGIN - { - mSync.bump(); - result = suspendUntilEventOn("source"); - mSync.bump(); - } - END + mSync.bump(); + result = suspendUntilEventOn("source"); + mSync.bump(); } template<> template<> @@ -187,15 +180,11 @@ namespace tut void test_data::coroPump() { - BEGIN - { - mSync.bump(); - LLCoroEventPump waiter; - replyName = waiter.getName(); - result = waiter.suspend(); - mSync.bump(); - } - END + mSync.bump(); + LLCoroEventPump waiter; + replyName = waiter.getName(); + result = waiter.suspend(); + mSync.bump(); } template<> template<> @@ -215,16 +204,12 @@ namespace tut void test_data::postAndWait1() { - BEGIN - { - mSync.bump(); - result = postAndSuspend(LLSDMap("value", 17), // request event - immediateAPI.getPump(), // requestPump - "reply1", // replyPump - "reply"); // request["reply"] = name - mSync.bump(); - } - END + mSync.bump(); + result = postAndSuspend(LLSDMap("value", 17), // request event + immediateAPI.getPump(), // requestPump + "reply1", // replyPump + "reply"); // request["reply"] = name + mSync.bump(); } template<> template<> @@ -238,15 +223,11 @@ namespace tut void test_data::coroPumpPost() { - BEGIN - { - mSync.bump(); - LLCoroEventPump waiter; - result = waiter.postAndSuspend(LLSDMap("value", 17), - immediateAPI.getPump(), "reply"); - mSync.bump(); - } - END + mSync.bump(); + LLCoroEventPump waiter; + result = waiter.postAndSuspend(LLSDMap("value", 17), + immediateAPI.getPump(), "reply"); + mSync.bump(); } template<> template<> diff --git a/indra/llcommon/tests/lleventdispatcher_test.cpp b/indra/llcommon/tests/lleventdispatcher_test.cpp index 44f772e322..be05ca4e85 100644 --- a/indra/llcommon/tests/lleventdispatcher_test.cpp +++ b/indra/llcommon/tests/lleventdispatcher_test.cpp @@ -17,13 +17,13 @@ // std headers // external library headers // other Linden headers +#include "StringVec.h" #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" diff --git a/indra/llcommon/tests/lleventfilter_test.cpp b/indra/llcommon/tests/lleventfilter_test.cpp index d7b80e2545..9ca1ca4e2e 100644 --- a/indra/llcommon/tests/lleventfilter_test.cpp +++ b/indra/llcommon/tests/lleventfilter_test.cpp @@ -34,10 +34,10 @@ // std headers // external library headers // other Linden headers +#include "listener.h" #include "../test/lltut.h" #include "stringize.h" #include "llsdutil.h" -#include "listener.h" #include "tests/wrapllerrs.h" #include <typeinfo> @@ -51,6 +51,7 @@ // as we've carefully put all functionality except actual LLTimer calls into // LLEventTimeoutBase, that should suffice. We're not not not trying to test // LLTimer here. +#if 0 // time testing needs reworking class TestEventTimeout: public LLEventTimeoutBase { public: @@ -151,6 +152,7 @@ public: F32 mAlarmRemaining, mTimerRemaining; LLEventTimeoutBase::Action mAlarmAction; }; +#endif // time testing needs reworking /***************************************************************************** * TUT @@ -220,6 +222,8 @@ namespace tut void filter_object::test<2>() { set_test_name("LLEventTimeout::actionAfter()"); + skip("time testing needs reworking"); +#if 0 // time testing needs reworking LLEventPump& driver(pumps.obtain("driver")); TestEventTimeout filter(driver); listener0.reset(0); @@ -285,12 +289,15 @@ namespace tut filter.forceTimeout(); mainloop.post(17); check_listener("no timeout 6", listener1, LLSD(0)); +#endif // time testing needs reworking } template<> template<> void filter_object::test<3>() { set_test_name("LLEventTimeout::eventAfter()"); + skip("time testing needs reworking"); +#if 0 // time testing needs reworking LLEventPump& driver(pumps.obtain("driver")); TestEventTimeout filter(driver); listener0.reset(0); @@ -322,12 +329,15 @@ namespace tut filter.forceTimeout(); mainloop.post(17); check_listener("no timeout 3", listener0, LLSD(0)); +#endif // time testing needs reworking } template<> template<> void filter_object::test<4>() { set_test_name("LLEventTimeout::errorAfter()"); + skip("time testing needs reworking"); +#if 0 // time testing needs reworking WrapLLErrs capture; LLEventPump& driver(pumps.obtain("driver")); TestEventTimeout filter(driver); @@ -362,12 +372,15 @@ namespace tut filter.forceTimeout(); mainloop.post(17); check_listener("no timeout 3", listener0, LLSD(0)); +#endif // time testing needs reworking } template<> template<> void filter_object::test<5>() { set_test_name("LLEventThrottle"); + skip("time testing needs reworking"); +#if 0 // time testing needs reworking TestEventThrottle throttle(3); Concat cat; throttle.listen("concat", boost::ref(cat)); @@ -403,6 +416,7 @@ namespace tut throttle.advance(5); throttle.post(";17"); ensure_equals("17", cat.result, "136;12;17"); // "17" delivered +#endif // time testing needs reworking } template<class PUMP> diff --git a/indra/llcommon/tests/llleap_test.cpp b/indra/llcommon/tests/llleap_test.cpp index fa48bcdefd..ca1939c81e 100644 --- a/indra/llcommon/tests/llleap_test.cpp +++ b/indra/llcommon/tests/llleap_test.cpp @@ -18,6 +18,7 @@ #include <functional> // external library headers // other Linden headers +#include "StringVec.h" #include "../test/lltut.h" #include "../test/namedtempfile.h" #include "../test/catch_and_store_what_in.h" @@ -26,7 +27,6 @@ #include "llprocess.h" #include "llstring.h" #include "stringize.h" -#include "StringVec.h" #if defined(LL_WINDOWS) #define sleep(secs) _sleep((secs) * 1000) @@ -385,8 +385,7 @@ namespace tut "result = '' if resp == dict(pump=replypump(), data='ack')\\\n" " else 'bad: ' + str(resp)\n" "send(pump='" << result.getName() << "', data=result)\n";}); - waitfor(LLLeap::create(get_test_name(), - StringVec{PYTHON, script.getName()})); + waitfor(LLLeap::create(get_test_name(), StringVec{PYTHON, script.getName()})); result.ensure(); } diff --git a/indra/llcommon/tests/llmainthreadtask_test.cpp b/indra/llcommon/tests/llmainthreadtask_test.cpp index 9ccf391327..ea4232ad78 100644 --- a/indra/llcommon/tests/llmainthreadtask_test.cpp +++ b/indra/llcommon/tests/llmainthreadtask_test.cpp @@ -20,8 +20,8 @@ // other Linden headers #include "../test/lltut.h" #include "../test/sync.h" +#include "llcallbacklist.h" #include "llthread.h" // on_main_thread() -#include "lleventtimer.h" #include "lockstatic.h" /***************************************************************************** @@ -108,7 +108,7 @@ namespace tut lk.unlock(); // run the task -- should unblock thread, which will immediately block // on mSync - LLEventTimer::updateClass(); + LLCallbackList::instance().callFunctions(); // 'lk', having unlocked, can no longer be used to access; relock with // a new LockStatic instance ensure("should now have run", LockStatic()->ran); diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 6e8422ca0c..9b3b345217 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -259,6 +259,7 @@ public: } std::string getName() const { return mPath.string(); } + std::string getNormalName() const { return mPath.lexically_normal().make_preferred().string(); } private: boost::filesystem::path mPath; @@ -590,7 +591,7 @@ namespace tut " f.write(os.path.normcase(os.path.normpath(os.getcwd())))\n"); // Before running, call setWorkingDirectory() py.mParams.cwd = tempdir.getName(); - std::string expected{ tempdir.getName() }; + std::string expected{ tempdir.getNormalName() }; #if LL_WINDOWS // SIGH, don't get tripped up by "C:" != "c:" -- // but on the Mac, using tolower() fails because "/users" != "/Users"! diff --git a/indra/llcommon/tests/llsdserialize_test.cpp b/indra/llcommon/tests/llsdserialize_test.cpp index fae9f7023f..ff0607f503 100644 --- a/indra/llcommon/tests/llsdserialize_test.cpp +++ b/indra/llcommon/tests/llsdserialize_test.cpp @@ -44,19 +44,17 @@ typedef U32 uint32_t; #include "llstring.h" #endif -#include "boost/range.hpp" - #include "llsd.h" #include "llsdserialize.h" #include "llsdutil.h" #include "llformat.h" #include "llmemorystream.h" -#include "../test/hexdump.h" +#include "hexdump.h" +#include "StringVec.h" #include "../test/lltut.h" #include "../test/namedtempfile.h" #include "stringize.h" -#include "StringVec.h" #include <functional> typedef std::function<void(const LLSD& data, std::ostream& str)> FormatterFunction; @@ -1921,12 +1919,12 @@ namespace tut int bufflen{ static_cast<int>(buffstr.length()) }; out.write(reinterpret_cast<const char*>(&bufflen), sizeof(bufflen)); LL_DEBUGS() << "Wrote length: " - << hexdump(reinterpret_cast<const char*>(&bufflen), - sizeof(bufflen)) + << LL::hexdump(reinterpret_cast<const char*>(&bufflen), + sizeof(bufflen)) << LL_ENDL; out.write(buffstr.c_str(), buffstr.length()); LL_DEBUGS() << "Wrote data: " - << hexmix(buffstr.c_str(), buffstr.length()) + << LL::hexmix(buffstr.c_str(), buffstr.length()) << LL_ENDL; } } @@ -2097,7 +2095,8 @@ namespace tut NamedTempFile file("llsd", ""); python("Python " + pyformatter, - [&](std::ostream& out){ out << + [pyformatter, &file](std::ostream& out) { + out << import_llsd << "import struct\n" "lenformat = struct.Struct('i')\n" diff --git a/indra/llcommon/threadpool.cpp b/indra/llcommon/threadpool.cpp index 451e60c083..c94b5eecce 100644 --- a/indra/llcommon/threadpool.cpp +++ b/indra/llcommon/threadpool.cpp @@ -18,6 +18,7 @@ // external library headers // other Linden headers #include "commoncontrol.h" +#include "llcoros.h" #include "llerror.h" #include "llevents.h" #include "llsd.h" @@ -90,20 +91,14 @@ void LL::ThreadPoolBase::start() return; } - // Listen on "LLApp", and when the app is shutting down, close the queue - // and join the workers. - LLEventPumps::instance().obtain("LLApp").listen( + // When the app is shutting down, close the queue and join the workers. + mStopListener = LLCoros::getStopListener( mName, - [this](const LLSD& stat) + [this](const LLSD& status) { - std::string status(stat["status"]); - if (status != "running") - { - // viewer is starting shutdown -- proclaim the end is nigh! - LL_DEBUGS("ThreadPool") << mName << " saw " << status << LL_ENDL; - close(); - } - return false; + // viewer is starting shutdown -- proclaim the end is nigh! + LL_DEBUGS("ThreadPool") << mName << " saw " << status << LL_ENDL; + close(); }); } @@ -118,20 +113,19 @@ LL::ThreadPoolBase::~ThreadPoolBase() void LL::ThreadPoolBase::close() { - if (! mQueue->isClosed()) + // mQueue might have been closed already, but in any case we must join or + // detach each of our threads before destroying the mThreads vector. + LL_DEBUGS("ThreadPool") << mName << " closing queue and joining threads" << LL_ENDL; + mQueue->close(); + for (auto& pair: mThreads) { - LL_DEBUGS("ThreadPool") << mName << " closing queue and joining threads" << LL_ENDL; - mQueue->close(); - for (auto& pair: mThreads) + if (pair.second.joinable()) { - if (pair.second.joinable()) - { - LL_DEBUGS("ThreadPool") << mName << " waiting on thread " << pair.first << LL_ENDL; - pair.second.join(); - } + LL_DEBUGS("ThreadPool") << mName << " waiting on thread " << pair.first << LL_ENDL; + pair.second.join(); } - LL_DEBUGS("ThreadPool") << mName << " shutdown complete" << LL_ENDL; } + LL_DEBUGS("ThreadPool") << mName << " shutdown complete" << LL_ENDL; } void LL::ThreadPoolBase::run(const std::string& name) diff --git a/indra/llcommon/threadpool.h b/indra/llcommon/threadpool.h index 0eb1891754..2748d7b073 100644 --- a/indra/llcommon/threadpool.h +++ b/indra/llcommon/threadpool.h @@ -13,6 +13,7 @@ #if ! defined(LL_THREADPOOL_H) #define LL_THREADPOOL_H +#include "llcoros.h" #include "threadpool_fwd.h" #include "workqueue.h" #include <memory> // std::unique_ptr @@ -52,8 +53,8 @@ namespace LL void start(); /** - * ThreadPool listens for application shutdown messages on the "LLApp" - * LLEventPump. Call close() to shut down this ThreadPool early. + * ThreadPool listens for application shutdown events. Call close() to + * shut down this ThreadPool early. */ virtual void close(); @@ -95,6 +96,7 @@ namespace LL std::string mName; size_t mThreadCount; + LLTempBoundListener mStopListener; }; /** diff --git a/indra/llcommon/throttle.cpp b/indra/llcommon/throttle.cpp new file mode 100644 index 0000000000..deb0a26fb0 --- /dev/null +++ b/indra/llcommon/throttle.cpp @@ -0,0 +1,34 @@ +/** + * @file throttle.cpp + * @author Nat Goodspeed + * @date 2024-08-12 + * @brief Implementation for ThrottleBase. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "throttle.h" +// STL headers +// std headers +// external library headers +// other Linden headers +#include "lltimer.h" + +bool ThrottleBase::too_fast() +{ + F64 now = LLTimer::getElapsedSeconds(); + if (now < mNext) + { + return true; + } + else + { + mNext = now + mInterval; + return false; + } +} diff --git a/indra/llcommon/throttle.h b/indra/llcommon/throttle.h new file mode 100644 index 0000000000..7f5389f464 --- /dev/null +++ b/indra/llcommon/throttle.h @@ -0,0 +1,138 @@ +/** + * @file throttle.h + * @author Nat Goodspeed + * @date 2024-08-12 + * @brief Throttle class + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_THROTTLE_H) +#define LL_THROTTLE_H + +#include "apply.h" // LL::bind_front() +#include "llerror.h" +#include <functional> +#include <iomanip> // std::quoted() + +class ThrottleBase +{ +public: + ThrottleBase(F64 interval): + mInterval(interval) + {} + +protected: + bool too_fast(); // not const: we update mNext + F64 mInterval, mNext{ 0. }; +}; + +/** + * An instance of Throttle mediates calls to some other specified function, + * ensuring that it's called no more often than the specified time interval. + * Throttle is an abstract base class that delegates the behavior when the + * specified interval is exceeded. + */ +template <typename SIGNATURE> +class Throttle: public ThrottleBase +{ +public: + Throttle(const std::string& desc, + const std::function<SIGNATURE>& func, + F64 interval): + ThrottleBase(interval), + mDesc(desc), + mFunc(func) + {} + // Constructing Throttle with a member function pointer but without an + // instance pointer requires you to pass the instance pointer/reference as + // the first argument to operator()(). + template <typename R, class C> + Throttle(const std::string& desc, R C::* method, F64 interval): + Throttle(desc, std::mem_fn(method), interval) + {} + template <typename R, class C> + Throttle(const std::string& desc, R C::* method, C* instance, F64 interval): + Throttle(desc, LL::bind_front(method, instance), interval) + {} + template <typename R, class C> + Throttle(const std::string& desc, R C::* method, const C* instance, F64 interval): + Throttle(desc, LL::bind_front(method, instance), interval) + {} + virtual ~Throttle() {} + + template <typename... ARGS> + auto operator()(ARGS... args) + { + if (too_fast()) + { + suppress(); + using rtype = decltype(mFunc(std::forward<ARGS>(args)...)); + if constexpr (! std::is_same_v<rtype, void>) + { + return rtype{}; + } + } + else + { + return mFunc(std::forward<ARGS>(args)...); + } + } + +protected: + // override with desired behavior when calls come too often + virtual void suppress() = 0; + const std::string mDesc; + +private: + std::function<SIGNATURE> mFunc; +}; + +/** + * An instance of LogThrottle mediates calls to some other specified function, + * ensuring that it's called no more often than the specified time interval. + * When that interval is exceeded, it logs a message at the specified log + * level. It uses LL_MUMBLES_ONCE() logic to prevent spamming, since a too- + * frequent call may well be spammy. + */ +template <LLError::ELevel LOGLEVEL, typename SIGNATURE> +class LogThrottle: public Throttle<SIGNATURE> +{ + using super = Throttle<SIGNATURE>; +public: + LogThrottle(const std::string& desc, + const std::function<SIGNATURE>& func, + F64 interval): + super(desc, func, interval) + {} + template <typename R, class C> + LogThrottle(const std::string& desc, R C::* method, F64 interval): + super(desc, method, interval) + {} + template <typename R, class C> + LogThrottle(const std::string& desc, R C::* method, C* instance, F64 interval): + super(desc, method, instance, interval) + {} + template <typename R, class C> + LogThrottle(const std::string& desc, R C::* method, const C* instance, F64 interval): + super(desc, method, instance, interval) + {} + +private: + void suppress() override + { + // Using lllog(), the macro underlying LL_WARNS() et al., allows + // specifying compile-time LOGLEVEL. It does NOT support a variable + // LOGLEVEL, which is why LOGLEVEL is a non-type template parameter. + // See llvlog() for variable support, which is a bit more expensive. + // true = only print the log message once + lllog(LOGLEVEL, true, "LogThrottle") << std::quoted(super::mDesc) + << " called more than once per " + << super::mInterval + << LL_ENDL; + } +}; + +#endif /* ! defined(LL_THROTTLE_H) */ diff --git a/indra/llcommon/workqueue.cpp b/indra/llcommon/workqueue.cpp index 6066e74fb5..9138c862f9 100644 --- a/indra/llcommon/workqueue.cpp +++ b/indra/llcommon/workqueue.cpp @@ -32,8 +32,11 @@ using Lock = LLCoros::LockType; LL::WorkQueueBase::WorkQueueBase(const std::string& name): super(makeName(name)) { - // TODO: register for "LLApp" events so we can implicitly close() on - // viewer shutdown. + // Register for status change events so we'll implicitly close() on viewer + // shutdown. + mStopListener = LLCoros::getStopListener( + "WorkQueue:" + getKey(), + [this](const LLSD&){ close(); }); } void LL::WorkQueueBase::runUntilClose() diff --git a/indra/llcommon/workqueue.h b/indra/llcommon/workqueue.h index 9d7bbfbf7a..eb6923df0b 100644 --- a/indra/llcommon/workqueue.h +++ b/indra/llcommon/workqueue.h @@ -13,6 +13,7 @@ #define LL_WORKQUEUE_H #include "llcoros.h" +#include "llevents.h" #include "llexception.h" #include "llinstancetracker.h" #include "llinstancetrackersubclass.h" @@ -116,16 +117,29 @@ namespace LL ARGS&&... args); /** - * Post work to another WorkQueue, blocking the calling coroutine - * until then, returning the result to caller on completion. Optional - * final argument is TimePoint for WorkSchedule. + * Post work, blocking the calling coroutine, returning the result to + * caller on completion. Optional final argument is TimePoint for + * WorkSchedule. * * In general, we assume that each thread's default coroutine is busy * servicing its WorkQueue or whatever. To try to prevent mistakes, we * forbid calling waitForResult() from a thread's default coroutine. */ template <typename CALLABLE, typename... ARGS> - auto waitForResult(CALLABLE&& callable, ARGS&&... args); + auto waitForResult(CALLABLE&& callable, ARGS&&... args) + { + checkCoroutine("waitForResult()"); + return waitForResult_(std::forward<CALLABLE>(callable), + std::forward<ARGS>(args)...); + } + + /** + * Post work, blocking the calling coroutine, returning the result to + * caller on completion. Optional final argument is TimePoint for + * WorkSchedule. + */ + template <typename CALLABLE, typename... ARGS> + auto waitForResult_(CALLABLE&& callable, ARGS&&... args); /*--------------------------- worker API ---------------------------*/ @@ -194,6 +208,8 @@ namespace LL static std::string makeName(const std::string& name); void callWork(const Work& work); + LLTempBoundListener mStopListener; + private: virtual Work pop_() = 0; virtual bool tryPop_(Work&) = 0; @@ -359,6 +375,10 @@ namespace LL * CALLABLE that returns bool, a TimePoint and an interval at which to * relaunch it. As long as the callable continues returning true, BackJack * keeps resubmitting it to the target WorkQueue. + * + * "You go back, Jack, and do it again -- wheel turnin' round and round..." + * --Steely Dan, from "Can't Buy a Thrill" (1972) + * https://www.youtube.com/watch?v=yCgHTmv4YU8 */ // Why is BackJack a class and not a lambda? Because, unlike a lambda, a // class method gets its own 'this' pointer -- which we need to resubmit @@ -525,7 +545,7 @@ namespace LL reply, // Bind the current exception to transport back to the // originating WorkQueue. Once there, rethrow it. - [exc = std::current_exception()](){ std::rethrow_exception(exc); }); + [exc = std::current_exception()]{ std::rethrow_exception(exc); }); } }, // if caller passed a TimePoint, pass it along to post() @@ -618,9 +638,8 @@ namespace LL }; template <typename CALLABLE, typename... ARGS> - auto WorkQueueBase::waitForResult(CALLABLE&& callable, ARGS&&... args) + auto WorkQueueBase::waitForResult_(CALLABLE&& callable, ARGS&&... args) { - checkCoroutine("waitForResult()"); // derive callable's return type so we can specialize for void return WaitForResult<CALLABLE, decltype(std::forward<CALLABLE>(callable)())>() (this, std::forward<CALLABLE>(callable), std::forward<ARGS>(args)...); diff --git a/indra/llfilesystem/lldir.cpp b/indra/llfilesystem/lldir.cpp index 99d4850610..2818aaf86c 100644 --- a/indra/llfilesystem/lldir.cpp +++ b/indra/llfilesystem/lldir.cpp @@ -468,6 +468,7 @@ static std::string ELLPathToString(ELLPath location) ENT(LL_PATH_DEFAULT_SKIN) ENT(LL_PATH_FONTS) ENT(LL_PATH_LAST) + ENT(LL_PATH_SCRIPTS) ; #undef ENT @@ -588,6 +589,10 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd prefix = add(getAppRODataDir(), "fonts"); break; + case LL_PATH_SCRIPTS: + prefix = add(getAppRODataDir(), "scripts"); + break; + default: llassert(0); } diff --git a/indra/llfilesystem/lldir.h b/indra/llfilesystem/lldir.h index b0d2b6aada..241f151d47 100644 --- a/indra/llfilesystem/lldir.h +++ b/indra/llfilesystem/lldir.h @@ -49,6 +49,7 @@ typedef enum ELLPath LL_PATH_DEFAULT_SKIN = 17, LL_PATH_FONTS = 18, LL_PATH_DUMP = 19, + LL_PATH_SCRIPTS = 20, LL_PATH_LAST } ELLPath; diff --git a/indra/llinventory/llfoldertype.cpp b/indra/llinventory/llfoldertype.cpp index 7e1be17ecc..74eb801cfd 100644 --- a/indra/llinventory/llfoldertype.cpp +++ b/indra/llinventory/llfoldertype.cpp @@ -29,6 +29,7 @@ #include "llfoldertype.h" #include "lldictionary.h" #include "llmemory.h" +#include "llsd.h" #include "llsingleton.h" ///---------------------------------------------------------------------------- @@ -220,3 +221,21 @@ const std::string &LLFolderType::badLookup() static const std::string sBadLookup = "llfoldertype_bad_lookup"; return sBadLookup; } + +LLSD LLFolderType::getTypeNames() +{ + LLSD type_names; + const LLFolderDictionary *dict = LLFolderDictionary::getInstance(); + for (S32 type = 0; type < FT_COUNT; ++type) + { + if (lookupIsEnsembleType(LLFolderType::EType(type))) continue; + + const FolderEntry *entry = dict->lookup(LLFolderType::EType(type)); + //skip llfoldertype_bad_lookup + if (entry) + { + type_names.append(entry->mName); + } + } + return type_names; +} diff --git a/indra/llinventory/llfoldertype.h b/indra/llinventory/llfoldertype.h index 46a1b92a96..dd12693f66 100644 --- a/indra/llinventory/llfoldertype.h +++ b/indra/llinventory/llfoldertype.h @@ -115,6 +115,8 @@ public: static const std::string& badLookup(); // error string when a lookup fails + static LLSD getTypeNames(); + protected: LLFolderType() {} ~LLFolderType() {} diff --git a/indra/llmath/tests/mathmisc_test.cpp b/indra/llmath/tests/mathmisc_test.cpp index ff0899e975..2b23987616 100644 --- a/indra/llmath/tests/mathmisc_test.cpp +++ b/indra/llmath/tests/mathmisc_test.cpp @@ -709,14 +709,34 @@ namespace tut first_plane, second_plane); - ensure("plane intersection should succeed", success); + try + { + ensure("plane intersection should succeed", success); - F32 dot = fabs(known_intersection.getDirection() * measured_intersection.getDirection()); - ensure("measured intersection should be parallel to known intersection", - dot > ALMOST_PARALLEL); + F32 dot = fabs(known_intersection.getDirection() * measured_intersection.getDirection()); + ensure("measured intersection should be parallel to known intersection", + dot > ALMOST_PARALLEL); - ensure("measured intersection should pass near known point", - measured_intersection.intersects(some_point, LARGE_RADIUS * allowable_relative_error)); + ensure("measured intersection should pass near known point", + measured_intersection.intersects(some_point, LARGE_RADIUS * allowable_relative_error)); + } + catch (const failure&) + { + // If any of these assertions fail, since the values involved + // are randomly generated, unless we report them, we have no + // hope of diagnosing the problem. + LL_INFOS() << "some_point = " << some_point << '\n' + << "another_point = " << another_point << '\n' + << "known_intersection = " << known_intersection << '\n' + << "point_on_plane = " << point_on_plane << '\n' + << "plane_normal = " << plane_normal << '\n' + << "first_plane = " << first_plane << '\n' + << "point_on_different_plane = " << point_on_different_plane << '\n' + << "different_plane_normal = " << different_plane_normal << '\n' + << "second_plane = " << second_plane << '\n' + << "measured_intersection = " << measured_intersection << LL_ENDL; + throw; + } } } } diff --git a/indra/llmeshoptimizer/llmeshoptimizer.cpp b/indra/llmeshoptimizer/llmeshoptimizer.cpp index 7339454367..9d62a72188 100644 --- a/indra/llmeshoptimizer/llmeshoptimizer.cpp +++ b/indra/llmeshoptimizer/llmeshoptimizer.cpp @@ -57,7 +57,7 @@ void LLMeshOptimizer::generateShadowIndexBufferU32(U32 *destination, S32 index = 0; if (vertex_positions) { - streams[index].data = (const float*)vertex_positions; + streams[index].data = vertex_positions->getF32ptr(); // Despite being LLVector4a, only x, y and z are in use streams[index].size = sizeof(F32) * 3; streams[index].stride = sizeof(F32) * 4; @@ -65,14 +65,14 @@ void LLMeshOptimizer::generateShadowIndexBufferU32(U32 *destination, } if (normals) { - streams[index].data = (const float*)normals; + streams[index].data = normals->getF32ptr(); streams[index].size = sizeof(F32) * 3; streams[index].stride = sizeof(F32) * 4; index++; } if (text_coords) { - streams[index].data = (const float*)text_coords; + streams[index].data = text_coords->mV; streams[index].size = sizeof(F32) * 2; streams[index].stride = sizeof(F32) * 2; index++; @@ -108,21 +108,21 @@ void LLMeshOptimizer::generateShadowIndexBufferU16(U16 *destination, S32 index = 0; if (vertex_positions) { - streams[index].data = (const float*)vertex_positions; + streams[index].data = vertex_positions->getF32ptr(); streams[index].size = sizeof(F32) * 3; streams[index].stride = sizeof(F32) * 4; index++; } if (normals) { - streams[index].data = (const float*)normals; + streams[index].data = normals->getF32ptr(); streams[index].size = sizeof(F32) * 3; streams[index].stride = sizeof(F32) * 4; index++; } if (text_coords) { - streams[index].data = (const float*)text_coords; + streams[index].data = text_coords->mV; streams[index].size = sizeof(F32) * 2; streams[index].stride = sizeof(F32) * 2; index++; @@ -162,9 +162,9 @@ size_t LLMeshOptimizer::generateRemapMultiU32( U64 vertex_count) { meshopt_Stream streams[] = { - {(const float*)vertex_positions, sizeof(F32) * 3, sizeof(F32) * 4}, - {(const float*)normals, sizeof(F32) * 3, sizeof(F32) * 4}, - {(const float*)text_coords, sizeof(F32) * 2, sizeof(F32) * 2}, + {vertex_positions->getF32ptr(), sizeof(F32) * 3, sizeof(F32) * 4}, + {normals->getF32ptr(), sizeof(F32) * 3, sizeof(F32) * 4}, + {text_coords->mV, sizeof(F32) * 2, sizeof(F32) * 2}, }; // Remap can function without indices, @@ -236,7 +236,7 @@ void LLMeshOptimizer::remapPositionsBuffer(LLVector4a * destination_vertices, U64 vertex_count, const unsigned int* remap) { - meshopt_remapVertexBuffer((float*)destination_vertices, (const float*)vertex_positions, vertex_count, sizeof(LLVector4a), remap); + meshopt_remapVertexBuffer(destination_vertices->getF32ptr(), vertex_positions->getF32ptr(), vertex_count, sizeof(LLVector4a), remap); } void LLMeshOptimizer::remapNormalsBuffer(LLVector4a * destination_normalss, @@ -244,7 +244,7 @@ void LLMeshOptimizer::remapNormalsBuffer(LLVector4a * destination_normalss, U64 mormals_count, const unsigned int* remap) { - meshopt_remapVertexBuffer((float*)destination_normalss, (const float*)normals, mormals_count, sizeof(LLVector4a), remap); + meshopt_remapVertexBuffer(destination_normalss->getF32ptr(), normals->getF32ptr(), mormals_count, sizeof(LLVector4a), remap); } void LLMeshOptimizer::remapUVBuffer(LLVector2 * destination_uvs, @@ -252,7 +252,7 @@ void LLMeshOptimizer::remapUVBuffer(LLVector2 * destination_uvs, U64 uv_count, const unsigned int* remap) { - meshopt_remapVertexBuffer((float*)destination_uvs, (const float*)uv_positions, uv_count, sizeof(LLVector2), remap); + meshopt_remapVertexBuffer(destination_uvs->mV, uv_positions->mV, uv_count, sizeof(LLVector2), remap); } //static @@ -273,7 +273,7 @@ U64 LLMeshOptimizer::simplifyU32(U32 *destination, return meshopt_simplifySloppy<unsigned int>(destination, indices, index_count, - (const float*)vertex_positions, + vertex_positions->getF32ptr(), vertex_count, vertex_positions_stride, target_index_count, @@ -286,7 +286,7 @@ U64 LLMeshOptimizer::simplifyU32(U32 *destination, return meshopt_simplify<unsigned int>(destination, indices, index_count, - (const float*)vertex_positions, + vertex_positions->getF32ptr(), vertex_count, vertex_positions_stride, target_index_count, @@ -315,7 +315,7 @@ U64 LLMeshOptimizer::simplify(U16 *destination, return meshopt_simplifySloppy<unsigned short>(destination, indices, index_count, - (const float*)vertex_positions, + vertex_positions->getF32ptr(), vertex_count, vertex_positions_stride, target_index_count, @@ -328,7 +328,7 @@ U64 LLMeshOptimizer::simplify(U16 *destination, return meshopt_simplify<unsigned short>(destination, indices, index_count, - (const float*)vertex_positions, + vertex_positions->getF32ptr(), vertex_count, vertex_positions_stride, target_index_count, diff --git a/indra/llmessage/llcoproceduremanager.cpp b/indra/llmessage/llcoproceduremanager.cpp index 263670bdac..13972ad399 100644 --- a/indra/llmessage/llcoproceduremanager.cpp +++ b/indra/llmessage/llcoproceduremanager.cpp @@ -307,25 +307,20 @@ LLCoprocedurePool::LLCoprocedurePool(const std::string &poolName, size_t size): { try { - // store in our LLTempBoundListener so that when the LLCoprocedurePool is - // destroyed, we implicitly disconnect from this LLEventPump - // Monitores application status - mStatusListener = LLEventPumps::instance().obtain("LLApp").listen( + // Store in our LLTempBoundListener so that when the LLCoprocedurePool is + // destroyed, we implicitly disconnect from this LLEventPump. + // Monitors application status. + mStatusListener = LLCoros::getStopListener( poolName + "_pool", // Make sure it won't repeat names from lleventcoro - [pendingCoprocs = mPendingCoprocs, poolName](const LLSD& status) - { - auto& statsd = status["status"]; - if (statsd.asString() != "running") + [pendingCoprocs = mPendingCoprocs, poolName](const LLSD& event) { LL_INFOS("CoProcMgr") << "Pool " << poolName - << " closing queue because status " << statsd + << " closing queue because status " << event << LL_ENDL; // This should ensure that all waiting coprocedures in this // pool will wake up and terminate. pendingCoprocs->close(); - } - return false; - }); + }); } catch (const LLEventPump::DupListenerName &) { @@ -334,7 +329,7 @@ LLCoprocedurePool::LLCoprocedurePool(const std::string &poolName, size_t size): // // If this somehow happens again it is better to crash later on shutdown due to pump // not stopping coroutine and see warning in logs than on startup or during login. - LL_WARNS("CoProcMgr") << "Attempted to register dupplicate listener name: " << poolName + LL_WARNS("CoProcMgr") << "Attempted to register duplicate listener name: " << poolName << "_pool. Failed to start listener." << LL_ENDL; llassert(0); // Fix Me! Ignoring missing listener! diff --git a/indra/llplugin/llpluginprocessparent.cpp b/indra/llplugin/llpluginprocessparent.cpp index bd1e19c294..00cc6902d5 100644 --- a/indra/llplugin/llpluginprocessparent.cpp +++ b/indra/llplugin/llpluginprocessparent.cpp @@ -48,7 +48,7 @@ LLPluginProcessParentOwner::~LLPluginProcessParentOwner() bool LLPluginProcessParent::sUseReadThread = false; apr_pollset_t *LLPluginProcessParent::sPollSet = NULL; bool LLPluginProcessParent::sPollsetNeedsRebuild = false; -LLCoros::Mutex *LLPluginProcessParent::sInstancesMutex; +llcoro::Mutex *LLPluginProcessParent::sInstancesMutex; LLPluginProcessParent::mapInstances_t LLPluginProcessParent::sInstances; LLThread *LLPluginProcessParent::sReadThread = NULL; @@ -87,7 +87,7 @@ LLPluginProcessParent::LLPluginProcessParent(LLPluginProcessParentOwner *owner): { if(!sInstancesMutex) { - sInstancesMutex = new LLCoros::Mutex(); + sInstancesMutex = new llcoro::Mutex(); } mOwner = owner; @@ -145,7 +145,7 @@ LLPluginProcessParent::ptr_t LLPluginProcessParent::create(LLPluginProcessParent // Don't add to the global list until fully constructed. { - LLCoros::LockType lock(*sInstancesMutex); + llcoro::LockType lock(*sInstancesMutex); sInstances.insert(mapInstances_t::value_type(that.get(), that)); } @@ -155,7 +155,7 @@ LLPluginProcessParent::ptr_t LLPluginProcessParent::create(LLPluginProcessParent /*static*/ void LLPluginProcessParent::shutdown() { - LLCoros::LockType lock(*sInstancesMutex); + llcoro::LockType lock(*sInstancesMutex); mapInstances_t::iterator it; for (it = sInstances.begin(); it != sInstances.end(); ++it) @@ -213,7 +213,7 @@ bool LLPluginProcessParent::pollTick() { // this grabs a copy of the smart pointer to ourselves to ensure that we do not // get destroyed until after this method returns. - LLCoros::LockType lock(*sInstancesMutex); + llcoro::LockType lock(*sInstancesMutex); mapInstances_t::iterator it = sInstances.find(this); if (it != sInstances.end()) that = (*it).second; @@ -232,7 +232,7 @@ void LLPluginProcessParent::removeFromProcessing() // Remove from the global list before beginning destruction. { // Make sure to get the global mutex _first_ here, to avoid a possible deadlock against LLPluginProcessParent::poll() - LLCoros::LockType lock(*sInstancesMutex); + llcoro::LockType lock(*sInstancesMutex); { LLMutexLock lock2(&mIncomingQueueMutex); sInstances.erase(this); @@ -817,7 +817,7 @@ void LLPluginProcessParent::updatePollset() return; } - LLCoros::LockType lock(*sInstancesMutex); + llcoro::LockType lock(*sInstancesMutex); if(sPollSet) { @@ -940,7 +940,7 @@ void LLPluginProcessParent::poll(F64 timeout) mapInstances_t::iterator it; { - LLCoros::LockType lock(*sInstancesMutex); + llcoro::LockType lock(*sInstancesMutex); it = sInstances.find(thatId); if (it != sInstances.end()) that = (*it).second; diff --git a/indra/llplugin/llpluginprocessparent.h b/indra/llplugin/llpluginprocessparent.h index 334f1411af..6422fc21f3 100644 --- a/indra/llplugin/llpluginprocessparent.h +++ b/indra/llplugin/llpluginprocessparent.h @@ -33,6 +33,7 @@ #include <boost/enable_shared_from_this.hpp> #include "llapr.h" +#include "llcoromutex.h" #include "llprocess.h" #include "llpluginmessage.h" #include "llpluginmessagepipe.h" @@ -200,7 +201,7 @@ private: apr_pollfd_t mPollFD; static apr_pollset_t *sPollSet; static bool sPollsetNeedsRebuild; - static LLCoros::Mutex *sInstancesMutex; + static llcoro::Mutex *sInstancesMutex; static mapInstances_t sInstances; static void dirtyPollSet(); static void updatePollset(); diff --git a/indra/llui/CMakeLists.txt b/indra/llui/CMakeLists.txt index a0314cb5f2..69e1b57245 100644 --- a/indra/llui/CMakeLists.txt +++ b/indra/llui/CMakeLists.txt @@ -49,6 +49,7 @@ set(llui_SOURCE_FILES lllineeditor.cpp llloadingindicator.cpp lllocalcliprect.cpp + llluafloater.cpp llmenubutton.cpp llmenugl.cpp llmodaldialog.cpp @@ -164,6 +165,7 @@ set(llui_HEADER_FILES lllineeditor.h llloadingindicator.h lllocalcliprect.h + llluafloater.h llmenubutton.h llmenugl.h llmodaldialog.h diff --git a/indra/llui/llchat.h b/indra/llui/llchat.h index 5f75ed2f8d..8adb3a87ef 100644 --- a/indra/llui/llchat.h +++ b/indra/llui/llchat.h @@ -89,7 +89,8 @@ public: mPosAgent(), mURL(), mChatStyle(CHAT_STYLE_NORMAL), - mSessionID() + mSessionID(), + mIsScript(false) { } std::string mText; // UTF-8 line of text @@ -107,6 +108,22 @@ public: std::string mURL; EChatStyle mChatStyle; LLUUID mSessionID; + + bool mIsScript; }; +static const std::string LUA_PREFIX("[LUA]"); + +inline +std::string without_LUA_PREFIX(const std::string& string, bool is_lua) +{ + if (is_lua) + { + return string.substr(LUA_PREFIX.size()); + } + else + { + return string; + } +} #endif diff --git a/indra/llui/llcommandmanager.cpp b/indra/llui/llcommandmanager.cpp index 03717da80b..812a360190 100644 --- a/indra/llui/llcommandmanager.cpp +++ b/indra/llui/llcommandmanager.cpp @@ -32,6 +32,7 @@ #include "llcommandmanager.h" #include "lldir.h" #include "llerror.h" +#include "llsdutil.h" #include "llxuiparser.h" @@ -189,3 +190,8 @@ bool LLCommandManager::load() return true; } + +LLSD LLCommandManager::getCommandNames() +{ + return llsd::toArray(mCommands, [](const auto &cmd) { return cmd->name(); }); + } diff --git a/indra/llui/llcommandmanager.h b/indra/llui/llcommandmanager.h index e6df0d3a4b..69d631a398 100644 --- a/indra/llui/llcommandmanager.h +++ b/indra/llui/llcommandmanager.h @@ -192,6 +192,8 @@ public: LLCommand * getCommand(const LLCommandId& commandId); LLCommand * getCommand(const std::string& name); + LLSD getCommandNames(); + static bool load(); protected: diff --git a/indra/llui/llflashtimer.cpp b/indra/llui/llflashtimer.cpp index c3db24c987..2711e8088d 100644 --- a/indra/llui/llflashtimer.cpp +++ b/indra/llui/llflashtimer.cpp @@ -35,7 +35,7 @@ LLFlashTimer::LLFlashTimer(callback_t cb, S32 count, F32 period) mIsCurrentlyHighlighted(false), mUnset(false) { - mEventTimer.stop(); + stop(); // By default use settings from settings.xml to be able change them via Debug settings. See EXT-5973. // Due to Timer is implemented as derived class from EventTimer it is impossible to change period @@ -74,12 +74,12 @@ void LLFlashTimer::startFlashing() { mIsFlashingInProgress = true; mIsCurrentlyHighlighted = true; - mEventTimer.start(); + start(); } void LLFlashTimer::stopFlashing() { - mEventTimer.stop(); + stop(); mIsFlashingInProgress = false; mIsCurrentlyHighlighted = false; mCurrentTickCount = 0; diff --git a/indra/llui/llflashtimer.h b/indra/llui/llflashtimer.h index b55ce53fc0..988b577ed2 100644 --- a/indra/llui/llflashtimer.h +++ b/indra/llui/llflashtimer.h @@ -46,7 +46,7 @@ public: LLFlashTimer(callback_t cb = NULL, S32 count = 0, F32 period = 0.0); ~LLFlashTimer() {}; - /*virtual*/ bool tick(); + bool tick() override; void startFlashing(); void stopFlashing(); diff --git a/indra/llui/llfloaterreg.cpp b/indra/llui/llfloaterreg.cpp index a818e72f59..c4e6061a12 100644 --- a/indra/llui/llfloaterreg.cpp +++ b/indra/llui/llfloaterreg.cpp @@ -624,3 +624,8 @@ U32 LLFloaterReg::getVisibleFloaterInstanceCount() return count; } + +LLSD LLFloaterReg::getFloaterNames() +{ + return llsd::toArray(sGroupMap, [](const auto &pair) { return pair.first; }); +} diff --git a/indra/llui/llfloaterreg.h b/indra/llui/llfloaterreg.h index 94a67c8d8b..2873080c99 100644 --- a/indra/llui/llfloaterreg.h +++ b/indra/llui/llfloaterreg.h @@ -153,6 +153,8 @@ public: static void blockShowFloaters(bool value) { sBlockShowFloaters = value;} static U32 getVisibleFloaterInstanceCount(); + + static LLSD getFloaterNames(); }; #endif diff --git a/indra/llui/llfloaterreglistener.cpp b/indra/llui/llfloaterreglistener.cpp index 17641b8375..6e5f048c27 100644 --- a/indra/llui/llfloaterreglistener.cpp +++ b/indra/llui/llfloaterreglistener.cpp @@ -37,6 +37,8 @@ #include "llfloaterreg.h" #include "llfloater.h" #include "llbutton.h" +#include "llluafloater.h" +#include "resultset.h" LLFloaterRegListener::LLFloaterRegListener(): LLEventAPI("LLFloaterReg", @@ -72,6 +74,18 @@ LLFloaterRegListener::LLFloaterRegListener(): "Simulate clicking the named [\"button\"] in the visible floater named in [\"name\"]", &LLFloaterRegListener::clickButton, requiredNameButton); + + add("showLuaFloater", + "Open the new floater using XML file specified in [\"xml_path\"] with ID in [\"reqid\"]", + &LLLuaFloater::showLuaFloater, {llsd::map("xml_path", LLSD(), "reqid", LLSD())}); + add("getFloaterEvents", + "Return the table of Lua Floater events which are send to the script", + &LLFloaterRegListener::getLuaFloaterEvents); + + add("getFloaterNames", + "Return result set key [\"floaters\"] for names of all registered floaters", + &LLFloaterRegListener::getFloaterNames, + llsd::map("reply", LLSD::String())); } void LLFloaterRegListener::getBuildMap(const LLSD& event) const @@ -113,6 +127,24 @@ void LLFloaterRegListener::instanceVisible(const LLSD& event) const event); } +struct NameResultSet: public LL::ResultSet +{ + NameResultSet(): + LL::ResultSet("floaters"), + mNames(LLFloaterReg::getFloaterNames()) + {} + LLSD mNames; + + int getLength() const override { return narrow(mNames.size()); } + LLSD getSingle(int index) const override { return mNames[index]; } +}; + +void LLFloaterRegListener::getFloaterNames(const LLSD &event) const +{ + auto nameresult = new NameResultSet; + sendReply(llsd::map("floaters", nameresult->getKeyLength()), event); +} + void LLFloaterRegListener::clickButton(const LLSD& event) const { // If the caller requests a reply, build the reply. @@ -154,3 +186,8 @@ void LLFloaterRegListener::clickButton(const LLSD& event) const LLEventPumps::instance().obtain(replyPump).post(reply); } } + +void LLFloaterRegListener::getLuaFloaterEvents(const LLSD &event) const +{ + Response response(llsd::map("events", LLLuaFloater::getEventsData()), event); +} diff --git a/indra/llui/llfloaterreglistener.h b/indra/llui/llfloaterreglistener.h index a36072892c..42e7178cbc 100644 --- a/indra/llui/llfloaterreglistener.h +++ b/indra/llui/llfloaterreglistener.h @@ -49,6 +49,9 @@ private: void toggleInstance(const LLSD& event) const; void instanceVisible(const LLSD& event) const; void clickButton(const LLSD& event) const; + void getFloaterNames(const LLSD &event) const; + + void getLuaFloaterEvents(const LLSD &event) const; }; #endif /* ! defined(LL_LLFLOATERREGLISTENER_H) */ diff --git a/indra/llui/llluafloater.cpp b/indra/llui/llluafloater.cpp new file mode 100644 index 0000000000..ccdadc6ae0 --- /dev/null +++ b/indra/llui/llluafloater.cpp @@ -0,0 +1,324 @@ +/** + * @file llluafloater.cpp + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2024, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + * + */ + +#include "llluafloater.h" + +#include "fsyspath.h" +#include "llevents.h" + +#include "llcheckboxctrl.h" +#include "llcombobox.h" +#include "llscrolllistctrl.h" +#include "lltexteditor.h" + +const std::string LISTENER_NAME("LLLuaFloater"); + +std::set<std::string> EVENT_LIST = { + "commit", + "double_click", + "mouse_enter", + "mouse_leave", + "mouse_down", + "mouse_up", + "right_mouse_down", + "right_mouse_up", + "post_build", + "floater_close", + "keystroke" +}; + +LLLuaFloater::LLLuaFloater(const LLSD &key) : + LLFloater(key), + mDispatchListener(LLUUID::generateNewID().asString(), "action"), + mReplyPumpName(key["reply"].asString()), + mReqID(key) +{ + auto ctrl_lookup = [this](const LLSD &event, std::function<LLSD(LLUICtrl*,const LLSD&)> cb) + { + LLUICtrl *ctrl = getChild<LLUICtrl>(event["ctrl_name"].asString()); + if (!ctrl) + { + LL_WARNS("LuaFloater") << "Control not found: " << event["ctrl_name"] << LL_ENDL; + return LLSD(); + } + return cb(ctrl, event); + }; + + LLSD requiredParams = llsd::map("ctrl_name", LLSD(), "value", LLSD()); + mDispatchListener.add("set_enabled", "", [ctrl_lookup](const LLSD &event) + { + return ctrl_lookup(event, [](LLUICtrl *ctrl, const LLSD &event) { ctrl->setEnabled(event["value"].asBoolean()); return LLSD(); }); + }, requiredParams); + mDispatchListener.add("set_visible", "", [ctrl_lookup](const LLSD &event) + { + return ctrl_lookup(event, [](LLUICtrl *ctrl, const LLSD &event) { ctrl->setVisible(event["value"].asBoolean()); return LLSD(); }); + }, requiredParams); + mDispatchListener.add("set_value", "", [ctrl_lookup](const LLSD &event) + { + return ctrl_lookup(event, [](LLUICtrl *ctrl, const LLSD &event) { ctrl->setValue(event["value"]); return LLSD(); }); + }, requiredParams); + + mDispatchListener.add("add_list_element", "", [this](const LLSD &event) + { + LLScrollListCtrl *ctrl = getChild<LLScrollListCtrl>(event["ctrl_name"].asString()); + if(ctrl) + { + LLSD element_data = event["value"]; + if (element_data.isArray()) + { + for (const auto &row : llsd::inArray(element_data)) + { + ctrl->addElement(row); + } + } + else + { + ctrl->addElement(element_data); + } + } + }, requiredParams); + + mDispatchListener.add("clear_list", "", [this](const LLSD &event) + { + LLScrollListCtrl *ctrl = getChild<LLScrollListCtrl>(event["ctrl_name"].asString()); + if(ctrl) + { + ctrl->deleteAllItems(); + } + }, llsd::map("ctrl_name", LLSD())); + + mDispatchListener.add("add_text", "", [this](const LLSD &event) + { + LLTextEditor *editor = getChild<LLTextEditor>(event["ctrl_name"].asString()); + if (editor) + { + editor->pasteTextWithLinebreaks(stringize(event["value"])); + editor->addLineBreakChar(true); + } + }, requiredParams); + + mDispatchListener.add("set_label", "", [this](const LLSD &event) + { + LLButton *btn = getChild<LLButton>(event["ctrl_name"].asString()); + if (btn) + { + btn->setLabel((event["value"]).asString()); + } + }, requiredParams); + + mDispatchListener.add("set_title", "", [this](const LLSD &event) + { + setTitle(event["value"].asString()); + }, llsd::map("value", LLSD())); + + mDispatchListener.add("get_value", "", [ctrl_lookup](const LLSD &event) + { + return ctrl_lookup(event, [](LLUICtrl *ctrl, const LLSD &event) { return llsd::map("value", ctrl->getValue()); }); + }, llsd::map("ctrl_name", LLSD(), "reqid", LLSD())); + + mDispatchListener.add("get_selected_id", "", [this](const LLSD &event) + { + LLScrollListCtrl *ctrl = getChild<LLScrollListCtrl>(event["ctrl_name"].asString()); + if (!ctrl) + { + LL_WARNS("LuaFloater") << "Control not found: " << event["ctrl_name"] << LL_ENDL; + return LLSD(); + } + return llsd::map("value", ctrl->getCurrentID()); + }, llsd::map("ctrl_name", LLSD(), "reqid", LLSD())); +} + +LLLuaFloater::~LLLuaFloater() +{ + //post empty LLSD() to indicate done, in case it wasn't handled by the script after CLOSE_EVENT + post(LLSD()); +} + +bool LLLuaFloater::postBuild() +{ + for (LLView *view : *getChildList()) + { + LLUICtrl *ctrl = dynamic_cast<LLUICtrl*>(view); + if (ctrl) + { + LLSD data; + data["ctrl_name"] = view->getName(); + + ctrl->setCommitCallback([this, data](LLUICtrl *ctrl, const LLSD ¶m) + { + LLSD event(data); + event["value"] = ctrl->getValue(); + postEvent(event, "commit"); + }); + } + } + + //optional field to send additional specified events to the script + if (mKey.has("extra_events")) + { + //the first value is ctrl name, the second contains array of events to send + for (const auto &[name, data] : llsd::inMap(mKey["extra_events"])) + { + for (const auto &event : llsd::inArray(data)) + { + registerCallback(name, event); + } + } + } + + //send pump name to the script after the floater is built + postEvent(llsd::map("command_name", mDispatchListener.getPumpName()), "post_build"); + + return true; +} + +void LLLuaFloater::onClose(bool app_quitting) +{ + postEvent(llsd::map("app_quitting", app_quitting), "floater_close"); +} + +bool event_is(const std::string &event_name, const std::string &list_event) +{ + llassert(EVENT_LIST.find(list_event) != EVENT_LIST.end()); + return (event_name == list_event); +} + +void LLLuaFloater::registerCallback(const std::string &ctrl_name, const std::string &event) +{ + LLUICtrl *ctrl = getChild<LLUICtrl>(ctrl_name); + if (!ctrl) return; + + LLSD data; + data["ctrl_name"] = ctrl_name; + data["event"] = event; + + auto mouse_event_cb = [this, data](LLUICtrl *ctrl, const LLSD ¶m) { post(data); }; + + auto mouse_event_coords_cb = [this, data](LLUICtrl *ctrl, S32 x, S32 y, MASK mask) + { + LLSD event(data); + post(event.with("x", x).with("y", y)); + }; + + auto post_with_value = [this, data](LLSD value) + { + LLSD event(data); + post(event.with("value", value)); + }; + + if (event_is(event, "mouse_enter")) + { + ctrl->setMouseEnterCallback(mouse_event_cb); + } + else if (event_is(event, "mouse_leave")) + { + ctrl->setMouseLeaveCallback(mouse_event_cb); + } + else if (event_is(event, "mouse_down")) + { + ctrl->setMouseDownCallback(mouse_event_coords_cb); + } + else if (event_is(event, "mouse_up")) + { + ctrl->setMouseUpCallback(mouse_event_coords_cb); + } + else if (event_is(event, "right_mouse_down")) + { + ctrl->setRightMouseDownCallback(mouse_event_coords_cb); + } + else if (event_is(event, "right_mouse_up")) + { + ctrl->setRightMouseUpCallback(mouse_event_coords_cb); + } + else if (event_is(event, "double_click")) + { + LLScrollListCtrl *list = dynamic_cast<LLScrollListCtrl *>(ctrl); + if (list) + { + list->setDoubleClickCallback( [post_with_value, list](){ post_with_value(LLSD(list->getCurrentID())); }); + } + else + { + ctrl->setDoubleClickCallback(mouse_event_coords_cb); + } + } + else if (event_is(event, "keystroke")) + { + LLTextEditor* text_editor = dynamic_cast<LLTextEditor*>(ctrl); + if (text_editor) + { + text_editor->setKeystrokeCallback([post_with_value](LLTextEditor *editor) { post_with_value(editor->getValue()); }); + } + LLLineEditor* line_editor = dynamic_cast<LLLineEditor*>(ctrl); + if (line_editor) + { + line_editor->setKeystrokeCallback([post_with_value](LLLineEditor *editor, void* userdata) { post_with_value(editor->getValue()); }, NULL); + } + } + else + { + LL_WARNS("LuaFloater") << "Can't register callback for unknown event: " << event << " , control: " << ctrl_name << LL_ENDL; + } +} + +void LLLuaFloater::post(const LLSD &data) +{ + // send event data to the script signed with ["reqid"] key + LLSD stamped_data(data); + mReqID.stamp(stamped_data); + LLEventPumps::instance().obtain(mReplyPumpName).post(stamped_data); +} + +void LLLuaFloater::postEvent(LLSD data, const std::string &event_name) +{ + llassert(EVENT_LIST.find(event_name) != EVENT_LIST.end()); + post(data.with("event", event_name)); +} + +void LLLuaFloater::showLuaFloater(const LLSD &data) +{ + fsyspath fs_path(data["xml_path"].asString()); + std::string path = fs_path.lexically_normal().u8string(); + if (!fs_path.is_absolute()) + { + std::string lib_path = gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua"); + path = (fsyspath(lib_path) / path).u8string(); + } + + LLLuaFloater *floater = new LLLuaFloater(data); + floater->buildFromFile(path); + floater->openFloater(floater->getKey()); +} + +LLSD LLLuaFloater::getEventsData() +{ + LLSD event_data; + for (auto &it : EVENT_LIST) + { + event_data.append(it); + } + return event_data; +} diff --git a/indra/llui/llluafloater.h b/indra/llui/llluafloater.h new file mode 100644 index 0000000000..41132f926d --- /dev/null +++ b/indra/llui/llluafloater.h @@ -0,0 +1,54 @@ +/** + * @file llluafloater.h + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2024, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLLUAFLOATER_H +#define LL_LLLUAFLOATER_H + +#include "llfloater.h" +#include "lleventdispatcher.h" +#include "llevents.h" + +class LLLuaFloater : public LLFloater +{ +public: + LLLuaFloater(const LLSD &key); + bool postBuild(); + virtual ~LLLuaFloater(); + + void registerCallback(const std::string &ctrl_name, const std::string &event); + void onClose(bool app_quitting); + + void post(const LLSD &data); + void postEvent(LLSD data, const std::string &event); + static void showLuaFloater(const LLSD &data); + static LLSD getEventsData(); + +private: + LLReqID mReqID; + LLDispatchListener mDispatchListener; + + std::string mReplyPumpName; +}; +#endif diff --git a/indra/llui/llmenugl.cpp b/indra/llui/llmenugl.cpp index 69ffa9a94f..cc770ca90a 100644 --- a/indra/llui/llmenugl.cpp +++ b/indra/llui/llmenugl.cpp @@ -2625,7 +2625,9 @@ void LLMenuGL::insert( S32 position, LLView * ctrl, bool arrange /*= true*/ ) { LLMenuItemGL * item = dynamic_cast<LLMenuItemGL *>(ctrl); - if (NULL == item || position < 0 || position >= mItems.size()) + // If position == size(), std::advance() will return end() -- which is + // okay, because insert(end()) is the same as append(). + if (NULL == item || position < 0 || position > mItems.size()) { return; } diff --git a/indra/llui/llmenugl.h b/indra/llui/llmenugl.h index 66f84393fe..88608b20ab 100644 --- a/indra/llui/llmenugl.h +++ b/indra/llui/llmenugl.h @@ -562,9 +562,6 @@ public: // add a context menu branch bool appendContextSubMenu(LLMenuGL *menu); - const LLFontGL *getFont() const { return mFont; } - - protected: void createSpilloverBranch(); void cleanupSpilloverBranch(); // Add the menu item to this menu. @@ -810,9 +807,10 @@ public: void resetMenuTrigger() { mAltKeyTrigger = false; } -private: // add a menu - this will create a drop down menu. - virtual bool appendMenu( LLMenuGL* menu ); + virtual bool appendMenu(LLMenuGL *menu); + +private: // rearrange the child rects so they fit the shape of the menu // bar. virtual void arrange( void ); @@ -948,16 +946,18 @@ public: LLUICtrl::EnableCallbackRegistry::currentRegistrar().add(name, boost::bind(&view_listener_t::handleEvent, listener, _2)); } - static void addCommit(view_listener_t* listener, const std::string& name) + typedef LLUICtrl::CommitCallbackInfo cb_info; + static void addCommit(view_listener_t *listener, const std::string &name, cb_info::EUntrustedCall handle_untrusted = cb_info::UNTRUSTED_ALLOW) { - LLUICtrl::CommitCallbackRegistry::currentRegistrar().add(name, boost::bind(&view_listener_t::handleEvent, listener, _2)); + LLUICtrl::CommitCallbackRegistry::currentRegistrar().add(name, + cb_info([listener](LLUICtrl*, const LLSD& param){ return listener->handleEvent(param); }, handle_untrusted)); } - static void addMenu(view_listener_t* listener, const std::string& name) + static void addMenu(view_listener_t *listener, const std::string &name, cb_info::EUntrustedCall handle_untrusted = cb_info::UNTRUSTED_ALLOW) { // For now, add to both click and enable registries addEnable(listener, name); - addCommit(listener, name); + addCommit(listener, name, handle_untrusted); } static void cleanup() diff --git a/indra/llui/llnotifications.cpp b/indra/llui/llnotifications.cpp index cd80e7f63f..501ac26f9f 100644 --- a/indra/llui/llnotifications.cpp +++ b/indra/llui/llnotifications.cpp @@ -1243,7 +1243,7 @@ LLNotifications::LLNotifications() mIgnoreAllNotifications(false) { mListener.reset(new LLNotificationsListener(*this)); - LLUICtrl::CommitCallbackRegistry::currentRegistrar().add("Notification.Show", boost::bind(&LLNotifications::addFromCallback, this, _2)); + LLUICtrl::CommitCallbackRegistry::currentRegistrar().add("Notification.Show", { boost::bind(&LLNotifications::addFromCallback, this, _2) }); // touch the instance tracker for notification channels, so that it will still be around in our destructor LLInstanceTracker<LLNotificationChannel, std::string>::instanceCount(); diff --git a/indra/llui/llnotifications.h b/indra/llui/llnotifications.h index 46286457cf..9b83da13ad 100644 --- a/indra/llui/llnotifications.h +++ b/indra/llui/llnotifications.h @@ -735,7 +735,7 @@ typedef std::multimap<std::string, LLNotificationPtr> LLNotificationMap; // all of the built-in tests should attach to the "Visible" channel // class LLNotificationChannelBase : - public LLEventTrackable, + public boost::signals2::trackable, public LLRefCount { LOG_CLASS(LLNotificationChannelBase); diff --git a/indra/llui/llnotificationslistener.cpp b/indra/llui/llnotificationslistener.cpp index 9c1fc27c51..dc12834b3f 100644 --- a/indra/llui/llnotificationslistener.cpp +++ b/indra/llui/llnotificationslistener.cpp @@ -204,7 +204,7 @@ void LLNotificationsListener::ignore(const LLSD& params) const } } -class LLNotificationsListener::Forwarder: public LLEventTrackable +class LLNotificationsListener::Forwarder: public boost::signals2::trackable { LOG_CLASS(LLNotificationsListener::Forwarder); public: @@ -213,8 +213,10 @@ public: mRespond(false) { // Connect to the specified channel on construction. Because - // LLEventTrackable is a base, we should automatically disconnect when - // destroyed. + // boost::signals2::trackable is a base, because we use boost::bind() + // below, and because connectPassedFilter() directly calls + // boost::signals2::signal::connect(), we should automatically + // disconnect when destroyed. LLNotificationChannelPtr channelptr(llnotifications.getChannel(channel)); if (channelptr) { @@ -252,10 +254,10 @@ void LLNotificationsListener::forward(const LLSD& params) if (! forward) { // This is a request to stop forwarding notifications on the specified - // channel. The rest of the params don't matter. - // Because mForwarders contains scoped_ptrs, erasing the map entry - // DOES delete the heap Forwarder object. Because Forwarder derives - // from LLEventTrackable, destroying it disconnects it from the + // channel. The rest of the params don't matter. Because mForwarders + // contains scoped_ptrs, erasing the map entry DOES delete the heap + // Forwarder object. Because Forwarder derives from + // boost::signals2::trackable, destroying it disconnects it from the // channel. mForwarders.erase(channel); return; diff --git a/indra/llui/llscrolllistctrl.cpp b/indra/llui/llscrolllistctrl.cpp index 445377d3a2..ea2caaa1c0 100644 --- a/indra/llui/llscrolllistctrl.cpp +++ b/indra/llui/llscrolllistctrl.cpp @@ -1975,7 +1975,7 @@ bool LLScrollListCtrl::handleRightMouseDown(S32 x, S32 y, MASK mask) // set up the callbacks for all of the avatar/group menu items // (N.B. callbacks don't take const refs as id is local scope) bool is_group = (mContextMenuType == MENU_GROUP); - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + ScopedRegistrarHelper registrar; registrar.add("Url.ShowProfile", boost::bind(&LLScrollListCtrl::showProfile, id, is_group)); registrar.add("Url.SendIM", boost::bind(&LLScrollListCtrl::sendIM, id)); registrar.add("Url.AddFriend", boost::bind(&LLScrollListCtrl::addFriend, id)); diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp index e2d31085c4..32fafe3c52 100644 --- a/indra/llui/lltextbase.cpp +++ b/indra/llui/lltextbase.cpp @@ -2091,7 +2091,7 @@ void LLTextBase::createUrlContextMenu(S32 x, S32 y, const std::string &in_url) // set up the callbacks for all of the potential menu items, N.B. we // don't use const ref strings in callbacks in case url goes out of scope - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + CommitRegistrarHelper registrar(LLUICtrl::CommitCallbackRegistry::currentRegistrar()); registrar.add("Url.Open", boost::bind(&LLUrlAction::openURL, url)); registrar.add("Url.OpenInternal", boost::bind(&LLUrlAction::openURLInternal, url)); registrar.add("Url.OpenExternal", boost::bind(&LLUrlAction::openURLExternal, url)); diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp index 3537c764b9..c8cde90032 100644 --- a/indra/llui/lltexteditor.cpp +++ b/indra/llui/lltexteditor.cpp @@ -1131,7 +1131,7 @@ void LLTextEditor::removeChar() // Add a single character to the text S32 LLTextEditor::addChar(S32 pos, llwchar wc) { - if ((wstring_utf8_length(getWText()) + wchar_utf8_length(wc)) > mMaxTextByteLength) + if ( (wstring_utf8_length( getWText() ) + wchar_utf8_length( wc )) > mMaxTextByteLength) { LLUI::getInstance()->reportBadKeystroke(); return 0; @@ -1166,12 +1166,12 @@ S32 LLTextEditor::addChar(S32 pos, llwchar wc) void LLTextEditor::addChar(llwchar wc) { - if (!getEnabled()) + if( !getEnabled() ) { return; } - if (hasSelection()) + if( hasSelection() ) { deleteSelection(true); } @@ -1595,7 +1595,8 @@ void LLTextEditor::cleanStringForPaste(LLWString & clean_string) } -void LLTextEditor::pasteTextWithLinebreaks(LLWString & clean_string) +template <> +void LLTextEditor::pasteTextWithLinebreaks<LLWString>(const LLWString & clean_string) { std::basic_string<llwchar>::size_type start = 0; std::basic_string<llwchar>::size_type pos = clean_string.find('\n',start); diff --git a/indra/llui/lltexteditor.h b/indra/llui/lltexteditor.h index e9e7070414..765c88a933 100644 --- a/indra/llui/lltexteditor.h +++ b/indra/llui/lltexteditor.h @@ -34,6 +34,7 @@ #include "llstyle.h" #include "lleditmenuhandler.h" #include "llviewborder.h" // for params +#include "llstring.h" #include "lltextbase.h" #include "lltextvalidate.h" @@ -249,7 +250,9 @@ protected: // Undoable operations void addChar(llwchar c); // at mCursorPos S32 addChar(S32 pos, llwchar wc); +public: void addLineBreakChar(bool group_together = false); +protected: S32 overwriteChar(S32 pos, llwchar wc); void removeChar(); S32 removeChar(S32 pos); @@ -305,8 +308,17 @@ private: // void pasteHelper(bool is_primary); void cleanStringForPaste(LLWString & clean_string); - void pasteTextWithLinebreaks(LLWString & clean_string); +public: + template <typename STRINGTYPE> + void pasteTextWithLinebreaks(const STRINGTYPE& clean_string) + { + pasteTextWithLinebreaks<LLWString>(ll_convert(clean_string)); + } + template <> + void pasteTextWithLinebreaks<LLWString>(const LLWString & clean_string); + +private: void onKeyStroke(); // Concrete TextCmd sub-classes used by the LLTextEditor base class diff --git a/indra/llui/lltextvalidate.cpp b/indra/llui/lltextvalidate.cpp index 9a087d8230..84f8b9daf0 100644 --- a/indra/llui/lltextvalidate.cpp +++ b/indra/llui/lltextvalidate.cpp @@ -31,6 +31,7 @@ #include "lltextvalidate.h" #include "llnotificationsutil.h" +#include "lltimer.h" #include "lltrans.h" #include "llresmgr.h" // for LLLocale diff --git a/indra/llui/lltoolbar.cpp b/indra/llui/lltoolbar.cpp index 5955a28fa3..6e6e332632 100644 --- a/indra/llui/lltoolbar.cpp +++ b/indra/llui/lltoolbar.cpp @@ -144,7 +144,7 @@ void LLToolBar::createContextMenu() { // Setup bindings specific to this instance for the context menu options - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar commit_reg; + CommitRegistrarHelper commit_reg(LLUICtrl::CommitCallbackRegistry::currentRegistrar()); commit_reg.add("Toolbars.EnableSetting", boost::bind(&LLToolBar::onSettingEnable, this, _2)); commit_reg.add("Toolbars.RemoveSelectedCommand", boost::bind(&LLToolBar::onRemoveSelectedCommand, this)); diff --git a/indra/llui/llui.cpp b/indra/llui/llui.cpp index e36dae3955..b4299ae8e5 100644 --- a/indra/llui/llui.cpp +++ b/indra/llui/llui.cpp @@ -169,8 +169,7 @@ mHelpImpl(NULL) LLFontGL::sShadowColor = LLUIColorTable::instance().getColor("ColorDropShadow"); - LLUICtrl::CommitCallbackRegistry::Registrar& reg = LLUICtrl::CommitCallbackRegistry::defaultRegistrar(); - + LLUICtrl::CommitRegistrarHelper reg(LLUICtrl::CommitCallbackRegistry::defaultRegistrar()); // Callbacks for associating controls with floater visibility: reg.add("Floater.Toggle", [](LLUICtrl* ctrl, const LLSD& param) -> void { LLFloaterReg::toggleInstance(param.asStringRef()); }); reg.add("Floater.ToggleOrBringToFront", [](LLUICtrl* ctrl, const LLSD& param) -> void { LLFloaterReg::toggleInstanceOrBringToFront(param.asStringRef()); }); diff --git a/indra/llui/lluictrl.cpp b/indra/llui/lluictrl.cpp index cbabb5a933..e3e8130f51 100644 --- a/indra/llui/lluictrl.cpp +++ b/indra/llui/lluictrl.cpp @@ -220,10 +220,10 @@ void LLUICtrl::initFromParams(const Params& p) } else { - commit_callback_t* initfunc = (CommitCallbackRegistry::getValue(p.init_callback.function_name)); - if (initfunc) + LLUICtrl::CommitCallbackInfo *info = (CommitCallbackRegistry::getValue(p.init_callback.function_name)); + if (info && info->callback_func) { - (*initfunc)(this, p.init_callback.parameter); + (info->callback_func)(this, p.init_callback.parameter); } } } @@ -283,13 +283,13 @@ LLUICtrl::commit_signal_t::slot_type LLUICtrl::initCommitCallback(const CommitCa { std::string function_name = cb.function_name; setFunctionName(function_name); - commit_callback_t* func = (CommitCallbackRegistry::getValue(function_name)); - if (func) + LLUICtrl::CommitCallbackInfo *info = (CommitCallbackRegistry::getValue(function_name)); + if (info && info->callback_func) { if (cb.parameter.isProvided()) - return boost::bind((*func), _1, cb.parameter); + return boost::bind((info->callback_func), _1, cb.parameter); else - return commit_signal_t::slot_type(*func); + return commit_signal_t::slot_type(info->callback_func); } else if (!function_name.empty()) { diff --git a/indra/llui/lluictrl.h b/indra/llui/lluictrl.h index 8cd9950917..2c73ff6b57 100644 --- a/indra/llui/lluictrl.h +++ b/indra/llui/lluictrl.h @@ -274,11 +274,56 @@ public: template <typename F, typename DERIVED> class CallbackRegistry : public LLRegistrySingleton<std::string, F, DERIVED > {}; + struct CommitCallbackInfo + { + enum EUntrustedCall + { + UNTRUSTED_ALLOW, + UNTRUSTED_BLOCK, + UNTRUSTED_THROTTLE + }; + + CommitCallbackInfo(commit_callback_t func = {}, EUntrustedCall handle_untrusted = UNTRUSTED_ALLOW) : + callback_func(func), + handle_untrusted(handle_untrusted) + { + } - class CommitCallbackRegistry : public CallbackRegistry<commit_callback_t, CommitCallbackRegistry> + public: + commit_callback_t callback_func; + EUntrustedCall handle_untrusted; + }; + typedef LLUICtrl::CommitCallbackInfo cb_info; + class CommitCallbackRegistry : public CallbackRegistry<CommitCallbackInfo, CommitCallbackRegistry> { LLSINGLETON_EMPTY_CTOR(CommitCallbackRegistry); }; + + class CommitRegistrarHelper + { + public: + CommitRegistrarHelper(LLUICtrl::CommitCallbackRegistry::Registrar ®istrar) : mRegistrar(registrar) {} + + template <typename... ARGS> void add(const std::string &name, ARGS &&...args) + { + mRegistrar.add(name, {std::forward<ARGS>(args)...}); + } + private: + LLUICtrl::CommitCallbackRegistry::Registrar &mRegistrar; + }; + + class ScopedRegistrarHelper + { + public: + template <typename... ARGS> void add(const std::string &name, ARGS &&...args) + { + mRegistrar.add(name, {std::forward<ARGS>(args)...}); + } + + private: + LLUICtrl::CommitCallbackRegistry::ScopedRegistrar mRegistrar; + }; + // the enable callback registry is also used for visiblity callbacks class EnableCallbackRegistry : public CallbackRegistry<enable_callback_t, EnableCallbackRegistry> { diff --git a/indra/llwindow/llwindow.h b/indra/llwindow/llwindow.h index 5e06e665f3..fcc4fd863a 100644 --- a/indra/llwindow/llwindow.h +++ b/indra/llwindow/llwindow.h @@ -184,6 +184,8 @@ public: virtual void interruptLanguageTextInput() {} virtual void spawnWebBrowser(const std::string& escaped_url, bool async) {}; + virtual void openFolder(const std::string &path) {}; + static std::vector<std::string> getDynamicFallbackFontList(); // Provide native key event data diff --git a/indra/llwindow/llwindowmacosx-objc.h b/indra/llwindow/llwindowmacosx-objc.h index d9d8bfce1f..620bbc8876 100644 --- a/indra/llwindow/llwindowmacosx-objc.h +++ b/indra/llwindow/llwindowmacosx-objc.h @@ -178,6 +178,8 @@ void setMarkedText(unsigned short *text, unsigned int *selectedRange, unsigned i void getPreeditLocation(float *location, unsigned int length); void allowDirectMarkedTextInput(bool allow, GLViewRef glView); +void openFolderWithFinder(const char *folder_path); + NSWindowRef getMainAppWindow(); GLViewRef getGLView(); diff --git a/indra/llwindow/llwindowmacosx-objc.mm b/indra/llwindow/llwindowmacosx-objc.mm index 2e75d309ea..01feac7885 100644 --- a/indra/llwindow/llwindowmacosx-objc.mm +++ b/indra/llwindow/llwindowmacosx-objc.mm @@ -463,6 +463,13 @@ long showAlert(std::string text, std::string title, int type) return ret; } +void openFolderWithFinder(const char *folder_path) +{ + @autoreleasepool { + NSString *folderPathString = [NSString stringWithUTF8String:folder_path]; + [[NSWorkspace sharedWorkspace] openFile:folderPathString withApplication:@"Finder"]; + } +} /* GLViewRef getGLView() { diff --git a/indra/llwindow/llwindowmacosx.cpp b/indra/llwindow/llwindowmacosx.cpp index 80001b14ee..e95ad4d970 100644 --- a/indra/llwindow/llwindowmacosx.cpp +++ b/indra/llwindow/llwindowmacosx.cpp @@ -221,14 +221,14 @@ bool callKeyDown(NSKeyEventRef event, unsigned short key, unsigned int mask, wch { //if (mask!=MASK_NONE) { - if((key == gKeyboard->inverseTranslateKey('Z')) && (character == 'y')) - { - key = gKeyboard->inverseTranslateKey('Y'); - } - else if ((key == gKeyboard->inverseTranslateKey('Y')) && (character == 'z')) - { - key = gKeyboard->inverseTranslateKey('Z'); - } + if((key == gKeyboard->inverseTranslateKey('Z')) && (character == 'y')) + { + key = gKeyboard->inverseTranslateKey('Y'); + } + else if ((key == gKeyboard->inverseTranslateKey('Y')) && (character == 'z')) + { + key = gKeyboard->inverseTranslateKey('Z'); + } } mRawKeyEvent = event; @@ -2556,6 +2556,11 @@ F32 LLWindowMacOSX::getSystemUISize() return gHiDPISupport ? ::getDeviceUnitSize(mGLView) : LLWindow::getSystemUISize(); } +void LLWindowMacOSX::openFolder(const std::string &path) +{ + openFolderWithFinder(path.c_str()); +} + #if LL_OS_DRAGDROP_ENABLED /* S16 LLWindowMacOSX::dragTrackingHandler(DragTrackingMessage message, WindowRef theWindow, diff --git a/indra/llwindow/llwindowmacosx.h b/indra/llwindow/llwindowmacosx.h index f5b6441746..211ae872c6 100644 --- a/indra/llwindow/llwindowmacosx.h +++ b/indra/llwindow/llwindowmacosx.h @@ -113,6 +113,8 @@ public: void spawnWebBrowser(const std::string& escaped_url, bool async) override; F32 getSystemUISize() override; + void openFolder(const std::string &path) override; + bool getInputDevices(U32 device_type_filter, std::function<bool(std::string&, LLSD&, void*)> osx_callback, void* win_callback, diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 0b6ee541c0..01edbb53af 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -3742,6 +3742,23 @@ S32 OSMessageBoxWin32(const std::string& text, const std::string& caption, U32 t return retval; } +void shell_open(const std::string &file, bool async) +{ + std::wstring url_utf16 = ll_convert(file); + + // let the OS decide what to use to open the URL + SHELLEXECUTEINFO sei = {sizeof(sei)}; + // NOTE: this assumes that SL will stick around long enough to complete the DDE message exchange + // necessary for ShellExecuteEx to complete + if (async) + { + sei.fMask = SEE_MASK_ASYNCOK; + } + sei.nShow = SW_SHOWNORMAL; + sei.lpVerb = L"open"; + sei.lpFile = url_utf16.c_str(); + ShellExecuteEx(&sei); +} void LLWindowWin32::spawnWebBrowser(const std::string& escaped_url, bool async) { @@ -3767,22 +3784,12 @@ void LLWindowWin32::spawnWebBrowser(const std::string& escaped_url, bool async) // replaced ShellExecute code with ShellExecuteEx since ShellExecute doesn't work // reliablly on Vista. - // this is madness.. no, this is.. - LLWString url_wstring = utf8str_to_wstring( escaped_url ); - llutf16string url_utf16 = wstring_to_utf16str( url_wstring ); + shell_open(escaped_url, async); +} - // let the OS decide what to use to open the URL - SHELLEXECUTEINFO sei = { sizeof( sei ) }; - // NOTE: this assumes that SL will stick around long enough to complete the DDE message exchange - // necessary for ShellExecuteEx to complete - if (async) - { - sei.fMask = SEE_MASK_ASYNCOK; - } - sei.nShow = SW_SHOWNORMAL; - sei.lpVerb = L"open"; - sei.lpFile = url_utf16.c_str(); - ShellExecuteEx( &sei ); +void LLWindowWin32::openFolder(const std::string &path) +{ + shell_open(path, false); } /* diff --git a/indra/llwindow/llwindowwin32.h b/indra/llwindow/llwindowwin32.h index 287402faa0..381f8882ca 100644 --- a/indra/llwindow/llwindowwin32.h +++ b/indra/llwindow/llwindowwin32.h @@ -120,6 +120,8 @@ public: /*virtual*/ void interruptLanguageTextInput(); /*virtual*/ void spawnWebBrowser(const std::string& escaped_url, bool async); + void openFolder(const std::string &path) override; + /*virtual*/ F32 getSystemUISize(); LLWindowCallbacks::DragNDropResult completeDragNDropRequest( const LLCoordGL gl_coord, const MASK mask, LLWindowCallbacks::DragNDropAction action, const std::string url ); diff --git a/indra/llxml/llcontrol.cpp b/indra/llxml/llcontrol.cpp index bb590ebd76..6d02cbaa34 100644 --- a/indra/llxml/llcontrol.cpp +++ b/indra/llxml/llcontrol.cpp @@ -728,7 +728,7 @@ void LLControlGroup::setLLSD(std::string_view name, const LLSD& val) set(name, val); } -void LLControlGroup::setUntypedValue(std::string_view name, const LLSD& val) +void LLControlGroup::setUntypedValue(std::string_view name, const LLSD& val, bool saved_value) { if (name.empty()) { @@ -739,7 +739,7 @@ void LLControlGroup::setUntypedValue(std::string_view name, const LLSD& val) if (control) { - control->setValue(val); + control->setValue(val, saved_value); } else { diff --git a/indra/llxml/llcontrol.h b/indra/llxml/llcontrol.h index 4f54a9d705..a614ff216b 100644 --- a/indra/llxml/llcontrol.h +++ b/indra/llxml/llcontrol.h @@ -119,7 +119,7 @@ public: LLSD getDefault() const { return mValues.front(); } LLSD getSaveValue() const; - void set(const LLSD& val) { setValue(val); } + void set(const LLSD& val, bool saved_value = true) { setValue(val, saved_value); } void setValue(const LLSD& value, bool saved_value = true); void setDefaultValue(const LLSD& value); void setPersist(ePersist); @@ -254,7 +254,7 @@ public: void setLLSD(std::string_view name, const LLSD& val); // type agnostic setter that takes LLSD - void setUntypedValue(std::string_view name, const LLSD& val); + void setUntypedValue(std::string_view name, const LLSD& val, bool saved_value = true); // generic setter template<typename T> void set(std::string_view name, const T& val) diff --git a/indra/llxml/tests/llcontrol_test.cpp b/indra/llxml/tests/llcontrol_test.cpp index 4192e029c5..52c202bceb 100644 --- a/indra/llxml/tests/llcontrol_test.cpp +++ b/indra/llxml/tests/llcontrol_test.cpp @@ -97,7 +97,7 @@ namespace tut template<> template<> void control_group_t::test<1>() { - int results = mCG->loadFromFile(mTestConfigFile.c_str()); + int results = mCG->loadFromFile(mTestConfigFile); ensure("number of settings", (results == 1)); ensure("value of setting", (mCG->getU32("TestSetting") == 12)); } @@ -106,14 +106,14 @@ namespace tut template<> template<> void control_group_t::test<2>() { - int results = mCG->loadFromFile(mTestConfigFile.c_str()); + int results = mCG->loadFromFile(mTestConfigFile); mCG->setU32("TestSetting", 13); ensure_equals("value of changed setting", mCG->getU32("TestSetting"), 13); LLControlGroup test_cg("foo2"); std::string temp_test_file = (mTestConfigDir + "setting_llsd_temp.xml"); mCleanups.push_back(temp_test_file); mCG->saveToFile(temp_test_file.c_str(), true); - results = test_cg.loadFromFile(temp_test_file.c_str()); + results = test_cg.loadFromFile(temp_test_file); ensure("number of changed settings loaded", (results == 1)); ensure("value of changed settings loaded", (test_cg.getU32("TestSetting") == 13)); } @@ -126,7 +126,7 @@ namespace tut // a default settings file that declares variables, rather than a user // settings file. When loadFromFile() encounters an unrecognized user // settings variable, it forcibly preserves it (CHOP-962). - int results = mCG->loadFromFile(mTestConfigFile.c_str(), true); + int results = mCG->loadFromFile(mTestConfigFile, true); LLControlVariable* control = mCG->getControl("TestSetting"); LLSD new_value = 13; control->setValue(new_value, false); @@ -135,7 +135,7 @@ namespace tut std::string temp_test_file = (mTestConfigDir + "setting_llsd_persist_temp.xml"); mCleanups.push_back(temp_test_file); mCG->saveToFile(temp_test_file.c_str(), true); - results = test_cg.loadFromFile(temp_test_file.c_str()); + results = test_cg.loadFromFile(temp_test_file); //If we haven't changed any settings, then we shouldn't have any settings to load ensure("number of non-persisted changed settings loaded", (results == 0)); } @@ -144,7 +144,7 @@ namespace tut template<> template<> void control_group_t::test<4>() { - int results = mCG->loadFromFile(mTestConfigFile.c_str()); + int results = mCG->loadFromFile(mTestConfigFile); ensure("number of settings", (results == 1)); mCG->getControl("TestSetting")->getSignal()->connect(boost::bind(&this->handleListenerTest)); mCG->setU32("TestSetting", 13); diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 859ccbd4cd..106fe81682 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -46,6 +46,7 @@ include(VisualLeakDetector) include(VulkanGltf) include(ZLIBNG) include(LLPrimitive) +include(Lualibs) if (NOT HAVOK_TPV) # When using HAVOK_TPV, the library is precompiled, so no need for this @@ -92,6 +93,7 @@ set(viewer_SOURCE_FILES llagentwearables.cpp llanimstatelabels.cpp llappcorehttp.cpp + llappearancelistener.cpp llappearancemgr.cpp llappviewer.cpp llappviewerlistener.cpp @@ -245,6 +247,8 @@ set(viewer_SOURCE_FILES llfloaterlandholdings.cpp llfloaterlinkreplace.cpp llfloaterloadprefpreset.cpp + llfloaterluadebug.cpp + llfloaterluascripts.cpp llfloatermarketplacelistings.cpp llfloatermap.cpp llfloatermediasettings.cpp @@ -359,6 +363,7 @@ set(viewer_SOURCE_FILES llinventorygallerymenu.cpp llinventoryicon.cpp llinventoryitemslist.cpp + llinventorylistener.cpp llinventorylistitem.cpp llinventorymodel.cpp llinventorymodelbackgroundfetch.cpp @@ -378,6 +383,7 @@ set(viewer_SOURCE_FILES lllogchat.cpp llloginhandler.cpp lllogininstance.cpp + llluamanager.cpp llmachineid.cpp llmanip.cpp llmaniprotate.cpp @@ -759,6 +765,7 @@ set(viewer_HEADER_FILES llanimstatelabels.h llappcorehttp.h llappearance.h + llappearancelistener.h llappearancemgr.h llappviewer.h llappviewerlistener.h @@ -915,6 +922,8 @@ set(viewer_HEADER_FILES llfloaterlandholdings.h llfloaterlinkreplace.h llfloaterloadprefpreset.h + llfloaterluadebug.h + llfloaterluascripts.h llfloatermap.h llfloatermarketplacelistings.h llfloatermediasettings.h @@ -1027,6 +1036,7 @@ set(viewer_HEADER_FILES llinventorygallerymenu.h llinventoryicon.h llinventoryitemslist.h + llinventorylistener.h llinventorylistitem.h llinventorymodel.h llinventorymodelbackgroundfetch.h @@ -1046,6 +1056,7 @@ set(viewer_HEADER_FILES lllogchat.h llloginhandler.h lllogininstance.h + llluamanager.h llmachineid.h llmanip.h llmaniprotate.h @@ -1816,20 +1827,6 @@ if (WINDOWS) if (PACKAGE) add_custom_command( - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/event_host.tar.xz - COMMAND ${PYTHON_EXECUTABLE} - ARGS - ${CMAKE_CURRENT_SOURCE_DIR}/event_host_manifest.py - ${CMAKE_CURRENT_SOURCE_DIR}/.. - ${CMAKE_CURRENT_BINARY_DIR} - ${CMAKE_CFG_INTDIR} - DEPENDS - lleventhost - ${EVENT_HOST_SCRIPTS} - ${CMAKE_CURRENT_SOURCE_DIR}/event_host_manifest.py - ) - - add_custom_command( OUTPUT ${CMAKE_CFG_INTDIR}/touched.bat COMMAND ${PYTHON_EXECUTABLE} ARGS @@ -1858,9 +1855,6 @@ if (WINDOWS) add_custom_target(llpackage ALL DEPENDS ${CMAKE_CFG_INTDIR}/touched.bat ) - # temporarily disable packaging of event_host until hg subrepos get - # sorted out on the parabuild cluster... - #${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/event_host.tar.xz) endif (PACKAGE) elseif (DARWIN) @@ -1922,12 +1916,13 @@ target_link_libraries(${VIEWER_BINARY_NAME} llcommon llmeshoptimizer llwebrtc - ll::ndof lllogin llprimitive llappearance ${LLPHYSICSEXTENSIONS_LIBRARIES} ll::bugsplat + ll::lualibs + ll::ndof ll::tracy ll::openxr ) @@ -2304,6 +2299,11 @@ if (LL_TESTS) "${test_libs}" ) + LL_ADD_INTEGRATION_TEST(llluamanager + "llluamanager.cpp" + "${test_libs};ll::lualibs" + ) + LL_ADD_INTEGRATION_TEST(llsechandler_basic llsechandler_basic.cpp "${test_libs}" @@ -2358,4 +2358,3 @@ if (LL_TESTS) endif (LL_TESTS) check_message_template(${VIEWER_BINARY_NAME}) - diff --git a/indra/newview/app_settings/cmd_line.xml b/indra/newview/app_settings/cmd_line.xml index e16a5c7e76..e610775332 100644 --- a/indra/newview/app_settings/cmd_line.xml +++ b/indra/newview/app_settings/cmd_line.xml @@ -195,7 +195,33 @@ <string>LogPerformance</string> </map> - <key>multiple</key> + <key>lua</key> + <map> + <key>desc</key> + <string>Run specified Lua chunk</string> + <key>count</key> + <integer>1</integer> + <!-- you can specify multiple such chunks --> + <key>compose</key> + <boolean>true</boolean> + <key>map-to</key> + <string>LuaChunk</string> + </map> + + <key>luafile</key> + <map> + <key>desc</key> + <string>Run specified Lua script</string> + <key>count</key> + <integer>1</integer> + <!-- you can specify multiple such scripts --> + <key>compose</key> + <boolean>true</boolean> + <key>map-to</key> + <string>LuaScript</string> + </map> + + <key>multiple</key> <map> <key>desc</key> <string>Allow multiple viewers.</string> diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 9380b0d6f7..c6946d1ec1 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -68,7 +68,7 @@ <key>Value</key> <integer>1</integer> </map> - <key>CrashHostUrl</key> + <key>CrashHostUrl</key> <map> <key>Comment</key> <string>A URL pointing to a crash report handler; overrides cluster negotiation to locate crash handler.</string> @@ -346,7 +346,7 @@ <key>Value</key> <real>0.5</real> </map> - <key>AudioStreamingMedia</key> + <key>AudioStreamingMedia</key> <map> <key>Comment</key> <string>Enable streaming</string> @@ -1314,7 +1314,7 @@ <key>Type</key> <string>Boolean</string> <key>Value</key> - <integer>0</integer> + <integer>0</integer> </map> <key>CameraPositionSmoothing</key> <map> @@ -1952,17 +1952,17 @@ <key>Value</key> <string /> </map> - <key>DebugAvatarRezTime</key> - <map> - <key>Comment</key> - <string>Display times for avatars to resolve.</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>Boolean</string> - <key>Value</key> - <integer>0</integer> - </map> + <key>DebugAvatarRezTime</key> + <map> + <key>Comment</key> + <string>Display times for avatars to resolve.</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> <key>DebugAvatarLocalTexLoadedTime</key> <map> <key>Comment</key> @@ -2337,39 +2337,39 @@ <key>Value</key> <integer>0</integer> </map> - <key>DefaultFemaleAvatar</key> - <map> - <key>Comment</key> - <string>Default Female Avatar</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>String</string> - <key>Value</key> - <string>Female Shape & Outfit</string> - </map> - <key>DefaultLoginLocation</key> - <map> - <key>Comment</key> - <string>Startup destination default (if not specified on command line)</string> - <key>Persist</key> - <integer>0</integer> - <key>Type</key> - <string>String</string> - <key>Value</key> - <string/> - </map> - <key>DefaultMaleAvatar</key> - <map> - <key>Comment</key> - <string>Default Male Avatar</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>String</string> - <key>Value</key> - <string>Male Shape & Outfit</string> - </map> + <key>DefaultFemaleAvatar</key> + <map> + <key>Comment</key> + <string>Default Female Avatar</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>Female Shape & Outfit</string> + </map> + <key>DefaultLoginLocation</key> + <map> + <key>Comment</key> + <string>Startup destination default (if not specified on command line)</string> + <key>Persist</key> + <integer>0</integer> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string/> + </map> + <key>DefaultMaleAvatar</key> + <map> + <key>Comment</key> + <string>Default Male Avatar</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>Male Shape & Outfit</string> + </map> <key>DestinationGuideURL</key> <map> <key>Comment</key> @@ -2744,7 +2744,7 @@ <key>Value</key> <integer>0</integer> </map> - <key>FirstSelectedEnabledPopups</key> + <key>FirstSelectedEnabledPopups</key> <map> <key>Comment</key> <string>Return false if there is not enable popup selected in the list of floater preferences popups</string> @@ -3566,39 +3566,39 @@ <key>Value</key> <integer>0</integer> </map> - <key>InventoryLinking</key> - <map> - <key>Comment</key> - <string>Enable ability to create links to folders and items via "Paste as link".</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>Boolean</string> - <key>Value</key> - <integer>1</integer> - </map> - <key>InventoryOutboxLogging</key> - <map> - <key>Comment</key> - <string>Enable debug output associated with the Merchant Outbox.</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>Boolean</string> - <key>Value</key> - <integer>0</integer> - </map> - <key>InventoryOutboxMakeVisible</key> - <map> - <key>Comment</key> - <string>Enable making the Merchant Outbox and Inbox visible in the inventory for debug purposes.</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>Boolean</string> - <key>Value</key> - <integer>0</integer> - </map> + <key>InventoryLinking</key> + <map> + <key>Comment</key> + <string>Enable ability to create links to folders and items via "Paste as link".</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>1</integer> + </map> + <key>InventoryOutboxLogging</key> + <map> + <key>Comment</key> + <string>Enable debug output associated with the Merchant Outbox.</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> + <key>InventoryOutboxMakeVisible</key> + <map> + <key>Comment</key> + <string>Enable making the Merchant Outbox and Inbox visible in the inventory for debug purposes.</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> <key>InventoryOutboxMaxFolderCount</key> <map> <key>Comment</key> @@ -3852,8 +3852,8 @@ <key>Value</key> <real>0.25</real> </map> - <key>Jpeg2000AdvancedCompression</key> - <map> + <key>Jpeg2000AdvancedCompression</key> + <map> <key>Comment</key> <string>Use advanced Jpeg2000 compression options (precincts, blocks, ordering, markers)</string> <key>Persist</key> @@ -3862,9 +3862,9 @@ <string>Boolean</string> <key>Value</key> <integer>0</integer> - </map> - <key>Jpeg2000PrecinctsSize</key> - <map> + </map> + <key>Jpeg2000PrecinctsSize</key> + <map> <key>Comment</key> <string>Size of image precincts. Assumed square and same for all levels. Must be power of 2.</string> <key>Persist</key> @@ -3873,9 +3873,9 @@ <string>S32</string> <key>Value</key> <integer>256</integer> - </map> - <key>Jpeg2000BlocksSize</key> - <map> + </map> + <key>Jpeg2000BlocksSize</key> + <map> <key>Comment</key> <string>Size of encoding blocks. Assumed square and same for all levels. Must be power of 2. Max 64, Min 4.</string> <key>Persist</key> @@ -3884,7 +3884,7 @@ <string>S32</string> <key>Value</key> <integer>64</integer> - </map> + </map> <key>KeepAspectForSnapshot</key> <map> <key>Comment</key> @@ -3962,6 +3962,78 @@ <key>Value</key> <string>Monospace</string> </map> + <key>LuaAutorunPath</key> + <map> + <key>Comment</key> + <string>Directories containing scripts to autorun at viewer startup</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>LLSD</string> + <key>Value</key> + <array> + <string>scripts/lua/auto</string> + </array> + </map> + <key>LuaChunk</key> + <map> + <key>Comment</key> + <string>Zero or more Lua chunks to run from command line</string> + <key>Persist</key> + <integer>0</integer> + <key>Type</key> + <string>LLSD</string> + <key>Value</key> + <array /> + </map> + <key>LuaCommandPath</key> + <map> + <key>Comment</key> + <string>Directories containing scripts recognized as chat slash commands</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>LLSD</string> + <key>Value</key> + <array> + <string>scripts/lua</string> + </array> + </map> + <key>LuaFeature</key> + <map> + <key>Comment</key> + <string>Enable viewer's Lua script engine.</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> + <key>LuaRequirePath</key> + <map> + <key>Comment</key> + <string>Directories containing Lua modules loadable by require()</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>LLSD</string> + <key>Value</key> + <array> + <string>scripts/lua/require</string> + </array> + </map> + <key>LuaScript</key> + <map> + <key>Comment</key> + <string>Zero or more Lua script files to run from command line</string> + <key>Persist</key> + <integer>0</integer> + <key>Type</key> + <string>LLSD</string> + <key>Value</key> + <array /> + </map> <key>GridStatusRSS</key> <map> <key>Comment</key> @@ -4492,17 +4564,17 @@ <key>Value</key> <string /> </map> - <key>MarketplaceListingsLogging</key> - <map> - <key>Comment</key> - <string>Enable debug output associated with the Marketplace Listings (SLM) API.</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>Boolean</string> - <key>Value</key> - <integer>0</integer> - </map> + <key>MarketplaceListingsLogging</key> + <map> + <key>Comment</key> + <string>Enable debug output associated with the Marketplace Listings (SLM) API.</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> <key>MarketplaceURL</key> <map> <key>Comment</key> @@ -5388,28 +5460,28 @@ <key>Value</key> <integer>1000</integer> </map> - <key>FakeInitialOutfitName</key> - <map> - <key>Comment</key> - <string>Pretend that this is first time login and specified name was chosen</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> + <key>FakeInitialOutfitName</key> + <map> + <key>Comment</key> + <string>Pretend that this is first time login and specified name was chosen</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> <string>String</string> <key>Value</key> <string /> - </map> - <key>MyOutfitsAutofill</key> - <map> - <key>Comment</key> - <string>Always autofill My Outfits from library when empty (else happens just once).</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>Boolean</string> - <key>Value</key> - <integer>0</integer> - </map> + </map> + <key>MyOutfitsAutofill</key> + <map> + <key>Comment</key> + <string>Always autofill My Outfits from library when empty (else happens just once).</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> <key>NearMeRange</key> <map> <key>Comment</key> @@ -5518,7 +5590,7 @@ <key>Type</key> <string>Boolean</string> <key>Value</key> - <integer>0</integer> + <integer>0</integer> </map> <key>NonvisibleObjectsInMemoryTime</key> <map> @@ -5529,7 +5601,7 @@ <key>Type</key> <string>U32</string> <key>Value</key> - <integer>64</integer> + <integer>64</integer> </map> <key>NoPreload</key> <map> @@ -6441,7 +6513,7 @@ <key>Value</key> <real>6.0</real> </map> - <key>PreferredMaturity</key> + <key>PreferredMaturity</key> <map> <key>Comment</key> <string>Setting for the user's preferred maturity level (consts in indra_constants.h)</string> @@ -6450,7 +6522,7 @@ <key>Type</key> <string>U32</string> <key>Value</key> - <integer>13</integer> + <integer>13</integer> </map> <key>PreviewAmbientColor</key> <map> @@ -6620,8 +6692,8 @@ </map> <key>PrimMediaMasterEnabled</key> - <map> - <key>Comment</key> + <map> + <key>Comment</key> <string>Whether or not Media on a Prim is enabled.</string> <key>Persist</key> <integer>1</integer> @@ -6630,9 +6702,9 @@ <key>Value</key> <integer>1</integer> </map> - <key>PrimMediaControlsUseHoverControlSet</key> - <map> - <key>Comment</key> + <key>PrimMediaControlsUseHoverControlSet</key> + <map> + <key>Comment</key> <string>Whether or not hovering over prim media uses minimal "hover" controls or the authored control set.</string> <key>Persist</key> <integer>1</integer> @@ -6641,17 +6713,17 @@ <key>Value</key> <integer>0</integer> </map> - <key>PrimMediaDragNDrop</key> - <map> - <key>Comment</key> - <string>Enable drag and drop of URLs onto prim faces</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>Boolean</string> - <key>Value</key> - <integer>1</integer> - </map> + <key>PrimMediaDragNDrop</key> + <map> + <key>Comment</key> + <string>Enable drag and drop of URLs onto prim faces</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>1</integer> + </map> <key>PrimMediaMaxRetries</key> <map> <key>Comment</key> @@ -6685,7 +6757,7 @@ <key>Value</key> <real>5.0</real> </map> - <key>PrimMediaMaxSortedQueueSize</key> + <key>PrimMediaMaxSortedQueueSize</key> <map> <key>Comment</key> <string>Maximum number of objects the viewer will load media for initially</string> @@ -6696,7 +6768,7 @@ <key>Value</key> <integer>100000</integer> </map> - <key>PrimMediaMaxRoundRobinQueueSize</key> + <key>PrimMediaMaxRoundRobinQueueSize</key> <map> <key>Comment</key> <string>Maximum number of objects the viewer will continuously update media for</string> @@ -8855,17 +8927,17 @@ <key>Value</key> <integer>1024</integer> </map> - <key>RenderHeroProbeDistance</key> - <map> - <key>Comment</key> - <string>Distance in meters for hero probes to render out to.</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>F32</string> - <key>Value</key> - <real>8</real> - </map> + <key>RenderHeroProbeDistance</key> + <map> + <key>Comment</key> + <string>Distance in meters for hero probes to render out to.</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>8</real> + </map> <key>RenderHeroProbeUpdateRate</key> <map> <key>Comment</key> @@ -9430,17 +9502,17 @@ <key>Value</key> <integer>1</integer> </map> - <key>RenderTransparentWater</key> - <map> - <key>Comment</key> - <string>Render water as transparent. Setting to false renders water as opaque with a simple texture applied.</string> + <key>RenderTransparentWater</key> + <map> + <key>Comment</key> + <string>Render water as transparent. Setting to false renders water as opaque with a simple texture applied.</string> <key>Persist</key> <integer>1</integer> <key>Type</key> <string>Boolean</string> <key>Value</key> <integer>1</integer> - </map> + </map> <key>RenderTreeLODFactor</key> <map> <key>Comment</key> @@ -9733,18 +9805,18 @@ <key>Value</key> <integer>1</integer> </map> - <key>RenderPreferStreamDraw</key> - <map> - <key>Comment</key> - <string>Use GL_STREAM_DRAW in place of GL_DYNAMIC_DRAW</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>Boolean</string> - <key>Value</key> - <integer>0</integer> - </map> - <key>RenderVolumeLODFactor</key> + <key>RenderPreferStreamDraw</key> + <map> + <key>Comment</key> + <string>Use GL_STREAM_DRAW in place of GL_DYNAMIC_DRAW</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> + <key>RenderVolumeLODFactor</key> <map> <key>Comment</key> <string>Controls level of detail of primitives (multiplier for current screen area when calculated level of detail)</string> @@ -9876,18 +9948,18 @@ <key>Value</key> <integer>0</integer> </map> - <key>ReportBugURL</key> - <map> - <key>Comment</key> - <string>URL used for filing bugs from viewer</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>String</string> - <key>Value</key> - <string>https://feedback.secondlife.com/</string> - </map> - <key>RevokePermsOnStopAnimation</key> + <key>ReportBugURL</key> + <map> + <key>Comment</key> + <string>URL used for filing bugs from viewer</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>https://feedback.secondlife.com/</string> + </map> + <key>RevokePermsOnStopAnimation</key> <map> <key>Comment</key> <string>Clear animation permssions when choosing "Stop Animating Me"</string> @@ -10140,39 +10212,39 @@ <key>Value</key> <real>400.0</real> </map> - <key>SceneLoadingMonitorEnabled</key> - <map> - <key>Comment</key> - <string>Enabled scene loading monitor if set</string> - <key>Persist</key> - <integer>0</integer> - <key>Type</key> - <string>Boolean</string> - <key>Value</key> - <integer>0</integer> - </map> - <key>SceneLoadingMonitorSampleTime</key> - <map> - <key>Comment</key> - <string>Time between screen samples when monitor scene load (seconds)</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>F32</string> - <key>Value</key> - <real>0.25</real> - </map> - <key>SceneLoadingMonitorPixelDiffThreshold</key> - <map> - <key>Comment</key> - <string>Amount of pixels changed required to consider the scene as still loading (square root of fraction of pixels on screen)</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>F32</string> - <key>Value</key> - <real>0.02</real> - </map> + <key>SceneLoadingMonitorEnabled</key> + <map> + <key>Comment</key> + <string>Enabled scene loading monitor if set</string> + <key>Persist</key> + <integer>0</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> + <key>SceneLoadingMonitorSampleTime</key> + <map> + <key>Comment</key> + <string>Time between screen samples when monitor scene load (seconds)</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.25</real> + </map> + <key>SceneLoadingMonitorPixelDiffThreshold</key> + <map> + <key>Comment</key> + <string>Amount of pixels changed required to consider the scene as still loading (square root of fraction of pixels on screen)</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.02</real> + </map> <key>ScriptHelpFollowsCursor</key> <map> <key>Comment</key> @@ -10349,7 +10421,7 @@ <key>Value</key> <integer>0</integer> </map> - <key>AvatarNameTagMode</key> + <key>AvatarNameTagMode</key> <map> <key>Comment</key> <string>Select Avatar Name Tag Mode</string> @@ -10558,7 +10630,7 @@ <key>Value</key> <integer>1</integer> </map> - <key>ShowScriptErrors</key> + <key>ShowScriptErrors</key> <map> <key>Comment</key> <string>Show script errors</string> @@ -10569,7 +10641,7 @@ <key>Value</key> <integer>1</integer> </map> - <key>ShowScriptErrorsLocation</key> + <key>ShowScriptErrorsLocation</key> <map> <key>Comment</key> <string>Show script error in chat (0) or window (1).</string> @@ -10828,8 +10900,8 @@ <string>Display results of find events that are flagged as moderate</string> <key>Persist</key> <integer>1</integer> - <key>HideFromEditor</key> - <integer>1</integer> + <key>HideFromEditor</key> + <integer>1</integer> <key>Type</key> <string>Boolean</string> <key>Value</key> @@ -10841,8 +10913,8 @@ <string>Display results of find events that are flagged as adult</string> <key>Persist</key> <integer>1</integer> - <key>HideFromEditor</key> - <integer>1</integer> + <key>HideFromEditor</key> + <integer>1</integer> <key>Type</key> <string>Boolean</string> <key>Value</key> @@ -10854,8 +10926,8 @@ <string>Display results of find land sales that are flagged as general</string> <key>Persist</key> <integer>1</integer> - <key>HideFromEditor</key> - <integer>1</integer> + <key>HideFromEditor</key> + <integer>1</integer> <key>Type</key> <string>Boolean</string> <key>Value</key> @@ -10867,8 +10939,8 @@ <string>Display results of find land sales that are flagged as moderate</string> <key>Persist</key> <integer>1</integer> - <key>HideFromEditor</key> - <integer>1</integer> + <key>HideFromEditor</key> + <integer>1</integer> <key>Type</key> <string>Boolean</string> <key>Value</key> @@ -10880,8 +10952,8 @@ <string>Display results of find land sales that are flagged as adult</string> <key>Persist</key> <integer>1</integer> - <key>HideFromEditor</key> - <integer>1</integer> + <key>HideFromEditor</key> + <integer>1</integer> <key>Type</key> <string>Boolean</string> <key>Value</key> @@ -10893,8 +10965,8 @@ <string>Display results of find places or find popular that are in general sims</string> <key>Persist</key> <integer>1</integer> - <key>HideFromEditor</key> - <integer>1</integer> + <key>HideFromEditor</key> + <integer>1</integer> <key>Type</key> <string>Boolean</string> <key>Value</key> @@ -10906,8 +10978,8 @@ <string>Display results of find places or find popular that are in moderate sims</string> <key>Persist</key> <integer>1</integer> - <key>HideFromEditor</key> - <integer>1</integer> + <key>HideFromEditor</key> + <integer>1</integer> <key>Type</key> <string>Boolean</string> <key>Value</key> @@ -10919,8 +10991,8 @@ <string>Display results of find places or find popular that are in adult sims</string> <key>Persist</key> <integer>1</integer> - <key>HideFromEditor</key> - <integer>1</integer> + <key>HideFromEditor</key> + <integer>1</integer> <key>Type</key> <string>Boolean</string> <key>Value</key> @@ -11058,17 +11130,17 @@ <key>Value</key> <integer>0</integer> </map> - <key>ShowTutorial</key> - <map> - <key>Comment</key> - <string>Show tutorial window on login</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>Boolean</string> - <key>Value</key> - <integer>0</integer> - </map> + <key>ShowTutorial</key> + <map> + <key>Comment</key> + <string>Show tutorial window on login</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> <key>ShowVoiceVisualizersInCalls</key> <map> <key>Comment</key> @@ -13708,17 +13780,17 @@ <key>Value</key> <real>0.40000000596</real> </map> - <key>moapbeacon</key> - <map> - <key>Comment</key> - <string>Beacon / Highlight media on a prim sources</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>Boolean</string> - <key>Value</key> - <integer>0</integer> - </map> + <key>moapbeacon</key> + <map> + <key>Comment</key> + <string>Beacon / Highlight media on a prim sources</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> <key>particlesbeacon</key> <map> <key>Comment</key> @@ -13818,17 +13890,17 @@ <key>Value</key> <integer>0</integer> </map> - <key>SLURLDragNDrop</key> - <map> - <key>Comment</key> - <string>Enable drag and drop of SLURLs onto the viewer</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>Boolean</string> - <key>Value</key> - <integer>1</integer> - </map> + <key>SLURLDragNDrop</key> + <map> + <key>Comment</key> + <string>Enable drag and drop of SLURLs onto the viewer</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>1</integer> + </map> <key>SLURLPassToOtherInstance</key> <map> <key>Comment</key> @@ -14016,10 +14088,10 @@ <string>LLSD</string> <key>Value</key> <array> - <string>snapshot</string> - <string>postcard</string> - <string>mini_map</string> - <string>beacons</string> + <string>snapshot</string> + <string>postcard</string> + <string>mini_map</string> + <string>beacons</string> </array> </map> <key>LandmarksSortedByDate</key> @@ -14780,7 +14852,7 @@ <key>Value</key> <integer>0</integer> </map> - <key>LocalTerrainAsset1</key> + <key>LocalTerrainAsset1</key> <map> <key>Comment</key> <string>If set to a non-null UUID, overrides the terrain asset locally for all regions with material assets. Local terrain assets are not visible to others. Please keep in mind that this debug setting may be temporary. Do not rely on this setting existing in future viewer builds.</string> @@ -14791,7 +14863,7 @@ <key>Value</key> <string>00000000-0000-0000-0000-000000000000</string> </map> - <key>LocalTerrainAsset2</key> + <key>LocalTerrainAsset2</key> <map> <key>Comment</key> <string>If set to a non-null UUID, overrides the terrain asset locally for all regions with material assets. Local terrain assets are not visible to others. Please keep in mind that this debug setting may be temporary. Do not rely on this setting existing in future viewer builds.</string> @@ -14802,7 +14874,7 @@ <key>Value</key> <string>00000000-0000-0000-0000-000000000000</string> </map> - <key>LocalTerrainAsset3</key> + <key>LocalTerrainAsset3</key> <map> <key>Comment</key> <string>If set to a non-null UUID, overrides the terrain asset locally for all regions with material assets. Local terrain assets are not visible to others. Please keep in mind that this debug setting may be temporary. Do not rely on this setting existing in future viewer builds.</string> @@ -14813,7 +14885,7 @@ <key>Value</key> <string>00000000-0000-0000-0000-000000000000</string> </map> - <key>LocalTerrainAsset4</key> + <key>LocalTerrainAsset4</key> <map> <key>Comment</key> <string>If set to a non-null UUID, overrides the terrain asset locally for all regions with material assets. Local terrain assets are not visible to others. Please keep in mind that this debug setting may be temporary. Do not rely on this setting existing in future viewer builds.</string> @@ -15077,7 +15149,7 @@ <key>Value</key> <integer>2048</integer> </map> - <key>PathfindingRetrieveNeighboringRegion</key> + <key>PathfindingRetrieveNeighboringRegion</key> <map> <key>Comment</key> <string>Download a neighboring region when visualizing a pathfinding navmesh (default val 99 means do not download neighbors).</string> @@ -15086,9 +15158,9 @@ <key>Type</key> <string>U32</string> <key>Value</key> - <integer>99</integer> + <integer>99</integer> </map> - <key>PathfindingNavMeshClear</key> + <key>PathfindingNavMeshClear</key> <map> <key>Comment</key> <string>Background color when displaying pathfinding navmesh.</string> @@ -15248,7 +15320,7 @@ <real>1.0</real> </array> </map> - <key>PathfindingTestPathValidEndColor</key> + <key>PathfindingTestPathValidEndColor</key> <map> <key>Comment</key> <string>Color of the pathfinding test-pathing tool end-point when the path is valid.</string> @@ -15280,7 +15352,7 @@ <real>1.0</real> </array> </map> - <key>PathfindingTestPathColor</key> + <key>PathfindingTestPathColor</key> <map> <key>Comment</key> <string>Color of the pathfinding test-path when the path is valid.</string> @@ -15928,17 +16000,17 @@ <key>Value</key> <real>300</real> </map> - <key>StatsReportFileInterval</key> - <map> - <key>Comment</key> - <string>Interval to save viewer stats file data</string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>F32</string> - <key>Value</key> - <real>0.2</real> - </map> + <key>StatsReportFileInterval</key> + <map> + <key>Comment</key> + <string>Interval to save viewer stats file data</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.2</real> + </map> <key>StatsReportSkipZeroDataSaves</key> <map> <key>Comment</key> diff --git a/indra/newview/groupchatlistener.cpp b/indra/newview/groupchatlistener.cpp index 43507f13e9..e48cbe824a 100644 --- a/indra/newview/groupchatlistener.cpp +++ b/indra/newview/groupchatlistener.cpp @@ -2,11 +2,11 @@ * @file groupchatlistener.cpp * @author Nat Goodspeed * @date 2011-04-11 - * @brief Implementation for groupchatlistener. + * @brief Implementation for LLGroupChatListener. * - * $LicenseInfo:firstyear=2011&license=viewerlgpl$ + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2011, Linden Research, Inc. + * Copyright (C) 2024, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -34,43 +34,82 @@ // std headers // external library headers // other Linden headers +#include "llchat.h" #include "llgroupactions.h" #include "llimview.h" +static const F64 GROUP_CHAT_THROTTLE_PERIOD = 1.f; -namespace { - void startIm_wrapper(LLSD const & event) +LLGroupChatListener::LLGroupChatListener(): + LLEventAPI("GroupChat", + "API to enter, leave, send and intercept group chat messages"), + mIMThrottle("sendGroupIM", &LLGroupChatListener::sendGroupIM_, this, + GROUP_CHAT_THROTTLE_PERIOD) +{ + add("startGroupChat", + "Enter a group chat in group with UUID [\"group_id\"]\n" + "Assumes the logged-in agent is already a member of this group.", + &LLGroupChatListener::startGroupChat, + llsd::map("group_id", LLSD())); + add("leaveGroupChat", + "Leave a group chat in group with UUID [\"group_id\"]\n" + "Assumes a prior successful startIM request.", + &LLGroupChatListener::leaveGroupChat, + llsd::map("group_id", LLSD())); + add("sendGroupIM", + "send a [\"message\"] to group with UUID [\"group_id\"]", + &LLGroupChatListener::sendGroupIM, + llsd::map("message", LLSD(), "group_id", LLSD())); +} + +bool is_in_group(LLEventAPI::Response &response, const LLSD &data) +{ + if (!LLGroupActions::isInGroup(data["group_id"])) { - LLUUID session_id = LLGroupActions::startIM(event["id"].asUUID()); - sendReply(LLSDMap("session_id", LLSD(session_id)), event); + response.error(stringize("You are not the member of the group:", std::quoted(data["group_id"].asString()))); + return false; } + return true; +} - void send_message_wrapper(const std::string& text, const LLUUID& session_id, const LLUUID& group_id) +void LLGroupChatListener::startGroupChat(LLSD const &data) +{ + Response response(LLSD(), data); + if (!is_in_group(response, data)) + { + return; + } + if (LLGroupActions::startIM(data["group_id"]).isNull()) { - LLIMModel::sendMessage(text, session_id, group_id, IM_SESSION_GROUP_START); + return response.error(stringize("Failed to start group chat session ", std::quoted(data["group_id"].asString()))); } } +void LLGroupChatListener::leaveGroupChat(LLSD const &data) +{ + Response response(LLSD(), data); + if (is_in_group(response, data)) + { + LLGroupActions::endIM(data["group_id"].asUUID()); + } +} -GroupChatListener::GroupChatListener(): - LLEventAPI("GroupChat", - "API to enter, leave, send and intercept group chat messages") +void LLGroupChatListener::sendGroupIM(LLSD const &data) { - add("startIM", - "Enter a group chat in group with UUID [\"id\"]\n" - "Assumes the logged-in agent is already a member of this group.", - &startIm_wrapper); - add("endIM", - "Leave a group chat in group with UUID [\"id\"]\n" - "Assumes a prior successful startIM request.", - &LLGroupActions::endIM, - llsd::array("id")); - add("sendIM", - "send a groupchat IM", - &send_message_wrapper, - llsd::array("text", "session_id", "group_id")); + Response response(LLSD(), data); + if (!is_in_group(response, data)) + { + return; + } + + mIMThrottle(data["group_id"], data["message"]); +} + +void LLGroupChatListener::sendGroupIM_(const LLUUID& group_id, const std::string& message) +{ + LLIMModel::sendMessage(LUA_PREFIX + message, + gIMMgr->computeSessionID(IM_SESSION_GROUP_START, group_id), + group_id, + IM_SESSION_SEND); } -/* - static void sendMessage(const std::string& utf8_text, const LLUUID& im_session_id, - const LLUUID& other_participant_id, EInstantMessage dialog); -*/ + diff --git a/indra/newview/groupchatlistener.h b/indra/newview/groupchatlistener.h index 3819ac59b7..a75fecb254 100644 --- a/indra/newview/groupchatlistener.h +++ b/indra/newview/groupchatlistener.h @@ -4,9 +4,9 @@ * @date 2011-04-11 * @brief * - * $LicenseInfo:firstyear=2011&license=viewerlgpl$ + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2011, Linden Research, Inc. + * Copyright (C) 2024, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -26,15 +26,24 @@ * $/LicenseInfo$ */ -#if ! defined(LL_GROUPCHATLISTENER_H) -#define LL_GROUPCHATLISTENER_H +#if ! defined(LL_LLGROUPCHATLISTENER_H) +#define LL_LLGROUPCHATLISTENER_H #include "lleventapi.h" +#include "throttle.h" -class GroupChatListener: public LLEventAPI +class LLGroupChatListener: public LLEventAPI { public: - GroupChatListener(); + LLGroupChatListener(); + +private: + void startGroupChat(LLSD const &data); + void leaveGroupChat(LLSD const &data); + void sendGroupIM(LLSD const &data); + void sendGroupIM_(const LLUUID& group_id, const std::string& message); + + LogThrottle<LLError::LEVEL_DEBUG, void(const LLUUID&, const std::string&)> mIMThrottle; }; -#endif /* ! defined(LL_GROUPCHATLISTENER_H) */ +#endif /* ! defined(LL_LLGROUPCHATLISTENER_H) */ diff --git a/indra/newview/llagentlistener.cpp b/indra/newview/llagentlistener.cpp index 0c120ae01d..d9002bf073 100644 --- a/indra/newview/llagentlistener.cpp +++ b/indra/newview/llagentlistener.cpp @@ -31,23 +31,30 @@ #include "llagentlistener.h" #include "llagent.h" +#include "llagentcamera.h" #include "llvoavatar.h" #include "llcommandhandler.h" +#include "llinventorymodel.h" #include "llslurl.h" #include "llurldispatcher.h" #include "llviewernetwork.h" #include "llviewerobject.h" #include "llviewerobjectlist.h" #include "llviewerregion.h" +#include "llvoavatarself.h" #include "llsdutil.h" #include "llsdutil_math.h" #include "lltoolgrab.h" #include "llhudeffectlookat.h" #include "llagentcamera.h" +#include <functional> + +static const F64 PLAY_ANIM_THROTTLE_PERIOD = 1.f; LLAgentListener::LLAgentListener(LLAgent &agent) : LLEventAPI("LLAgent", "LLAgent listener to (e.g.) teleport, sit, stand, etc."), + mPlayAnimThrottle("playAnimation", &LLAgentListener::playAnimation_, this, PLAY_ANIM_THROTTLE_PERIOD), mAgent(agent) { add("requestTeleport", @@ -69,13 +76,6 @@ LLAgentListener::LLAgentListener(LLAgent &agent) add("resetAxes", "Set the agent to a fixed orientation (optionally specify [\"lookat\"] = array of [x, y, z])", &LLAgentListener::resetAxes); - add("getAxes", - "Obsolete - use getPosition instead\n" - "Send information about the agent's orientation on [\"reply\"]:\n" - "[\"euler\"]: map of {roll, pitch, yaw}\n" - "[\"quat\"]: array of [x, y, z, w] quaternion values", - &LLAgentListener::getAxes, - LLSDMap("reply", LLSD())); add("getPosition", "Send information about the agent's position and orientation on [\"reply\"]:\n" "[\"region\"]: array of region {x, y, z} position\n" @@ -138,6 +138,43 @@ LLAgentListener::LLAgentListener(LLAgent &agent) "[\"contrib\"]: user's land contribution to this group\n", &LLAgentListener::getGroups, LLSDMap("reply", LLSD())); + //camera params are similar to LSL, see https://wiki.secondlife.com/wiki/LlSetCameraParams + add("setCameraParams", + "Set Follow camera params, and then activate it:\n" + "[\"camera_pos\"]: vector3, camera position in region coordinates\n" + "[\"focus_pos\"]: vector3, what the camera is aimed at (in region coordinates)\n" + "[\"focus_offset\"]: vector3, adjusts the camera focus position relative to the target, default is (1, 0, 0)\n" + "[\"distance\"]: float (meters), distance the camera wants to be from its target, default is 3\n" + "[\"focus_threshold\"]: float (meters), sets the radius of a sphere around the camera's target position within which its focus is not affected by target motion, default is 1\n" + "[\"camera_threshold\"]: float (meters), sets the radius of a sphere around the camera's ideal position within which it is not affected by target motion, default is 1\n" + "[\"focus_lag\"]: float (seconds), how much the camera lags as it tries to aim towards the target, default is 0.1\n" + "[\"camera_lag\"]: float (seconds), how much the camera lags as it tries to move towards its 'ideal' position, default is 0.1\n" + "[\"camera_pitch\"]: float (degrees), adjusts the angular amount that the camera aims straight ahead vs. straight down, maintaining the same distance, default is 0\n" + "[\"behindness_angle\"]: float (degrees), sets the angle in degrees within which the camera is not constrained by changes in target rotation, default is 10\n" + "[\"behindness_lag\"]: float (seconds), sets how strongly the camera is forced to stay behind the target if outside of behindness angle, default is 0\n" + "[\"camera_locked\"]: bool, locks the camera position so it will not move\n" + "[\"focus_locked\"]: bool, locks the camera focus so it will not move", + &LLAgentListener::setFollowCamParams); + add("setFollowCamActive", + "Turns on or off scripted control of the camera using boolean [\"active\"]", + &LLAgentListener::setFollowCamActive, + llsd::map("active", LLSD())); + add("removeCameraParams", + "Reset Follow camera params", + &LLAgentListener::removeFollowCamParams); + + add("playAnimation", + "Play [\"item_id\"] animation locally (by default) or [\"inworld\"] (when set to true)", + &LLAgentListener::playAnimation, + llsd::map("item_id", LLSD(), "reply", LLSD())); + add("stopAnimation", + "Stop playing [\"item_id\"] animation", + &LLAgentListener::stopAnimation, + llsd::map("item_id", LLSD(), "reply", LLSD())); + add("getAnimationInfo", + "Return information about [\"item_id\"] animation", + &LLAgentListener::getAnimationInfo, + llsd::map("item_id", LLSD(), "reply", LLSD())); } void LLAgentListener::requestTeleport(LLSD const & event_data) const @@ -296,22 +333,6 @@ void LLAgentListener::resetAxes(const LLSD& event_data) const } } -void LLAgentListener::getAxes(const LLSD& event_data) const -{ - LLQuaternion quat(mAgent.getQuat()); - F32 roll, pitch, yaw; - quat.getEulerAngles(&roll, &pitch, &yaw); - // The official query API for LLQuaternion's [x, y, z, w] values is its - // public member mQ... - LLSD reply = LLSD::emptyMap(); - reply["quat"] = llsd_copy_array(boost::begin(quat.mQ), boost::end(quat.mQ)); - reply["euler"] = LLSD::emptyMap(); - reply["euler"]["roll"] = roll; - reply["euler"]["pitch"] = pitch; - reply["euler"]["yaw"] = yaw; - sendReply(reply, event_data); -} - void LLAgentListener::getPosition(const LLSD& event_data) const { F32 roll, pitch, yaw; @@ -519,3 +540,129 @@ void LLAgentListener::getGroups(const LLSD& event) const } sendReply(LLSDMap("groups", reply), event); } + +/*----------------------------- camera control -----------------------------*/ +// specialize LLSDParam to support (const LLVector3&) arguments -- this +// wouldn't even be necessary except that the relevant LLVector3 constructor +// is explicitly explicit +template <> +class LLSDParam<const LLVector3&>: public LLSDParamBase +{ +public: + LLSDParam(const LLSD& value): value(LLVector3(value)) {} + + operator const LLVector3&() const { return value; } + +private: + LLVector3 value; +}; + +// accept any of a number of similar LLFollowCamMgr methods with different +// argument types, and return a wrapper lambda that accepts LLSD and converts +// to the target argument type +template <typename T> +auto wrap(void (LLFollowCamMgr::*method)(const LLUUID& source, T arg)) +{ + return [method](LLFollowCamMgr& followcam, const LLUUID& source, const LLSD& arg) + { (followcam.*method)(source, LLSDParam<T>(arg)); }; +} + +// table of supported LLFollowCamMgr methods, +// with the corresponding setFollowCamParams() argument keys +static std::pair<std::string, std::function<void(LLFollowCamMgr&, const LLUUID&, const LLSD&)>> +cam_params[] = +{ + { "camera_pos", wrap(&LLFollowCamMgr::setPosition) }, + { "focus_pos", wrap(&LLFollowCamMgr::setFocus) }, + { "focus_offset", wrap(&LLFollowCamMgr::setFocusOffset) }, + { "camera_locked", wrap(&LLFollowCamMgr::setPositionLocked) }, + { "focus_locked", wrap(&LLFollowCamMgr::setFocusLocked) }, + { "distance", wrap(&LLFollowCamMgr::setDistance) }, + { "focus_threshold", wrap(&LLFollowCamMgr::setFocusThreshold) }, + { "camera_threshold", wrap(&LLFollowCamMgr::setPositionThreshold) }, + { "focus_lag", wrap(&LLFollowCamMgr::setFocusLag) }, + { "camera_lag", wrap(&LLFollowCamMgr::setPositionLag) }, + { "camera_pitch", wrap(&LLFollowCamMgr::setPitch) }, + { "behindness_lag", wrap(&LLFollowCamMgr::setBehindnessLag) }, + { "behindness_angle", wrap(&LLFollowCamMgr::setBehindnessAngle) }, +}; + +void LLAgentListener::setFollowCamParams(const LLSD& event) const +{ + auto& followcam{ LLFollowCamMgr::instance() }; + for (const auto& pair : cam_params) + { + if (event.has(pair.first)) + { + pair.second(followcam, gAgentID, event[pair.first]); + } + } + followcam.setCameraActive(gAgentID, true); +} + +void LLAgentListener::setFollowCamActive(LLSD const & event) const +{ + LLFollowCamMgr::getInstance()->setCameraActive(gAgentID, event["active"]); +} + +void LLAgentListener::removeFollowCamParams(LLSD const & event) const +{ + LLFollowCamMgr::getInstance()->removeFollowCamParams(gAgentID); +} + +LLViewerInventoryItem* get_anim_item(LLEventAPI::Response &response, const LLSD &event_data) +{ + LLViewerInventoryItem* item = gInventory.getItem(event_data["item_id"].asUUID()); + if (!item || (item->getInventoryType() != LLInventoryType::IT_ANIMATION)) + { + response.error(stringize("Animation item ", std::quoted(event_data["item_id"].asString()), " was not found")); + return NULL; + } + return item; +} + +void LLAgentListener::playAnimation(LLSD const &event_data) +{ + Response response(LLSD(), event_data); + if (LLViewerInventoryItem* item = get_anim_item(response, event_data)) + { + mPlayAnimThrottle(item->getAssetUUID(), event_data["inworld"].asBoolean()); + } +} + +void LLAgentListener::playAnimation_(const LLUUID& asset_id, const bool inworld) +{ + if (inworld) + { + mAgent.sendAnimationRequest(asset_id, ANIM_REQUEST_START); + } + else + { + gAgentAvatarp->startMotion(asset_id); + } +} + +void LLAgentListener::stopAnimation(LLSD const &event_data) +{ + Response response(LLSD(), event_data); + if (LLViewerInventoryItem* item = get_anim_item(response, event_data)) + { + gAgentAvatarp->stopMotion(item->getAssetUUID()); + mAgent.sendAnimationRequest(item->getAssetUUID(), ANIM_REQUEST_STOP); + } +} + +void LLAgentListener::getAnimationInfo(LLSD const &event_data) +{ + Response response(LLSD(), event_data); + if (LLViewerInventoryItem* item = get_anim_item(response, event_data)) + { + // if motion exists, will return existing one + LLMotion* motion = gAgentAvatarp->createMotion(item->getAssetUUID()); + response["anim_info"] = llsd::map("duration", motion->getDuration(), + "is_loop", motion->getLoop(), + "num_joints", motion->getNumJointMotions(), + "asset_id", item->getAssetUUID(), + "priority", motion->getPriority()); + } +} diff --git a/indra/newview/llagentlistener.h b/indra/newview/llagentlistener.h index c544d089ce..2765bb5b66 100644 --- a/indra/newview/llagentlistener.h +++ b/indra/newview/llagentlistener.h @@ -31,6 +31,7 @@ #define LL_LLAGENTLISTENER_H #include "lleventapi.h" +#include "throttle.h" class LLAgent; class LLSD; @@ -48,7 +49,6 @@ private: void requestStand(LLSD const & event_data) const; void requestTouch(LLSD const & event_data) const; void resetAxes(const LLSD& event_data) const; - void getAxes(const LLSD& event_data) const; void getGroups(const LLSD& event) const; void getPosition(const LLSD& event_data) const; void startAutoPilot(const LLSD& event_data); @@ -58,11 +58,22 @@ private: void stopAutoPilot(const LLSD& event_data) const; void lookAt(LLSD const & event_data) const; + void setFollowCamParams(LLSD const & event_data) const; + void setFollowCamActive(LLSD const & event_data) const; + void removeFollowCamParams(LLSD const & event_data) const; + + void playAnimation(LLSD const &event_data); + void playAnimation_(const LLUUID& asset_id, const bool inworld); + void stopAnimation(LLSD const &event_data); + void getAnimationInfo(LLSD const &event_data); + LLViewerObject * findObjectClosestTo( const LLVector3 & position ) const; private: LLAgent & mAgent; LLUUID mFollowTarget; + + LogThrottle<LLError::LEVEL_DEBUG, void(const LLUUID &, const bool)> mPlayAnimThrottle; }; #endif // LL_LLAGENTLISTENER_H diff --git a/indra/newview/llappearancelistener.cpp b/indra/newview/llappearancelistener.cpp new file mode 100644 index 0000000000..0dab352311 --- /dev/null +++ b/indra/newview/llappearancelistener.cpp @@ -0,0 +1,146 @@ +/** + * @file llappearancelistener.cpp + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2024, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llappearancelistener.h" + +#include "llappearancemgr.h" +#include "llinventoryfunctions.h" +#include "lltransutil.h" +#include "llwearableitemslist.h" +#include "stringize.h" + +LLAppearanceListener::LLAppearanceListener() + : LLEventAPI("LLAppearance", + "API to wear a specified outfit and wear/remove individual items") +{ + add("wearOutfit", + "Wear outfit by folder id: [\"folder_id\"] OR by folder name: [\"folder_name\"]\n" + "When [\"append\"] is true, outfit will be added to COF\n" + "otherwise it will replace current oufit", + &LLAppearanceListener::wearOutfit); + + add("wearItems", + "Wear items by id: [items_id]", + &LLAppearanceListener::wearItems, + llsd::map("items_id", LLSD(), "replace", LLSD())); + + add("detachItems", + "Detach items by id: [items_id]", + &LLAppearanceListener::detachItems, + llsd::map("items_id", LLSD())); + + add("getOutfitsList", + "Return the table with Outfits info(id and name)", + &LLAppearanceListener::getOutfitsList); + + add("getOutfitItems", + "Return the table of items with info(id : name, wearable_type, is_worn) inside specified outfit folder", + &LLAppearanceListener::getOutfitItems); +} + + +void LLAppearanceListener::wearOutfit(LLSD const &data) +{ + Response response(LLSD(), data); + if (!data.has("folder_id") && !data.has("folder_name")) + { + return response.error("Either [folder_id] or [folder_name] is required"); + } + + std::string error_msg; + bool result(false); + bool append = data.has("append") ? data["append"].asBoolean() : false; + if (data.has("folder_id")) + { + result = LLAppearanceMgr::instance().wearOutfit(data["folder_id"].asUUID(), error_msg, append); + } + else + { + result = LLAppearanceMgr::instance().wearOutfitByName(data["folder_name"].asString(), error_msg, append); + } + + if (!result) + { + response.error(error_msg); + } +} + +void LLAppearanceListener::wearItems(LLSD const &data) +{ + LLAppearanceMgr::instance().wearItemsOnAvatar( + LLSDParam<uuid_vec_t>(data["items_id"]), + true, data["replace"].asBoolean()); +} + +void LLAppearanceListener::detachItems(LLSD const &data) +{ + LLAppearanceMgr::instance().removeItemsFromAvatar( + LLSDParam<uuid_vec_t>(data["items_id"])); +} + +void LLAppearanceListener::getOutfitsList(LLSD const &data) +{ + Response response(LLSD(), data); + const LLUUID outfits_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_MY_OUTFITS); + + LLInventoryModel::cat_array_t cat_array; + LLInventoryModel::item_array_t item_array; + + LLIsType is_category(LLAssetType::AT_CATEGORY); + gInventory.collectDescendentsIf(outfits_id, cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH, is_category); + + response["outfits"] = llsd::toMap(cat_array, + [](const LLPointer<LLViewerInventoryCategory> &cat) + { return std::make_pair(cat->getUUID().asString(), cat->getName()); }); +} + +void LLAppearanceListener::getOutfitItems(LLSD const &data) +{ + Response response(LLSD(), data); + LLUUID outfit_id(data["outfit_id"].asUUID()); + LLViewerInventoryCategory *cat = gInventory.getCategory(outfit_id); + if (!cat || cat->getPreferredType() != LLFolderType::FT_OUTFIT) + { + return response.error(stringize(LLTrans::getString("OutfitNotFound"), outfit_id.asString())); + } + LLInventoryModel::cat_array_t cat_array; + LLInventoryModel::item_array_t item_array; + + LLFindOutfitItems collector = LLFindOutfitItems(); + gInventory.collectDescendentsIf(outfit_id, cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH, collector); + + response["items"] = llsd::toMap(item_array, + [](const LLPointer<LLViewerInventoryItem> &it) + { + return std::make_pair( + it->getUUID().asString(), + llsd::map( + "name", it->getName(), + "wearable_type", LLWearableType::getInstance()->getTypeName(it->isWearableType() ? it->getWearableType() : LLWearableType::WT_NONE), + "is_worn", get_is_item_worn(it))); + }); +} diff --git a/indra/llcommon/llerrorlegacy.h b/indra/newview/llappearancelistener.h index 693e1501d5..04c5eac2eb 100644 --- a/indra/llcommon/llerrorlegacy.h +++ b/indra/newview/llappearancelistener.h @@ -1,11 +1,9 @@ /** - * @file llerrorlegacy.h - * @date January 2007 - * @brief old things from the older error system + * @file llappearancelistener.h * - * $LicenseInfo:firstyear=2007&license=viewerlgpl$ + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2024, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -25,8 +23,24 @@ * $/LicenseInfo$ */ -#ifndef LL_LLERRORLEGACY_H -#define LL_LLERRORLEGACY_H +#ifndef LL_LLAPPEARANCELISTENER_H +#define LL_LLAPPEARANCELISTENER_H + +#include "lleventapi.h" + +class LLAppearanceListener : public LLEventAPI +{ +public: + LLAppearanceListener(); + +private: + void wearOutfit(LLSD const &data); + void wearItems(LLSD const &data); + void detachItems(LLSD const &data); + void getOutfitsList(LLSD const &data); + void getOutfitItems(LLSD const &data); +}; + +#endif // LL_LLAPPEARANCELISTENER_H -#endif // LL_LLERRORLEGACY_H diff --git a/indra/newview/llappearancemgr.cpp b/indra/newview/llappearancemgr.cpp index 946d674e8b..2877bb7c49 100644 --- a/indra/newview/llappearancemgr.cpp +++ b/indra/newview/llappearancemgr.cpp @@ -31,6 +31,7 @@ #include "llagent.h" #include "llagentcamera.h" #include "llagentwearables.h" +#include "llappearancelistener.h" #include "llappearancemgr.h" #include "llattachmentsmgr.h" #include "llcommandhandler.h" @@ -48,6 +49,7 @@ #include "lloutfitslist.h" #include "llselectmgr.h" #include "llsidepanelappearance.h" +#include "lltransutil.h" #include "llviewerobjectlist.h" #include "llvoavatar.h" #include "llvoavatarself.h" @@ -66,6 +68,8 @@ #include "llavatarpropertiesprocessor.h" +LLAppearanceListener sAppearanceListener; + namespace { const S32 BAKE_RETRY_MAX_COUNT = 5; @@ -113,26 +117,16 @@ public: LLOutfitUnLockTimer(F32 period) : LLEventTimer(period) { // restart timer on BOF changed event - LLOutfitObserver::instance().addBOFChangedCallback(boost::bind( - &LLOutfitUnLockTimer::reset, this)); + LLOutfitObserver::instance().addBOFChangedCallback([this]{ start(); }); stop(); } - /*virtual*/ - bool tick() + bool tick() override { - if(mEventTimer.hasExpired()) - { - LLAppearanceMgr::instance().setOutfitLocked(false); - } + LLAppearanceMgr::instance().setOutfitLocked(false); return false; } - void stop() { mEventTimer.stop(); } - void start() { mEventTimer.start(); } - void reset() { mEventTimer.reset(); } - bool getStarted() { return mEventTimer.getStarted(); } - - LLTimer& getEventTimer() { return mEventTimer;} + bool getStarted() { return isRunning(); } }; // support for secondlife:///app/appearance SLapps @@ -327,7 +321,7 @@ public: // virtual // Will be deleted after returning true - only safe to do this if all callbacks have fired. - bool tick() + bool tick() override { // mPendingRequests will be zero if all requests have been // responded to. mWaitTimes.empty() will be true if we have @@ -620,8 +614,8 @@ void LLBrokenLinkObserver::changed(U32 mask) if (id == mUUID) { // Might not be processed yet and it is not a - // good idea to update appearane here, postpone. - doOnIdleOneTime([this]() + // good idea to update appearance here, postpone. + doOnIdleOneTime([this] { postProcess(); }); @@ -1707,7 +1701,6 @@ void LLAppearanceMgr::setOutfitLocked(bool locked) mOutfitLocked = locked; if (locked) { - mUnlockOutfitTimer->reset(); mUnlockOutfitTimer->start(); } else @@ -2906,8 +2899,18 @@ void LLAppearanceMgr::wearInventoryCategoryOnAvatar( LLInventoryCategory* catego LLAppearanceMgr::changeOutfit(true, category->getUUID(), append); } -// FIXME do we really want to search entire inventory for matching name? -void LLAppearanceMgr::wearOutfitByName(const std::string& name) +bool LLAppearanceMgr::wearOutfitByName(const std::string& name, bool append) +{ + std::string error_msg; + if(!wearOutfitByName(name, error_msg, append)) + { + LL_WARNS() << error_msg << LL_ENDL; + return false; + } + return true; +} + +bool LLAppearanceMgr::wearOutfitByName(const std::string& name, std::string& error_msg, bool append) { LL_INFOS("Avatar") << self_av_string() << "Wearing category " << name << LL_ENDL; @@ -2940,15 +2943,38 @@ void LLAppearanceMgr::wearOutfitByName(const std::string& name) } } - if(cat) + return wearOutfit(stringize(std::quoted(name)), cat, error_msg, copy_items, append); +} + +bool LLAppearanceMgr::wearOutfit(const LLUUID &cat_id, std::string &error_msg, bool append) +{ + LLViewerInventoryCategory *cat = gInventory.getCategory(cat_id); + return wearOutfit(stringize(cat_id), cat, error_msg, false, append); +} + +bool LLAppearanceMgr::wearOutfit(const std::string &desc, LLInventoryCategory* cat, + std::string &error_msg, bool copy_items, bool append) +{ + if (!cat) + { + error_msg = stringize(LLTrans::getString("OutfitNotFound"), desc); + return false; + } + // don't allow wearing a system folder + if (LLFolderType::lookupIsProtectedType(cat->getPreferredType())) { - LLAppearanceMgr::wearInventoryCategory(cat, copy_items, false); + error_msg = stringize(LLTrans::getString("SystemFolderNotWorn"), std::quoted(cat->getName())); + return false; } - else + bool can_wear = append ? getCanAddToCOF(cat->getUUID()) : getCanReplaceCOF(cat->getUUID()); + if (!can_wear) { - LL_WARNS() << "Couldn't find outfit " <<name<< " in wearOutfitByName()" - << LL_ENDL; + std::string msg = append ? LLTrans::getString("OutfitNotAdded") : LLTrans::getString("OutfitNotReplaced"); + error_msg = stringize(msg, std::quoted(cat->getName()), " , id: ", cat->getUUID()); + return false; } + wearInventoryCategory(cat, copy_items, append); + return true; } bool areMatchingWearables(const LLViewerInventoryItem *a, const LLViewerInventoryItem *b) diff --git a/indra/newview/llappearancemgr.h b/indra/newview/llappearancemgr.h index 6c45a32856..9e624f593f 100644 --- a/indra/newview/llappearancemgr.h +++ b/indra/newview/llappearancemgr.h @@ -59,7 +59,9 @@ public: void wearInventoryCategory(LLInventoryCategory* category, bool copy, bool append); void wearInventoryCategoryOnAvatar(LLInventoryCategory* category, bool append); void wearCategoryFinal(const LLUUID& cat_id, bool copy_items, bool append); - void wearOutfitByName(const std::string& name); + bool wearOutfit(const LLUUID &cat_id, std::string &error_msg, bool append = false); + bool wearOutfitByName(const std::string &name, std::string &error_msg, bool append = false); + bool wearOutfitByName(const std::string &name, bool append = false); void changeOutfit(bool proceed, const LLUUID& category, bool append); void replaceCurrentOutfit(const LLUUID& new_outfit); void renameOutfit(const LLUUID& outfit_id); @@ -261,6 +263,10 @@ private: static void onOutfitRename(const LLSD& notification, const LLSD& response); + // used by both wearOutfit(LLUUID) and wearOutfitByName(std::string) + bool wearOutfit(const std::string &desc, LLInventoryCategory* cat, + std::string &error_msg, bool copy_items, bool append); + bool mAttachmentInvLinkEnabled; bool mOutfitIsDirty; bool mIsInUpdateAppearanceFromCOF; // to detect recursive calls. diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 4eb4f5ae20..789aaab70d 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -29,6 +29,7 @@ #include "llappviewer.h" // Viewer includes +#include "coro_scheduler.h" #include "llversioninfo.h" #include "llfeaturemanager.h" #include "lluictrlfactory.h" @@ -60,6 +61,7 @@ #include "llslurl.h" #include "llstartup.h" #include "llfocusmgr.h" +#include "llluamanager.h" #include "llurlfloaterdispatchhandler.h" #include "llviewerjoystick.h" #include "llcalc.h" @@ -111,10 +113,12 @@ #include "llgltfmateriallist.h" // Linden library includes +#include "fsyspath.h" #include "llavatarnamecache.h" #include "lldiriterator.h" #include "llexperiencecache.h" #include "llimagej2c.h" +#include "llluamanager.h" #include "llmemory.h" #include "llprimitive.h" #include "llurlaction.h" @@ -380,6 +384,9 @@ static std::string gLaunchFileOnQuit; // Used on Win32 for other apps to identify our window (eg, win_setup) const char* const VIEWER_WINDOW_CLASSNAME = "Second Life"; +void processComposeSwitch(const std::string&, const std::string&, + const std::function<void(const LLSD&)>&); + //---------------------------------------------------------------------------- // List of entries from strings.xml to always replace @@ -757,6 +764,8 @@ bool LLAppViewer::init() //set the max heap size. initMaxHeapSize() ; LLCoros::instance().setStackSize(gSavedSettings.getS32("CoroutineStackSize")); + // Use our custom scheduler for coroutine scheduling. + llcoro::scheduler::use(); // Although initLoggingAndGetLastDuration() is the right place to mess with // setFatalFunction(), we can't query gSavedSettings until after @@ -1202,22 +1211,10 @@ bool LLAppViewer::init() } #endif //LL_RELEASE_FOR_DOWNLOAD - { - // Iterate over --leap command-line options. But this is a bit tricky: if - // there's only one, it won't be an array at all. - LLSD LeapCommand(gSavedSettings.getLLSD("LeapCommand")); - LL_DEBUGS("InitInfo") << "LeapCommand: " << LeapCommand << LL_ENDL; - if (LeapCommand.isDefined() && !LeapCommand.isArray()) - { - // If LeapCommand is actually a scalar value, make an array of it. - // Have to do it in two steps because LeapCommand.append(LeapCommand) - // trashes content! :-P - LLSD item(LeapCommand); - LeapCommand.append(item); - } - for (const auto& leap : llsd::inArray(LeapCommand)) + processComposeSwitch( + "--leap", "LeapCommand", + [](const LLSD& leap) { - LL_INFOS("InitInfo") << "processing --leap \"" << leap << '"' << LL_ENDL; // We don't have any better description of this plugin than the // user-specified command line. Passing "" causes LLLeap to derive a // description from the command line itself. @@ -1225,8 +1222,54 @@ bool LLAppViewer::init() // don't consider any one --leap command mission-critical, so if one // fails, log it, shrug and carry on. LLLeap::create("", leap, false); // exception=false - } - } + }); + processComposeSwitch( + "--lua", "LuaChunk", + [](const LLSD& chunk) + { + // no completion callback: we don't need to know + LLLUAmanager::runScriptLine(chunk); + }); + processComposeSwitch( + "--luafile", "LuaScript", + [](const LLSD& script) + { + LLSD paths(gSavedSettings.getLLSD("LuaCommandPath")); + LL_DEBUGS("Lua") << "LuaCommandPath = " << paths << LL_ENDL; + for (const auto& path : llsd::inArray(paths)) + { + // if script path is already absolute, operator/() preserves it + auto abspath(fsyspath(gDirUtilp->getAppRODataDir()) / path.asString()); + auto absscript{ (abspath / script.asString()) }; + std::error_code ec; + if (std::filesystem::exists(absscript, ec)) + { + // no completion callback: we don't need to know + LLLUAmanager::runScriptFile(absscript.u8string()); + return; // from lambda + } + } + LL_WARNS("Lua") << "--luafile " << std::quoted(script.asString()) + << " not found on " << paths << LL_ENDL; + }); + processComposeSwitch( + "LuaAutorunPath", "LuaAutorunPath", + [](const LLSD& directory) + { + // each directory can be relative to the viewer's install + // directory -- if directory is already absolute, operator/() + // preserves it + auto abspath(fsyspath(gDirUtilp->getAppRODataDir()) / directory.asString()); + std::string absdir(abspath.string()); + LL_DEBUGS("InitInfo") << "LuaAutorunPath: " << absdir << LL_ENDL; + LLDirIterator scripts(absdir, "*.lua"); + std::string script; + while (scripts.next(script)) + { + LL_DEBUGS("InitInfo") << "LuaAutorunPath: " << absdir << ": " << script << LL_ENDL; + LLLUAmanager::runScriptFile((abspath / script).string(), true); + } + }); if (gSavedSettings.getBOOL("QAMode") && gSavedSettings.getS32("QAModeEventHostPort") > 0) { @@ -1302,6 +1345,27 @@ bool LLAppViewer::init() return true; } +void processComposeSwitch(const std::string& option, + const std::string& setting, + const std::function<void(const LLSD&)>& action) +{ + // Iterate over 'option' command-line options. But this is a bit tricky: + // if there's only one, it won't be an array at all. + LLSD args(gSavedSettings.getLLSD(setting)); + LL_DEBUGS("InitInfo") << option << ": " << args << LL_ENDL; + if (args.isDefined() && ! args.isArray()) + { + // If args is actually a scalar value, make an array of it. Have to do + // it in two steps because args.append(args) trashes content! :-P + args.append(LLSD(args)); + } + for (const auto& arg : llsd::inArray(args)) + { + LL_INFOS("InitInfo") << "processing " << option << ' ' << arg << LL_ENDL; + action(arg); + } +} + void LLAppViewer::initMaxHeapSize() { //set the max heap size. @@ -4607,7 +4671,6 @@ void LLAppViewer::idle() LLFrameTimer::updateFrameTime(); LLFrameTimer::updateFrameCount(); - LLEventTimer::updateClass(); LLPerfStats::updateClass(); // LLApp::stepFrame() performs the above three calls plus mRunner.run(). diff --git a/indra/newview/llappviewerlistener.cpp b/indra/newview/llappviewerlistener.cpp index 6d519b6fef..d02b1b4a79 100644 --- a/indra/newview/llappviewerlistener.cpp +++ b/indra/newview/llappviewerlistener.cpp @@ -42,6 +42,9 @@ LLAppViewerListener::LLAppViewerListener(const LLAppViewerGetter& getter): mAppViewerGetter(getter) { // add() every method we want to be able to invoke via this event API. + add("userQuit", + "Ask to quit with user confirmation prompt", + &LLAppViewerListener::userQuit); add("requestQuit", "Ask to quit nicely", &LLAppViewerListener::requestQuit); @@ -50,6 +53,12 @@ LLAppViewerListener::LLAppViewerListener(const LLAppViewerGetter& getter): &LLAppViewerListener::forceQuit); } +void LLAppViewerListener::userQuit(const LLSD& event) +{ + LL_INFOS() << "Listener requested user quit" << LL_ENDL; + mAppViewerGetter()->userQuit(); +} + void LLAppViewerListener::requestQuit(const LLSD& event) { LL_INFOS() << "Listener requested quit" << LL_ENDL; diff --git a/indra/newview/llappviewerlistener.h b/indra/newview/llappviewerlistener.h index 5ade3d3e04..e116175eb7 100644 --- a/indra/newview/llappviewerlistener.h +++ b/indra/newview/llappviewerlistener.h @@ -30,7 +30,7 @@ #define LL_LLAPPVIEWERLISTENER_H #include "lleventapi.h" -#include <boost/function.hpp> +#include <functional> class LLAppViewer; class LLSD; @@ -39,11 +39,12 @@ class LLSD; class LLAppViewerListener: public LLEventAPI { public: - typedef boost::function<LLAppViewer*(void)> LLAppViewerGetter; + typedef std::function<LLAppViewer*(void)> LLAppViewerGetter; /// Bind the LLAppViewer instance to use (e.g. LLAppViewer::instance()). LLAppViewerListener(const LLAppViewerGetter& getter); private: + void userQuit(const LLSD& event); void requestQuit(const LLSD& event); void forceQuit(const LLSD& event); diff --git a/indra/newview/llblocklist.cpp b/indra/newview/llblocklist.cpp index 89516a8a84..bb1e3405d7 100644 --- a/indra/newview/llblocklist.cpp +++ b/indra/newview/llblocklist.cpp @@ -50,7 +50,7 @@ LLBlockList::LLBlockList(const Params& p) mMuteListSize = static_cast<U32>(LLMuteList::getInstance()->getMutes().size()); // Set up context menu. - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + ScopedRegistrarHelper registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; registrar.add ("Block.Action", boost::bind(&LLBlockList::onCustomAction, this, _2)); diff --git a/indra/newview/llcallbacklist.cpp b/indra/newview/llcallbacklist.cpp deleted file mode 100644 index c474f2885b..0000000000 --- a/indra/newview/llcallbacklist.cpp +++ /dev/null @@ -1,305 +0,0 @@ -/** - * @file llcallbacklist.cpp - * @brief A simple list of callback functions to call. - * - * $LicenseInfo:firstyear=2001&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#include "llviewerprecompiledheaders.h" - -#include "llcallbacklist.h" -#include "lleventtimer.h" - -// Library includes -#include "llerror.h" - - -// -// Globals -// -LLCallbackList gIdleCallbacks; - -// -// Member functions -// - -LLCallbackList::LLCallbackList() -{ - // nothing -} - -LLCallbackList::~LLCallbackList() -{ -} - - -void LLCallbackList::addFunction( callback_t func, void *data) -{ - if (!func) - { - LL_ERRS() << "LLCallbackList::addFunction - function is NULL" << LL_ENDL; - return; - } - - // only add one callback per func/data pair - callback_pair_t t(func, data); - callback_list_t::iterator iter = std::find(mCallbackList.begin(), mCallbackList.end(), t); - if (iter == mCallbackList.end()) - { - mCallbackList.push_back(t); - } -} - - -BOOL LLCallbackList::containsFunction( callback_t func, void *data) -{ - callback_pair_t t(func, data); - callback_list_t::iterator iter = std::find(mCallbackList.begin(), mCallbackList.end(), t); - if (iter != mCallbackList.end()) - { - return TRUE; - } - else - { - return FALSE; - } -} - - -BOOL LLCallbackList::deleteFunction( callback_t func, void *data) -{ - callback_pair_t t(func, data); - callback_list_t::iterator iter = std::find(mCallbackList.begin(), mCallbackList.end(), t); - if (iter != mCallbackList.end()) - { - mCallbackList.erase(iter); - return TRUE; - } - else - { - return FALSE; - } -} - - -void LLCallbackList::deleteAllFunctions() -{ - mCallbackList.clear(); -} - - -void LLCallbackList::callFunctions() -{ - for (callback_list_t::iterator iter = mCallbackList.begin(); iter != mCallbackList.end(); ) - { - callback_list_t::iterator curiter = iter++; - curiter->first(curiter->second); - } -} - -// Shim class to allow arbitrary boost::bind -// expressions to be run as one-time idle callbacks. -class OnIdleCallbackOneTime -{ -public: - OnIdleCallbackOneTime(nullary_func_t callable): - mCallable(callable) - { - } - static void onIdle(void *data) - { - gIdleCallbacks.deleteFunction(onIdle, data); - OnIdleCallbackOneTime* self = reinterpret_cast<OnIdleCallbackOneTime*>(data); - self->call(); - delete self; - } - void call() - { - mCallable(); - } -private: - nullary_func_t mCallable; -}; - -void doOnIdleOneTime(nullary_func_t callable) -{ - OnIdleCallbackOneTime* cb_functor = new OnIdleCallbackOneTime(callable); - gIdleCallbacks.addFunction(&OnIdleCallbackOneTime::onIdle,cb_functor); -} - -// Shim class to allow generic boost functions to be run as -// recurring idle callbacks. Callable should return true when done, -// false to continue getting called. -class OnIdleCallbackRepeating -{ -public: - OnIdleCallbackRepeating(bool_func_t callable): - mCallable(callable) - { - } - // Will keep getting called until the callable returns true. - static void onIdle(void *data) - { - OnIdleCallbackRepeating* self = reinterpret_cast<OnIdleCallbackRepeating*>(data); - bool done = self->call(); - if (done) - { - gIdleCallbacks.deleteFunction(onIdle, data); - delete self; - } - } - bool call() - { - return mCallable(); - } -private: - bool_func_t mCallable; -}; - -void doOnIdleRepeating(bool_func_t callable) -{ - OnIdleCallbackRepeating* cb_functor = new OnIdleCallbackRepeating(callable); - gIdleCallbacks.addFunction(&OnIdleCallbackRepeating::onIdle,cb_functor); -} - -class NullaryFuncEventTimer: public LLEventTimer -{ -public: - NullaryFuncEventTimer(nullary_func_t callable, F32 seconds): - LLEventTimer(seconds), - mCallable(callable) - { - } - -private: - bool tick() - { - mCallable(); - return true; - } - - nullary_func_t mCallable; -}; - -// Call a given callable once after specified interval. -void doAfterInterval(nullary_func_t callable, F32 seconds) -{ - new NullaryFuncEventTimer(callable, seconds); -} - -class BoolFuncEventTimer: public LLEventTimer -{ -public: - BoolFuncEventTimer(bool_func_t callable, F32 seconds): - LLEventTimer(seconds), - mCallable(callable) - { - } -private: - bool tick() - { - return mCallable(); - } - - bool_func_t mCallable; -}; - -// Call a given callable every specified number of seconds, until it returns true. -void doPeriodically(bool_func_t callable, F32 seconds) -{ - new BoolFuncEventTimer(callable, seconds); -} - -#ifdef _DEBUG - -void test1(void *data) -{ - S32 *s32_data = (S32 *)data; - LL_INFOS() << "testfunc1 " << *s32_data << LL_ENDL; -} - - -void test2(void *data) -{ - S32 *s32_data = (S32 *)data; - LL_INFOS() << "testfunc2 " << *s32_data << LL_ENDL; -} - - -void -LLCallbackList::test() -{ - S32 a = 1; - S32 b = 2; - LLCallbackList *list = new LLCallbackList; - - LL_INFOS() << "Testing LLCallbackList" << LL_ENDL; - - if (!list->deleteFunction(NULL)) - { - LL_INFOS() << "passed 1" << LL_ENDL; - } - else - { - LL_INFOS() << "error, removed function from empty list" << LL_ENDL; - } - - // LL_INFOS() << "This should crash" << LL_ENDL; - // list->addFunction(NULL); - - list->addFunction(&test1, &a); - list->addFunction(&test1, &a); - - LL_INFOS() << "Expect: test1 1, test1 1" << LL_ENDL; - list->callFunctions(); - - list->addFunction(&test1, &b); - list->addFunction(&test2, &b); - - LL_INFOS() << "Expect: test1 1, test1 1, test1 2, test2 2" << LL_ENDL; - list->callFunctions(); - - if (list->deleteFunction(&test1, &b)) - { - LL_INFOS() << "passed 3" << LL_ENDL; - } - else - { - LL_INFOS() << "error removing function" << LL_ENDL; - } - - LL_INFOS() << "Expect: test1 1, test1 1, test2 2" << LL_ENDL; - list->callFunctions(); - - list->deleteAllFunctions(); - - LL_INFOS() << "Expect nothing" << LL_ENDL; - list->callFunctions(); - - LL_INFOS() << "nothing :-)" << LL_ENDL; - - delete list; - - LL_INFOS() << "test complete" << LL_ENDL; -} - -#endif // _DEBUG diff --git a/indra/newview/llchathistory.cpp b/indra/newview/llchathistory.cpp index a48e22bc73..3e02820feb 100644 --- a/indra/newview/llchathistory.cpp +++ b/indra/newview/llchathistory.cpp @@ -125,6 +125,7 @@ public: mUserNameTextBox(NULL), mTimeBoxTextBox(NULL), mNeedsTimeBox(true), + mIsFromScript(false), mAvatarNameCacheConnection() {} @@ -658,11 +659,13 @@ public: const LLUUID& getAvatarId () const { return mAvatarID;} - void setup(const LLChat& chat, const LLStyle::Params& style_params, const LLSD& args) + void setup(const LLChat& chat, const LLStyle::Params& style_params, const LLSD& args, bool is_script) { mAvatarID = chat.mFromID; mSessionID = chat.mSessionID; mSourceType = chat.mSourceType; + mIsFromScript = is_script; + mPrefix = mIsFromScript ? LLTrans::getString("ScriptBy") : ""; // To be able to report a message, we need a copy of it's text // and it's easier to store text directly than trying to get @@ -732,7 +735,7 @@ public: username_end == (chat.mFromName.length() - 1)) { mFrom = chat.mFromName.substr(0, username_start); - user_name->setValue(mFrom); + user_name->setValue(mPrefix + mFrom); if (gSavedSettings.getBOOL("NameTagShowUsernames")) { @@ -774,7 +777,7 @@ public: switch (mSourceType) { case CHAT_SOURCE_AGENT: - icon->setValue(chat.mFromID); + icon->setValue(mIsFromScript ? LLSD("Inv_Script") : LLSD(chat.mFromID)); break; case CHAT_SOURCE_OBJECT: icon->setValue(LLSD("OBJECT_Icon")); @@ -787,7 +790,7 @@ public: icon->setValue(LLSD("Command_Destinations_Icon")); break; case CHAT_SOURCE_UNKNOWN: - icon->setValue(LLSD("Unknown_Icon")); + icon->setValue(mIsFromScript ? LLSD("Inv_Script") : LLSD("Unknown_Icon")); } // In case the message came from an object, save the object info @@ -868,7 +871,7 @@ protected: LLMenuGL* menu = (LLMenuGL*)mPopupMenuHandleObject.get(); if (!menu) { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + ScopedRegistrarHelper registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar registrar_enable; registrar.add("ObjectIcon.Action", boost::bind(&LLChatHistoryHeader::onObjectIconContextMenuItemClicked, this, _2)); registrar_enable.add("ObjectIcon.Visible", boost::bind(&LLChatHistoryHeader::onObjectIconContextMenuItemVisible, this, _2)); @@ -896,9 +899,9 @@ protected: LLMenuGL* menu = (LLMenuGL*)mPopupMenuHandleAvatar.get(); if (!menu) { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + ScopedRegistrarHelper registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar registrar_enable; - registrar.add("AvatarIcon.Action", boost::bind(&LLChatHistoryHeader::onAvatarIconContextMenuItemClicked, this, _2)); + registrar.add("AvatarIcon.Action", boost::bind(&LLChatHistoryHeader::onAvatarIconContextMenuItemClicked, this, _2), cb_info::UNTRUSTED_BLOCK); registrar_enable.add("AvatarIcon.Check", boost::bind(&LLChatHistoryHeader::onAvatarIconContextMenuItemChecked, this, _2)); registrar_enable.add("AvatarIcon.Enable", boost::bind(&LLChatHistoryHeader::onAvatarIconContextMenuItemEnabled, this, _2)); registrar_enable.add("AvatarIcon.Visible", boost::bind(&LLChatHistoryHeader::onAvatarIconContextMenuItemVisible, this, _2)); @@ -1029,7 +1032,7 @@ private: mFrom = av_name.getDisplayName(); LLTextBox* user_name = getChild<LLTextBox>("user_name"); - user_name->setValue( LLSD(av_name.getDisplayName() ) ); + user_name->setValue(LLSD(mPrefix + av_name.getDisplayName())); user_name->setToolTip( av_name.getUserName() ); if (gSavedSettings.getBOOL("NameTagShowUsernames") && @@ -1071,6 +1074,9 @@ protected: bool mNeedsTimeBox; + bool mIsFromScript; + std::string mPrefix; + private: boost::signals2::connection mAvatarNameCacheConnection; }; @@ -1088,6 +1094,7 @@ LLChatHistory::LLChatHistory(const LLChatHistory::Params& p) mTopHeaderPad(p.top_header_pad), mBottomHeaderPad(p.bottom_header_pad), mIsLastMessageFromLog(false), + mIsLastFromScript(false), mNotifyAboutUnreadMsg(p.notify_unread_msg) { LLTextEditor::Params editor_params(p); @@ -1185,11 +1192,11 @@ LLView* LLChatHistory::getSeparator() return separator; } -LLView* LLChatHistory::getHeader(const LLChat& chat,const LLStyle::Params& style_params, const LLSD& args) +LLView* LLChatHistory::getHeader(const LLChat& chat,const LLStyle::Params& style_params, const LLSD& args, bool is_script) { LLChatHistoryHeader* header = LLChatHistoryHeader::createInstance(mMessageHeaderFilename); if (header) - header->setup(chat, style_params, args); + header->setup(chat, style_params, args, is_script); return header; } @@ -1260,8 +1267,8 @@ void LLChatHistory::appendMessage(const LLChat& chat, const LLSD &args, const LL name_params.color(name_color); name_params.readonly_color(name_color); - std::string prefix = chat.mText.substr(0, 4); - + auto [message, is_lua] = LLStringUtil::withoutPrefix(chat.mText, LUA_PREFIX); + std::string prefix = message.substr(0, 4); //IRC styled /me messages. bool irc_me = prefix == "/me " || prefix == "/me'"; @@ -1337,6 +1344,7 @@ void LLChatHistory::appendMessage(const LLChat& chat, const LLSD &args, const LL // names showing if (args["show_names_for_p2p_conv"].asBoolean() && utf8str_trim(chat.mFromName).size()) { + std::string script_prefix = is_lua ? LLTrans::getString("ScriptBy") : ""; // Don't hotlink any messages from the system (e.g. "Second Life:"), so just add those in plain text. if (chat.mSourceType == CHAT_SOURCE_OBJECT && chat.mFromID.notNull()) { @@ -1361,7 +1369,7 @@ void LLChatHistory::appendMessage(const LLChat& chat, const LLSD &args, const LL link_params.overwriteFrom(LLStyleMap::instance().lookupAgent(chat.mFromID)); // Add link to avatar's inspector and delimiter to message. - mEditor->appendText(std::string(link_params.link_href) + delimiter, + mEditor->appendText(script_prefix + std::string(link_params.link_href) + delimiter, prependNewLineState, link_params); prependNewLineState = false; } @@ -1374,7 +1382,7 @@ void LLChatHistory::appendMessage(const LLChat& chat, const LLSD &args, const LL } else { - mEditor->appendText("<nolink>" + chat.mFromName + "</nolink>" + delimiter, + mEditor->appendText(script_prefix + "<nolink>" + chat.mFromName + "</nolink>" + delimiter, prependNewLineState, body_message_params); prependNewLineState = false; } @@ -1395,7 +1403,8 @@ void LLChatHistory::appendMessage(const LLChat& chat, const LLSD &args, const LL && mLastFromID == chat.mFromID && mLastMessageTime.notNull() && (new_message_time.secondsSinceEpoch() - mLastMessageTime.secondsSinceEpoch()) < 60.0 - && mIsLastMessageFromLog == message_from_log) //distinguish between current and previous chat session's histories + && mIsLastMessageFromLog == message_from_log //distinguish between current and previous chat session's histories + && mIsLastFromScript == is_lua) { view = getSeparator(); if (!view) @@ -1410,7 +1419,7 @@ void LLChatHistory::appendMessage(const LLChat& chat, const LLSD &args, const LL } else { - view = getHeader(chat, name_params, args); + view = getHeader(chat, name_params, args, is_lua); if (!view) { LL_WARNS() << "Failed to create header from " << mMessageHeaderFilename << ": can't append to history" << LL_ENDL; @@ -1439,6 +1448,7 @@ void LLChatHistory::appendMessage(const LLChat& chat, const LLSD &args, const LL mLastFromID = chat.mFromID; mLastMessageTime = new_message_time; mIsLastMessageFromLog = message_from_log; + mIsLastFromScript = is_lua; } // body of the message processing @@ -1493,7 +1503,7 @@ void LLChatHistory::appendMessage(const LLChat& chat, const LLSD &args, const LL // usual messages showing else if (!teleport_separator) { - std::string message = irc_me ? chat.mText.substr(3) : chat.mText; + message = irc_me ? message.substr(3) : message; //MESSAGE TEXT PROCESSING //*HACK getting rid of redundant sender names in system notifications sent using sender name (see EXT-5010) diff --git a/indra/newview/llchathistory.h b/indra/newview/llchathistory.h index b8364ff860..8acbea6ed5 100644 --- a/indra/newview/llchathistory.h +++ b/indra/newview/llchathistory.h @@ -101,7 +101,7 @@ class LLChatHistory : public LLUICtrl * Builds a message header. * @return pointer to LLView header object. */ - LLView* getHeader(const LLChat& chat,const LLStyle::Params& style_params, const LLSD& args); + LLView* getHeader(const LLChat& chat,const LLStyle::Params& style_params, const LLSD& args, bool is_script = false); public: ~LLChatHistory(); LLSD getValue() const; @@ -127,6 +127,7 @@ class LLChatHistory : public LLUICtrl LLDate mLastMessageTime; bool mIsLastMessageFromLog; bool mNotifyAboutUnreadMsg; + bool mIsLastFromScript; //std::string mLastMessageTimeStr; std::string mMessageHeaderFilename; diff --git a/indra/newview/llchatitemscontainerctrl.cpp b/indra/newview/llchatitemscontainerctrl.cpp index 550dfeb802..f1ed55e647 100644 --- a/indra/newview/llchatitemscontainerctrl.cpp +++ b/indra/newview/llchatitemscontainerctrl.cpp @@ -188,6 +188,7 @@ void LLFloaterIMNearbyChatToastPanel::init(LLSD& notification) int sType = notification["source"].asInteger(); mSourceType = (EChatSourceType)sType; + mIsFromScript = notification["is_lua"].asBoolean(); std::string color_name = notification["text_color"].asString(); @@ -215,7 +216,7 @@ void LLFloaterIMNearbyChatToastPanel::init(LLSD& notification) { std::string str_sender; - str_sender = fromName; + str_sender = mIsFromScript ? LLTrans::getString("ScriptBy") + fromName : fromName; str_sender+=" "; @@ -397,7 +398,7 @@ void LLFloaterIMNearbyChatToastPanel::draw() else if(mSourceType == CHAT_SOURCE_SYSTEM) icon->setValue(LLSD("SL_Logo")); else if(mSourceType == CHAT_SOURCE_AGENT) - icon->setValue(mFromID); + icon->setValue(mIsFromScript ? LLSD("Inv_Script") : LLSD(mFromID)); else if(!mFromID.isNull()) icon->setValue(mFromID); } diff --git a/indra/newview/llchatitemscontainerctrl.h b/indra/newview/llchatitemscontainerctrl.h index 4ae73a0c43..54b757d217 100644 --- a/indra/newview/llchatitemscontainerctrl.h +++ b/indra/newview/llchatitemscontainerctrl.h @@ -48,7 +48,8 @@ protected: LLFloaterIMNearbyChatToastPanel() : mIsDirty(false), - mSourceType(CHAT_SOURCE_OBJECT) + mSourceType(CHAT_SOURCE_OBJECT), + mIsFromScript(false) {}; public: ~LLFloaterIMNearbyChatToastPanel(){} @@ -58,6 +59,8 @@ public: const LLUUID& getFromID() const { return mFromID;} const std::string& getFromName() const { return mFromName; } + bool isFromScript() { return mIsFromScript; } + //void addText (const std::string& message , const LLStyle::Params& input_params = LLStyle::Params()); //void setMessage (const LLChat& msg); void snapToMessageHeight (); @@ -88,6 +91,7 @@ private: std::string mFromName; EChatSourceType mSourceType; LLChatMsgBox* mMsgText; + bool mIsFromScript; diff --git a/indra/newview/llchiclet.cpp b/indra/newview/llchiclet.cpp index 4c0f160f6f..ec2b570ccd 100644 --- a/indra/newview/llchiclet.cpp +++ b/indra/newview/llchiclet.cpp @@ -210,9 +210,9 @@ void LLNotificationChiclet::createMenu() return; } - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + ScopedRegistrarHelper registrar; registrar.add("NotificationWellChicletMenu.Action", - boost::bind(&LLNotificationChiclet::onMenuItemClicked, this, _2)); + boost::bind(&LLNotificationChiclet::onMenuItemClicked, this, _2), cb_info::UNTRUSTED_BLOCK); LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; enable_registrar.add("NotificationWellChicletMenu.EnableItem", @@ -1132,8 +1132,8 @@ void LLScriptChiclet::createPopupMenu() if(!canCreateMenu()) return; - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; - registrar.add("ScriptChiclet.Action", boost::bind(&LLScriptChiclet::onMenuItemClicked, this, _2)); + ScopedRegistrarHelper registrar; + registrar.add("ScriptChiclet.Action", boost::bind(&LLScriptChiclet::onMenuItemClicked, this, _2), cb_info::UNTRUSTED_BLOCK); LLMenuGL* menu = LLUICtrlFactory::getInstance()->createFromFile<LLMenuGL> ("menu_script_chiclet.xml", gMenuHolder, LLViewerMenuHolderGL::child_registry_t::instance()); @@ -1215,8 +1215,8 @@ void LLInvOfferChiclet::createPopupMenu() if(!canCreateMenu()) return; - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; - registrar.add("InvOfferChiclet.Action", boost::bind(&LLInvOfferChiclet::onMenuItemClicked, this, _2)); + ScopedRegistrarHelper registrar; + registrar.add("InvOfferChiclet.Action", boost::bind(&LLInvOfferChiclet::onMenuItemClicked, this, _2), cb_info::UNTRUSTED_BLOCK); LLMenuGL* menu = LLUICtrlFactory::getInstance()->createFromFile<LLMenuGL> ("menu_inv_offer_chiclet.xml", gMenuHolder, LLViewerMenuHolderGL::child_registry_t::instance()); diff --git a/indra/newview/llcofwearables.cpp b/indra/newview/llcofwearables.cpp index 47803edc73..b9df091f54 100644 --- a/indra/newview/llcofwearables.cpp +++ b/indra/newview/llcofwearables.cpp @@ -138,7 +138,7 @@ protected: /*virtual*/ LLContextMenu* createMenu() { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + LLUICtrl::ScopedRegistrarHelper registrar; registrar.add("Attachment.Touch", boost::bind(handleMultiple, handle_attachment_touch, mUUIDs)); registrar.add("Attachment.Edit", boost::bind(handleMultiple, handle_item_edit, mUUIDs)); @@ -191,7 +191,7 @@ protected: /*virtual*/ LLContextMenu* createMenu() { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + LLUICtrl::ScopedRegistrarHelper registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; LLUUID selected_id = mUUIDs.back(); @@ -251,9 +251,9 @@ protected: LLUUID selected_id = mUUIDs.back(); LLPanelOutfitEdit* panel_oe = dynamic_cast<LLPanelOutfitEdit*>(LLFloaterSidePanelContainer::getPanel("appearance", "panel_outfit_edit")); - registrar.add("BodyPart.Replace", boost::bind(&LLPanelOutfitEdit::onReplaceMenuItemClicked, panel_oe, selected_id)); - registrar.add("BodyPart.Edit", boost::bind(LLAgentWearables::editWearable, selected_id)); - registrar.add("BodyPart.Create", boost::bind(&CofBodyPartContextMenu::createNew, this, selected_id)); + registrar.add("BodyPart.Replace", { boost::bind(&LLPanelOutfitEdit::onReplaceMenuItemClicked, panel_oe, selected_id) }); + registrar.add("BodyPart.Edit", { boost::bind(LLAgentWearables::editWearable, selected_id) }); + registrar.add("BodyPart.Create", { boost::bind(&CofBodyPartContextMenu::createNew, this, selected_id) }); enable_registrar.add("BodyPart.OnEnable", boost::bind(&CofBodyPartContextMenu::onEnable, this, _2)); diff --git a/indra/newview/llconversationloglist.cpp b/indra/newview/llconversationloglist.cpp index 65863f0a5e..de15fba201 100644 --- a/indra/newview/llconversationloglist.cpp +++ b/indra/newview/llconversationloglist.cpp @@ -47,11 +47,11 @@ LLConversationLogList::LLConversationLogList(const Params& p) LLConversationLog::instance().addObserver(this); // Set up context menu. - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + ScopedRegistrarHelper registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar check_registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; - registrar.add ("Calllog.Action", boost::bind(&LLConversationLogList::onCustomAction, this, _2)); + registrar.add ("Calllog.Action", boost::bind(&LLConversationLogList::onCustomAction, this, _2), cb_info::UNTRUSTED_BLOCK); check_registrar.add ("Calllog.Check", boost::bind(&LLConversationLogList::isActionChecked,this, _2)); enable_registrar.add("Calllog.Enable", boost::bind(&LLConversationLogList::isActionEnabled,this, _2)); diff --git a/indra/newview/lldonotdisturbnotificationstorage.h b/indra/newview/lldonotdisturbnotificationstorage.h index 6683646a9b..0dc2515e02 100644 --- a/indra/newview/lldonotdisturbnotificationstorage.h +++ b/indra/newview/lldonotdisturbnotificationstorage.h @@ -42,7 +42,7 @@ public: ~LLDoNotDisturbNotificationStorageTimer(); public: - bool tick(); + bool tick() override; }; class LLDoNotDisturbNotificationStorage : public LLParamSingleton<LLDoNotDisturbNotificationStorage>, public LLNotificationStorage diff --git a/indra/newview/llfavoritesbar.cpp b/indra/newview/llfavoritesbar.cpp index 377710c170..49fd6a29a2 100644 --- a/indra/newview/llfavoritesbar.cpp +++ b/indra/newview/llfavoritesbar.cpp @@ -430,7 +430,7 @@ LLFavoritesBarCtrl::LLFavoritesBarCtrl(const LLFavoritesBarCtrl::Params& p) { // Register callback for menus with current registrar (will be parent panel's registrar) LLUICtrl::CommitCallbackRegistry::currentRegistrar().add("Favorites.DoToSelected", - boost::bind(&LLFavoritesBarCtrl::doToSelected, this, _2)); + { boost::bind(&LLFavoritesBarCtrl::doToSelected, this, _2), cb_info::UNTRUSTED_BLOCK }); // Add this if we need to selectively enable items LLUICtrl::EnableCallbackRegistry::currentRegistrar().add("Favorites.EnableSelected", diff --git a/indra/newview/llfilepicker.cpp b/indra/newview/llfilepicker.cpp index 0afb275d13..d5e3627d8e 100644 --- a/indra/newview/llfilepicker.cpp +++ b/indra/newview/llfilepicker.cpp @@ -65,6 +65,7 @@ LLFilePicker LLFilePicker::sInstance; #define MATERIAL_TEXTURES_FILTER L"GLTF Import (*.gltf; *.glb; *.tga; *.bmp; *.jpg; *.jpeg; *.png)\0*.gltf;*.glb;*.tga;*.bmp;*.jpg;*.jpeg;*.png\0" #define SCRIPT_FILTER L"Script files (*.lsl)\0*.lsl\0" #define DICTIONARY_FILTER L"Dictionary files (*.dic; *.xcu)\0*.dic;*.xcu\0" +#define LUA_FILTER L"Script files (*.lua)\0*.lua\0" #endif #ifdef LL_DARWIN @@ -241,6 +242,10 @@ bool LLFilePicker::setupFilter(ELoadFilter filter) mOFN.lpstrFilter = DICTIONARY_FILTER \ L"\0"; break; + case FFLOAD_LUA: + mOFN.lpstrFilter = LUA_FILTER \ + L"\0"; + break; default: res = false; break; diff --git a/indra/newview/llfilepicker.h b/indra/newview/llfilepicker.h index 75ff14f4cf..09f785b921 100644 --- a/indra/newview/llfilepicker.h +++ b/indra/newview/llfilepicker.h @@ -90,6 +90,7 @@ public: FFLOAD_MATERIAL = 15, FFLOAD_MATERIAL_TEXTURE = 16, FFLOAD_HDRI = 17, + FFLOAD_LUA = 18, }; enum ESaveFilter diff --git a/indra/newview/llfloaterauction.cpp b/indra/newview/llfloaterauction.cpp index a87ddfd76e..4608d3913b 100644 --- a/indra/newview/llfloaterauction.cpp +++ b/indra/newview/llfloaterauction.cpp @@ -75,10 +75,10 @@ LLFloaterAuction::LLFloaterAuction(const LLSD& key) : LLFloater(key), mParcelID(-1) { - mCommitCallbackRegistrar.add("ClickSnapshot", boost::bind(&LLFloaterAuction::onClickSnapshot, this)); - mCommitCallbackRegistrar.add("ClickSellToAnyone", boost::bind(&LLFloaterAuction::onClickSellToAnyone, this)); - mCommitCallbackRegistrar.add("ClickStartAuction", boost::bind(&LLFloaterAuction::onClickStartAuction, this)); - mCommitCallbackRegistrar.add("ClickResetParcel", boost::bind(&LLFloaterAuction::onClickResetParcel, this)); + mCommitCallbackRegistrar.add("ClickSnapshot", { boost::bind(&LLFloaterAuction::onClickSnapshot, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("ClickSellToAnyone", { boost::bind(&LLFloaterAuction::onClickSellToAnyone, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("ClickStartAuction", { boost::bind(&LLFloaterAuction::onClickStartAuction, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("ClickResetParcel", { boost::bind(&LLFloaterAuction::onClickResetParcel, this), cb_info::UNTRUSTED_BLOCK }); } // Destroys the object diff --git a/indra/newview/llfloateravatarpicker.cpp b/indra/newview/llfloateravatarpicker.cpp index 08a54b7369..196c9e8818 100644 --- a/indra/newview/llfloateravatarpicker.cpp +++ b/indra/newview/llfloateravatarpicker.cpp @@ -111,7 +111,7 @@ LLFloaterAvatarPicker::LLFloaterAvatarPicker(const LLSD& key) mContextConeOutAlpha(CONTEXT_CONE_OUT_ALPHA), mContextConeFadeTime(CONTEXT_CONE_FADE_TIME) { - mCommitCallbackRegistrar.add("Refresh.FriendList", boost::bind(&LLFloaterAvatarPicker::populateFriend, this)); + mCommitCallbackRegistrar.add("Refresh.FriendList", { boost::bind(&LLFloaterAvatarPicker::populateFriend, this), cb_info::UNTRUSTED_THROTTLE }); } bool LLFloaterAvatarPicker::postBuild() diff --git a/indra/newview/llfloateravatarrendersettings.cpp b/indra/newview/llfloateravatarrendersettings.cpp index 9101e6eb29..16e2903324 100644 --- a/indra/newview/llfloateravatarrendersettings.cpp +++ b/indra/newview/llfloateravatarrendersettings.cpp @@ -51,7 +51,7 @@ protected: { LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; - registrar.add("Settings.SetRendering", boost::bind(&LLFloaterAvatarRenderSettings::onCustomAction, mFloaterSettings, _2, mUUIDs.front())); + registrar.add("Settings.SetRendering", { boost::bind(&LLFloaterAvatarRenderSettings::onCustomAction, mFloaterSettings, _2, mUUIDs.front()) }); enable_registrar.add("Settings.IsSelected", boost::bind(&LLFloaterAvatarRenderSettings::isActionChecked, mFloaterSettings, _2, mUUIDs.front())); LLContextMenu* menu = createFromFile("menu_avatar_rendering_settings.xml"); @@ -75,7 +75,7 @@ LLFloaterAvatarRenderSettings::LLFloaterAvatarRenderSettings(const LLSD& key) { mContextMenu = new LLSettingsContextMenu(this); LLRenderMuteList::getInstance()->addObserver(&sAvatarRenderMuteListObserver); - mCommitCallbackRegistrar.add("Settings.AddNewEntry", boost::bind(&LLFloaterAvatarRenderSettings::onClickAdd, this, _2)); + mCommitCallbackRegistrar.add("Settings.AddNewEntry", {boost::bind(&LLFloaterAvatarRenderSettings::onClickAdd, this, _2)}); } LLFloaterAvatarRenderSettings::~LLFloaterAvatarRenderSettings() diff --git a/indra/newview/llfloaterbeacons.cpp b/indra/newview/llfloaterbeacons.cpp index 11c1c2a9f2..1b975f3553 100644 --- a/indra/newview/llfloaterbeacons.cpp +++ b/indra/newview/llfloaterbeacons.cpp @@ -49,7 +49,7 @@ LLFloaterBeacons::LLFloaterBeacons(const LLSD& seed) LLPipeline::setRenderHighlights( gSavedSettings.getBOOL("renderhighlights")); LLPipeline::setRenderBeacons( gSavedSettings.getBOOL("renderbeacons")); LLPipeline::setRenderMOAPBeacons( gSavedSettings.getBOOL("moapbeacon")); - mCommitCallbackRegistrar.add("Beacons.UICheck", boost::bind(&LLFloaterBeacons::onClickUICheck, this,_1)); + mCommitCallbackRegistrar.add("Beacons.UICheck", { boost::bind(&LLFloaterBeacons::onClickUICheck, this,_1) }); } bool LLFloaterBeacons::postBuild() diff --git a/indra/newview/llfloaterbulkpermission.cpp b/indra/newview/llfloaterbulkpermission.cpp index c09c02d32b..1d670bfb8c 100644 --- a/indra/newview/llfloaterbulkpermission.cpp +++ b/indra/newview/llfloaterbulkpermission.cpp @@ -56,12 +56,12 @@ LLFloaterBulkPermission::LLFloaterBulkPermission(const LLSD& seed) mDone(false) { mID.generate(); - mCommitCallbackRegistrar.add("BulkPermission.Ok", boost::bind(&LLFloaterBulkPermission::onOkBtn, this)); - mCommitCallbackRegistrar.add("BulkPermission.Apply", boost::bind(&LLFloaterBulkPermission::onApplyBtn, this)); - mCommitCallbackRegistrar.add("BulkPermission.Close", boost::bind(&LLFloaterBulkPermission::onCloseBtn, this)); - mCommitCallbackRegistrar.add("BulkPermission.CheckAll", boost::bind(&LLFloaterBulkPermission::onCheckAll, this)); - mCommitCallbackRegistrar.add("BulkPermission.UncheckAll", boost::bind(&LLFloaterBulkPermission::onUncheckAll, this)); - mCommitCallbackRegistrar.add("BulkPermission.CommitCopy", boost::bind(&LLFloaterBulkPermission::onCommitCopy, this)); + mCommitCallbackRegistrar.add("BulkPermission.Ok", { boost::bind(&LLFloaterBulkPermission::onOkBtn, this) }); + mCommitCallbackRegistrar.add("BulkPermission.Apply", { boost::bind(&LLFloaterBulkPermission::onApplyBtn, this) }); + mCommitCallbackRegistrar.add("BulkPermission.Close", { boost::bind(&LLFloaterBulkPermission::onCloseBtn, this) }); + mCommitCallbackRegistrar.add("BulkPermission.CheckAll", { boost::bind(&LLFloaterBulkPermission::onCheckAll, this) }); + mCommitCallbackRegistrar.add("BulkPermission.UncheckAll", { boost::bind(&LLFloaterBulkPermission::onUncheckAll, this) }); + mCommitCallbackRegistrar.add("BulkPermission.CommitCopy", { boost::bind(&LLFloaterBulkPermission::onCommitCopy, this) }); } bool LLFloaterBulkPermission::postBuild() diff --git a/indra/newview/llfloaterbump.cpp b/indra/newview/llfloaterbump.cpp index 162ad5e108..d56e6cdf20 100644 --- a/indra/newview/llfloaterbump.cpp +++ b/indra/newview/llfloaterbump.cpp @@ -51,18 +51,18 @@ LLFloaterBump::LLFloaterBump(const LLSD& key) : LLFloater(key) { - mCommitCallbackRegistrar.add("Avatar.SendIM", boost::bind(&LLFloaterBump::startIM, this)); - mCommitCallbackRegistrar.add("Avatar.ReportAbuse", boost::bind(&LLFloaterBump::reportAbuse, this)); - mCommitCallbackRegistrar.add("ShowAgentProfile", boost::bind(&LLFloaterBump::showProfile, this)); - mCommitCallbackRegistrar.add("Avatar.InviteToGroup", boost::bind(&LLFloaterBump::inviteToGroup, this)); - mCommitCallbackRegistrar.add("Avatar.Call", boost::bind(&LLFloaterBump::startCall, this)); + mCommitCallbackRegistrar.add("Avatar.SendIM", { boost::bind(&LLFloaterBump::startIM, this), cb_info::UNTRUSTED_THROTTLE }); + mCommitCallbackRegistrar.add("Avatar.ReportAbuse", { boost::bind(&LLFloaterBump::reportAbuse, this), cb_info::UNTRUSTED_THROTTLE }); + mCommitCallbackRegistrar.add("ShowAgentProfile", { boost::bind(&LLFloaterBump::showProfile, this), cb_info::UNTRUSTED_THROTTLE }); + mCommitCallbackRegistrar.add("Avatar.InviteToGroup", { boost::bind(&LLFloaterBump::inviteToGroup, this), cb_info::UNTRUSTED_THROTTLE }); + mCommitCallbackRegistrar.add("Avatar.Call", { boost::bind(&LLFloaterBump::startCall, this), cb_info::UNTRUSTED_BLOCK }); mEnableCallbackRegistrar.add("Avatar.EnableCall", boost::bind(&LLAvatarActions::canCall)); - mCommitCallbackRegistrar.add("Avatar.AddFriend", boost::bind(&LLFloaterBump::addFriend, this)); + mCommitCallbackRegistrar.add("Avatar.AddFriend", { boost::bind(&LLFloaterBump::addFriend, this), cb_info::UNTRUSTED_THROTTLE }); mEnableCallbackRegistrar.add("Avatar.EnableAddFriend", boost::bind(&LLFloaterBump::enableAddFriend, this)); - mCommitCallbackRegistrar.add("Avatar.Mute", boost::bind(&LLFloaterBump::muteAvatar, this)); + mCommitCallbackRegistrar.add("Avatar.Mute", { boost::bind(&LLFloaterBump::muteAvatar, this), cb_info::UNTRUSTED_BLOCK }); mEnableCallbackRegistrar.add("Avatar.EnableMute", boost::bind(&LLFloaterBump::enableMute, this)); - mCommitCallbackRegistrar.add("PayObject", boost::bind(&LLFloaterBump::payAvatar, this)); - mCommitCallbackRegistrar.add("Tools.LookAtSelection", boost::bind(&LLFloaterBump::zoomInAvatar, this)); + mCommitCallbackRegistrar.add("PayObject", { boost::bind(&LLFloaterBump::payAvatar, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Tools.LookAtSelection", { boost::bind(&LLFloaterBump::zoomInAvatar, this) }); } diff --git a/indra/newview/llfloatercamera.cpp b/indra/newview/llfloatercamera.cpp index 4a5a755696..2322103009 100644 --- a/indra/newview/llfloatercamera.cpp +++ b/indra/newview/llfloatercamera.cpp @@ -169,11 +169,11 @@ static LLPanelInjector<LLPanelCameraZoom> t_camera_zoom_panel("camera_zoom_panel void LLPanelCameraZoom::onCreate() { - mCommitCallbackRegistrar.add("Zoom.minus", boost::bind(&LLPanelCameraZoom::onZoomMinusHeldDown, this)); - mCommitCallbackRegistrar.add("Zoom.plus", boost::bind(&LLPanelCameraZoom::onZoomPlusHeldDown, this)); - mCommitCallbackRegistrar.add("Slider.value_changed", boost::bind(&LLPanelCameraZoom::onSliderValueChanged, this)); - mCommitCallbackRegistrar.add("Camera.track", boost::bind(&LLPanelCameraZoom::onCameraTrack, this)); - mCommitCallbackRegistrar.add("Camera.rotate", boost::bind(&LLPanelCameraZoom::onCameraRotate, this)); + mCommitCallbackRegistrar.add("Zoom.minus", { boost::bind(&LLPanelCameraZoom::onZoomMinusHeldDown, this) }); + mCommitCallbackRegistrar.add("Zoom.plus", { boost::bind(&LLPanelCameraZoom::onZoomPlusHeldDown, this) }); + mCommitCallbackRegistrar.add("Slider.value_changed", { boost::bind(&LLPanelCameraZoom::onSliderValueChanged, this) }); + mCommitCallbackRegistrar.add("Camera.track", { boost::bind(&LLPanelCameraZoom::onCameraTrack, this) }); + mCommitCallbackRegistrar.add("Camera.rotate", { boost::bind(&LLPanelCameraZoom::onCameraRotate, this) }); } bool LLPanelCameraZoom::postBuild() @@ -461,9 +461,9 @@ LLFloaterCamera::LLFloaterCamera(const LLSD& val) mPrevMode(CAMERA_CTRL_MODE_PAN) { LLHints::getInstance()->registerHintTarget("view_popup", getHandle()); - mCommitCallbackRegistrar.add("CameraPresets.ChangeView", boost::bind(&LLFloaterCamera::onClickCameraItem, _2)); - mCommitCallbackRegistrar.add("CameraPresets.Save", boost::bind(&LLFloaterCamera::onSavePreset, this)); - mCommitCallbackRegistrar.add("CameraPresets.ShowPresetsList", boost::bind(&LLFloaterReg::showInstance, "camera_presets", LLSD(), false)); + mCommitCallbackRegistrar.add("CameraPresets.ChangeView", {boost::bind(&LLFloaterCamera::onClickCameraItem, _2)}); + mCommitCallbackRegistrar.add("CameraPresets.Save", {boost::bind(&LLFloaterCamera::onSavePreset, this)}); + mCommitCallbackRegistrar.add("CameraPresets.ShowPresetsList", {boost::bind(&LLFloaterReg::showInstance, "camera_presets", LLSD(), false)}); } // virtual diff --git a/indra/newview/llfloatercamerapresets.cpp b/indra/newview/llfloatercamerapresets.cpp index b033af2564..d527541b76 100644 --- a/indra/newview/llfloatercamerapresets.cpp +++ b/indra/newview/llfloatercamerapresets.cpp @@ -90,8 +90,8 @@ LLCameraPresetFlatItem::LLCameraPresetFlatItem(const std::string &preset_name, b mPresetName(preset_name), mIsDefaultPrest(is_default) { - mCommitCallbackRegistrar.add("CameraPresets.Delete", boost::bind(&LLCameraPresetFlatItem::onDeleteBtnClick, this)); - mCommitCallbackRegistrar.add("CameraPresets.Reset", boost::bind(&LLCameraPresetFlatItem::onResetBtnClick, this)); + mCommitCallbackRegistrar.add("CameraPresets.Delete", { boost::bind(&LLCameraPresetFlatItem::onDeleteBtnClick, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("CameraPresets.Reset", { boost::bind(&LLCameraPresetFlatItem::onResetBtnClick, this), cb_info::UNTRUSTED_BLOCK }); buildFromFile("panel_camera_preset_item.xml"); } diff --git a/indra/newview/llfloaterconversationlog.cpp b/indra/newview/llfloaterconversationlog.cpp index 97399c9cf7..dc0f4ae555 100644 --- a/indra/newview/llfloaterconversationlog.cpp +++ b/indra/newview/llfloaterconversationlog.cpp @@ -35,7 +35,7 @@ LLFloaterConversationLog::LLFloaterConversationLog(const LLSD& key) : LLFloater(key), mConversationLogList(NULL) { - mCommitCallbackRegistrar.add("CallLog.Action", boost::bind(&LLFloaterConversationLog::onCustomAction, this, _2)); + mCommitCallbackRegistrar.add("CallLog.Action", { boost::bind(&LLFloaterConversationLog::onCustomAction, this, _2) }); mEnableCallbackRegistrar.add("CallLog.Check", boost::bind(&LLFloaterConversationLog::isActionChecked, this, _2)); } diff --git a/indra/newview/llfloatereditextdaycycle.cpp b/indra/newview/llfloatereditextdaycycle.cpp index fd58cd8aaf..626f06ce7e 100644 --- a/indra/newview/llfloatereditextdaycycle.cpp +++ b/indra/newview/llfloatereditextdaycycle.cpp @@ -183,8 +183,8 @@ LLFloaterEditExtDayCycle::LLFloaterEditExtDayCycle(const LLSD &key) : mLoadTrack(nullptr), mClearTrack(nullptr) { - mCommitCallbackRegistrar.add(EVNT_DAYTRACK, [this](LLUICtrl *ctrl, const LLSD &data) { onTrackSelectionCallback(data); }); - mCommitCallbackRegistrar.add(EVNT_PLAY, [this](LLUICtrl *ctrl, const LLSD &data) { onPlayActionCallback(data); }); + mCommitCallbackRegistrar.add(EVNT_DAYTRACK, { [this](LLUICtrl *ctrl, const LLSD &data) { onTrackSelectionCallback(data); }}); + mCommitCallbackRegistrar.add(EVNT_PLAY, { [this](LLUICtrl *ctrl, const LLSD &data) { onPlayActionCallback(data); }}); mScratchSky = LLSettingsVOSky::buildDefaultSky(); mScratchWater = LLSettingsVOWater::buildDefaultWater(); diff --git a/indra/newview/llfloatergesture.cpp b/indra/newview/llfloatergesture.cpp index 936096d8fe..0ff958c19b 100644 --- a/indra/newview/llfloatergesture.cpp +++ b/indra/newview/llfloatergesture.cpp @@ -122,11 +122,11 @@ LLFloaterGesture::LLFloaterGesture(const LLSD& key) mObserver = new LLFloaterGestureObserver(this); LLGestureMgr::instance().addObserver(mObserver); - mCommitCallbackRegistrar.add("Gesture.Action.ToggleActiveState", boost::bind(&LLFloaterGesture::onActivateBtnClick, this)); - mCommitCallbackRegistrar.add("Gesture.Action.ShowPreview", boost::bind(&LLFloaterGesture::onClickEdit, this)); - mCommitCallbackRegistrar.add("Gesture.Action.CopyPaste", boost::bind(&LLFloaterGesture::onCopyPasteAction, this, _2)); - mCommitCallbackRegistrar.add("Gesture.Action.SaveToCOF", boost::bind(&LLFloaterGesture::addToCurrentOutFit, this)); - mCommitCallbackRegistrar.add("Gesture.Action.Rename", boost::bind(&LLFloaterGesture::onRenameSelected, this)); + mCommitCallbackRegistrar.add("Gesture.Action.ToggleActiveState", { boost::bind(&LLFloaterGesture::onActivateBtnClick, this) }); + mCommitCallbackRegistrar.add("Gesture.Action.ShowPreview", { boost::bind(&LLFloaterGesture::onClickEdit, this) }); + mCommitCallbackRegistrar.add("Gesture.Action.CopyPaste", { boost::bind(&LLFloaterGesture::onCopyPasteAction, this, _2) }); + mCommitCallbackRegistrar.add("Gesture.Action.SaveToCOF", { boost::bind(&LLFloaterGesture::addToCurrentOutFit, this) }); + mCommitCallbackRegistrar.add("Gesture.Action.Rename", { boost::bind(&LLFloaterGesture::onRenameSelected, this) }); mEnableCallbackRegistrar.add("Gesture.EnableAction", boost::bind(&LLFloaterGesture::isActionEnabled, this, _2)); } diff --git a/indra/newview/llfloatergltfasseteditor.cpp b/indra/newview/llfloatergltfasseteditor.cpp index d2cf24f1dd..f28c01d713 100644 --- a/indra/newview/llfloatergltfasseteditor.cpp +++ b/indra/newview/llfloatergltfasseteditor.cpp @@ -46,8 +46,8 @@ LLFloaterGLTFAssetEditor::LLFloaterGLTFAssetEditor(const LLSD& key) { setTitle("GLTF Asset Editor (WIP)"); - mCommitCallbackRegistrar.add("PanelObject.menuDoToSelected", [this](LLUICtrl* ctrl, const LLSD& data) { onMenuDoToSelected(data); }); - mEnableCallbackRegistrar.add("PanelObject.menuEnable", [this](LLUICtrl* ctrl, const LLSD& data) { return onMenuEnableItem(data); }); + mCommitCallbackRegistrar.add("PanelObject.menuDoToSelected", { [this](LLUICtrl* ctrl, const LLSD& data) { onMenuDoToSelected(data); }}); + mEnableCallbackRegistrar.add("PanelObject.menuEnable", { [this](LLUICtrl* ctrl, const LLSD& data) { return onMenuEnableItem(data); }}); } LLFloaterGLTFAssetEditor::~LLFloaterGLTFAssetEditor() diff --git a/indra/newview/llfloatergodtools.cpp b/indra/newview/llfloatergodtools.cpp index 6c1d5b5cca..f2884c3a6e 100644 --- a/indra/newview/llfloatergodtools.cpp +++ b/indra/newview/llfloatergodtools.cpp @@ -431,15 +431,15 @@ const F32 PRICE_PER_METER_DEFAULT = 1.f; LLPanelRegionTools::LLPanelRegionTools() : LLPanel() { - mCommitCallbackRegistrar.add("RegionTools.ChangeAnything", boost::bind(&LLPanelRegionTools::onChangeAnything, this)); - mCommitCallbackRegistrar.add("RegionTools.ChangePrelude", boost::bind(&LLPanelRegionTools::onChangePrelude, this)); - mCommitCallbackRegistrar.add("RegionTools.BakeTerrain", boost::bind(&LLPanelRegionTools::onBakeTerrain, this)); - mCommitCallbackRegistrar.add("RegionTools.RevertTerrain", boost::bind(&LLPanelRegionTools::onRevertTerrain, this)); - mCommitCallbackRegistrar.add("RegionTools.SwapTerrain", boost::bind(&LLPanelRegionTools::onSwapTerrain, this)); - mCommitCallbackRegistrar.add("RegionTools.Refresh", boost::bind(&LLPanelRegionTools::onRefresh, this)); - mCommitCallbackRegistrar.add("RegionTools.ApplyChanges", boost::bind(&LLPanelRegionTools::onApplyChanges, this)); - mCommitCallbackRegistrar.add("RegionTools.SelectRegion", boost::bind(&LLPanelRegionTools::onSelectRegion, this)); - mCommitCallbackRegistrar.add("RegionTools.SaveState", boost::bind(&LLPanelRegionTools::onSaveState, this)); + mCommitCallbackRegistrar.add("RegionTools.ChangeAnything", { boost::bind(&LLPanelRegionTools::onChangeAnything, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("RegionTools.ChangePrelude", { boost::bind(&LLPanelRegionTools::onChangePrelude, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("RegionTools.BakeTerrain", { boost::bind(&LLPanelRegionTools::onBakeTerrain, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("RegionTools.RevertTerrain", { boost::bind(&LLPanelRegionTools::onRevertTerrain, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("RegionTools.SwapTerrain", { boost::bind(&LLPanelRegionTools::onSwapTerrain, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("RegionTools.Refresh", { boost::bind(&LLPanelRegionTools::onRefresh, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("RegionTools.ApplyChanges", { boost::bind(&LLPanelRegionTools::onApplyChanges, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("RegionTools.SelectRegion", { boost::bind(&LLPanelRegionTools::onSelectRegion, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("RegionTools.SaveState", { boost::bind(&LLPanelRegionTools::onSaveState, this), cb_info::UNTRUSTED_BLOCK }); } bool LLPanelRegionTools::postBuild() @@ -854,7 +854,8 @@ void LLPanelRegionTools::onSelectRegion() LLPanelGridTools::LLPanelGridTools() : LLPanel() { - mCommitCallbackRegistrar.add("GridTools.FlushMapVisibilityCaches", boost::bind(&LLPanelGridTools::onClickFlushMapVisibilityCaches, this)); + mCommitCallbackRegistrar.add("GridTools.FlushMapVisibilityCaches", + { boost::bind(&LLPanelGridTools::onClickFlushMapVisibilityCaches, this), cb_info::UNTRUSTED_BLOCK }); } // Destroys the object @@ -929,15 +930,15 @@ LLPanelObjectTools::LLPanelObjectTools() : LLPanel(), mTargetAvatar() { - mCommitCallbackRegistrar.add("ObjectTools.ChangeAnything", boost::bind(&LLPanelObjectTools::onChangeAnything, this)); - mCommitCallbackRegistrar.add("ObjectTools.DeletePublicOwnedBy", boost::bind(&LLPanelObjectTools::onClickDeletePublicOwnedBy, this)); - mCommitCallbackRegistrar.add("ObjectTools.DeleteAllScriptedOwnedBy", boost::bind(&LLPanelObjectTools::onClickDeleteAllScriptedOwnedBy, this)); - mCommitCallbackRegistrar.add("ObjectTools.DeleteAllOwnedBy", boost::bind(&LLPanelObjectTools::onClickDeleteAllOwnedBy, this)); - mCommitCallbackRegistrar.add("ObjectTools.ApplyChanges", boost::bind(&LLPanelObjectTools::onApplyChanges, this)); - mCommitCallbackRegistrar.add("ObjectTools.Set", boost::bind(&LLPanelObjectTools::onClickSet, this)); - mCommitCallbackRegistrar.add("ObjectTools.GetTopColliders", boost::bind(&LLPanelObjectTools::onGetTopColliders, this)); - mCommitCallbackRegistrar.add("ObjectTools.GetTopScripts", boost::bind(&LLPanelObjectTools::onGetTopScripts, this)); - mCommitCallbackRegistrar.add("ObjectTools.GetScriptDigest", boost::bind(&LLPanelObjectTools::onGetScriptDigest, this)); + mCommitCallbackRegistrar.add("ObjectTools.ChangeAnything", { boost::bind(&LLPanelObjectTools::onChangeAnything, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("ObjectTools.DeletePublicOwnedBy", { boost::bind(&LLPanelObjectTools::onClickDeletePublicOwnedBy, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("ObjectTools.DeleteAllScriptedOwnedBy", { boost::bind(&LLPanelObjectTools::onClickDeleteAllScriptedOwnedBy, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("ObjectTools.DeleteAllOwnedBy", { boost::bind(&LLPanelObjectTools::onClickDeleteAllOwnedBy, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("ObjectTools.ApplyChanges", { boost::bind(&LLPanelObjectTools::onApplyChanges, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("ObjectTools.Set", { boost::bind(&LLPanelObjectTools::onClickSet, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("ObjectTools.GetTopColliders", { boost::bind(&LLPanelObjectTools::onGetTopColliders, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("ObjectTools.GetTopScripts", { boost::bind(&LLPanelObjectTools::onGetTopScripts, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("ObjectTools.GetScriptDigest", { boost::bind(&LLPanelObjectTools::onGetScriptDigest, this), cb_info::UNTRUSTED_BLOCK }); } // Destroys the object @@ -1224,7 +1225,7 @@ const std::string AGENT_REGION = "Agent Region"; LLPanelRequestTools::LLPanelRequestTools(): LLPanel() { - mCommitCallbackRegistrar.add("GodTools.Request", boost::bind(&LLPanelRequestTools::onClickRequest, this)); + mCommitCallbackRegistrar.add("GodTools.Request", {boost::bind(&LLPanelRequestTools::onClickRequest, this), cb_info::UNTRUSTED_BLOCK }); } LLPanelRequestTools::~LLPanelRequestTools() diff --git a/indra/newview/llfloaterimcontainer.cpp b/indra/newview/llfloaterimcontainer.cpp index e55bf50724..1e2d790cfc 100644 --- a/indra/newview/llfloaterimcontainer.cpp +++ b/indra/newview/llfloaterimcontainer.cpp @@ -76,14 +76,14 @@ LLFloaterIMContainer::LLFloaterIMContainer(const LLSD& seed, const Params& param mConversationEventQueue() { mEnableCallbackRegistrar.add("IMFloaterContainer.Check", boost::bind(&LLFloaterIMContainer::isActionChecked, this, _2)); - mCommitCallbackRegistrar.add("IMFloaterContainer.Action", boost::bind(&LLFloaterIMContainer::onCustomAction, this, _2)); + mCommitCallbackRegistrar.add("IMFloaterContainer.Action", { boost::bind(&LLFloaterIMContainer::onCustomAction, this, _2) }); mEnableCallbackRegistrar.add("Avatar.CheckItem", boost::bind(&LLFloaterIMContainer::checkContextMenuItem, this, _2)); mEnableCallbackRegistrar.add("Avatar.EnableItem", boost::bind(&LLFloaterIMContainer::enableContextMenuItem, this, _2)); mEnableCallbackRegistrar.add("Avatar.VisibleItem", boost::bind(&LLFloaterIMContainer::visibleContextMenuItem, this, _2)); - mCommitCallbackRegistrar.add("Avatar.DoToSelected", boost::bind(&LLFloaterIMContainer::doToSelected, this, _2)); + mCommitCallbackRegistrar.add("Avatar.DoToSelected", { boost::bind(&LLFloaterIMContainer::doToSelected, this, _2) }); - mCommitCallbackRegistrar.add("Group.DoToSelected", boost::bind(&LLFloaterIMContainer::doToSelectedGroup, this, _2)); + mCommitCallbackRegistrar.add("Group.DoToSelected", { boost::bind(&LLFloaterIMContainer::doToSelectedGroup, this, _2) }); // Firstly add our self to IMSession observers, so we catch session events LLIMMgr::getInstance()->addSessionObserver(this); diff --git a/indra/newview/llfloaterimnearbychat.cpp b/indra/newview/llfloaterimnearbychat.cpp index 28c651f0cd..6c3e8391cd 100644 --- a/indra/newview/llfloaterimnearbychat.cpp +++ b/indra/newview/llfloaterimnearbychat.cpp @@ -52,6 +52,7 @@ #include "llfirstuse.h" #include "llfloaterimnearbychat.h" +#include "llfloaterimnearbychatlistener.h" #include "llagent.h" // gAgent #include "llgesturemgr.h" #include "llmultigesture.h" @@ -71,6 +72,8 @@ S32 LLFloaterIMNearbyChat::sLastSpecialChatChannel = 0; +static LLFloaterIMNearbyChatListener sChatListener; + constexpr S32 EXPANDED_HEIGHT = 266; constexpr S32 COLLAPSED_HEIGHT = 60; constexpr S32 EXPANDED_MIN_HEIGHT = 150; @@ -106,7 +109,7 @@ LLFloaterIMNearbyChat::LLFloaterIMNearbyChat(const LLSD& llsd) // Required by LLFloaterIMSessionTab::mGearBtn // But nearby floater has no 'per agent' menu items, mEnableCallbackRegistrar.add("Avatar.EnableGearItem", boost::bind(&cb_do_nothing)); - mCommitCallbackRegistrar.add("Avatar.GearDoToSelected", boost::bind(&cb_do_nothing)); + mCommitCallbackRegistrar.add("Avatar.GearDoToSelected", { boost::bind(&cb_do_nothing) }); mEnableCallbackRegistrar.add("Avatar.CheckGearItem", boost::bind(&cb_do_nothing)); mMinFloaterHeight = EXPANDED_MIN_HEIGHT; diff --git a/indra/newview/llfloaterimnearbychathandler.cpp b/indra/newview/llfloaterimnearbychathandler.cpp index c920a3c898..20bf896562 100644 --- a/indra/newview/llfloaterimnearbychathandler.cpp +++ b/indra/newview/llfloaterimnearbychathandler.cpp @@ -300,12 +300,13 @@ void LLFloaterIMNearbyChatScreenChannel::addChat(LLSD& chat) { LLUUID fromID = chat["from_id"].asUUID(); // agent id or object id std::string from = chat["from"].asString(); + bool is_lua = chat["is_lua"].asBoolean(); LLToast* toast = m_active_toasts[0].get(); if (toast) { LLFloaterIMNearbyChatToastPanel* panel = dynamic_cast<LLFloaterIMNearbyChatToastPanel*>(toast->getPanel()); - if (panel && panel->messageID() == fromID && panel->getFromName() == from && panel->canAddText()) + if (panel && panel->messageID() == fromID && panel->getFromName() == from && panel->isFromScript() == is_lua && panel->canAddText()) { panel->addMessage(chat); toast->reshapeToPanel(); @@ -596,17 +597,22 @@ void LLFloaterIMNearbyChatHandler::processChat(const LLChat& chat_msg, { // Handle IRC styled messages. std::string toast_msg; + std::string msg_text = without_LUA_PREFIX(chat_msg.mText, chat_msg.mIsScript); if (chat_msg.mChatStyle == CHAT_STYLE_IRC) { + if (chat_msg.mIsScript) + { + toast_msg += LLTrans::getString("ScriptStr"); + } if (!chat_msg.mFromName.empty()) { toast_msg += chat_msg.mFromName; } - toast_msg += chat_msg.mText.substr(3); + toast_msg += msg_text.substr(3); } else { - toast_msg = chat_msg.mText; + toast_msg = msg_text; } bool chat_overlaps = false; @@ -666,6 +672,7 @@ void LLFloaterIMNearbyChatHandler::processChat(const LLChat& chat_msg, chat["color_alpha"] = r_color_alpha; chat["font_size"] = (S32)LLViewerChat::getChatFontSize() ; chat["message"] = toast_msg; + chat["is_lua"] = chat_msg.mIsScript; channel->addChat(chat); } diff --git a/indra/newview/llfloaterimnearbychatlistener.cpp b/indra/newview/llfloaterimnearbychatlistener.cpp index 43173d3680..0618741cc4 100644 --- a/indra/newview/llfloaterimnearbychatlistener.cpp +++ b/indra/newview/llfloaterimnearbychatlistener.cpp @@ -34,12 +34,14 @@ #include "llagent.h" #include "llchat.h" #include "llviewercontrol.h" +#include "stringize.h" +static const F32 CHAT_THROTTLE_PERIOD = 1.f; -LLFloaterIMNearbyChatListener::LLFloaterIMNearbyChatListener(LLFloaterIMNearbyChat & chatbar) +LLFloaterIMNearbyChatListener::LLFloaterIMNearbyChatListener() : LLEventAPI("LLChatBar", "LLChatBar listener to (e.g.) sendChat, etc."), - mChatbar(chatbar) + mLastThrottleTime(0) { add("sendChat", "Send chat to the simulator:\n" @@ -51,10 +53,19 @@ LLFloaterIMNearbyChatListener::LLFloaterIMNearbyChatListener(LLFloaterIMNearbyCh // "sendChat" command -void LLFloaterIMNearbyChatListener::sendChat(LLSD const & chat_data) const +void LLFloaterIMNearbyChatListener::sendChat(LLSD const & chat_data) { + F64 cur_time = LLTimer::getElapsedSeconds(); + + if (cur_time < mLastThrottleTime + CHAT_THROTTLE_PERIOD) + { + LL_DEBUGS("LLFloaterIMNearbyChatListener") << "'sendChat' was throttled" << LL_ENDL; + return; + } + mLastThrottleTime = cur_time; + // Extract the data - std::string chat_text = chat_data["message"].asString(); + std::string chat_text = LUA_PREFIX + chat_data["message"].asString(); S32 channel = 0; if (chat_data.has("channel")) @@ -81,20 +92,14 @@ void LLFloaterIMNearbyChatListener::sendChat(LLSD const & chat_data) const } // Have to prepend /42 style channel numbers - std::string chat_to_send; - if (channel == 0) - { - chat_to_send = chat_text; - } - else + if (channel) { - chat_to_send += "/"; - chat_to_send += chat_data["channel"].asString(); - chat_to_send += " "; - chat_to_send += chat_text; + chat_text = stringize("/", chat_data["channel"].asString(), " ", chat_text); } // Send it as if it was typed in - mChatbar.sendChatFromViewer(chat_to_send, type_o_chat, ((bool)(channel == 0)) && gSavedSettings.getBOOL("PlayChatAnim")); + LLFloaterIMNearbyChat::sendChatFromViewer(chat_text, type_o_chat, + (channel == 0) && + gSavedSettings.getBOOL("PlayChatAnim")); } diff --git a/indra/newview/llfloaterimnearbychatlistener.h b/indra/newview/llfloaterimnearbychatlistener.h index 96184d95b3..18a8bacfaa 100644 --- a/indra/newview/llfloaterimnearbychatlistener.h +++ b/indra/newview/llfloaterimnearbychatlistener.h @@ -38,12 +38,12 @@ class LLFloaterIMNearbyChat; class LLFloaterIMNearbyChatListener : public LLEventAPI { public: - LLFloaterIMNearbyChatListener(LLFloaterIMNearbyChat & chatbar); + LLFloaterIMNearbyChatListener(); private: - void sendChat(LLSD const & chat_data) const; + void sendChat(LLSD const & chat_data); - LLFloaterIMNearbyChat & mChatbar; + F64 mLastThrottleTime{ 0.0 }; }; #endif // LL_LLFLOATERIMNEARBYCHATLISTENER_H diff --git a/indra/newview/llfloaterimsession.cpp b/indra/newview/llfloaterimsession.cpp index 97e0d01b52..e85aac3810 100644 --- a/indra/newview/llfloaterimsession.cpp +++ b/indra/newview/llfloaterimsession.cpp @@ -92,7 +92,7 @@ LLFloaterIMSession::LLFloaterIMSession(const LLUUID& session_id) LLTransientFloaterMgr::getInstance()->addControlView(LLTransientFloaterMgr::IM, this); mEnableCallbackRegistrar.add("Avatar.EnableGearItem", boost::bind(&LLFloaterIMSession::enableGearMenuItem, this, _2)); - mCommitCallbackRegistrar.add("Avatar.GearDoToSelected", boost::bind(&LLFloaterIMSession::GearDoToSelected, this, _2)); + mCommitCallbackRegistrar.add("Avatar.GearDoToSelected", { boost::bind(&LLFloaterIMSession::GearDoToSelected, this, _2), cb_info::UNTRUSTED_BLOCK }); mEnableCallbackRegistrar.add("Avatar.CheckGearItem", boost::bind(&LLFloaterIMSession::checkGearMenuItem, this, _2)); mVoiceChannelChanged = LLVoiceChannel::setCurrentVoiceChannelChangedCallback(boost::bind(&LLFloaterIMSession::onVoiceChannelChanged, this, _1)); diff --git a/indra/newview/llfloaterimsessiontab.cpp b/indra/newview/llfloaterimsessiontab.cpp index fe0916bf15..b2f2984c65 100644 --- a/indra/newview/llfloaterimsessiontab.cpp +++ b/indra/newview/llfloaterimsessiontab.cpp @@ -82,7 +82,7 @@ LLFloaterIMSessionTab::LLFloaterIMSessionTab(const LLSD& session_id) mSession = LLIMModel::getInstance()->findIMSession(mSessionID); mCommitCallbackRegistrar.add("IMSession.Menu.Action", - boost::bind(&LLFloaterIMSessionTab::onIMSessionMenuItemClicked, this, _2)); + { boost::bind(&LLFloaterIMSessionTab::onIMSessionMenuItemClicked, this, _2) }); mEnableCallbackRegistrar.add("IMSession.Menu.CompactExpandedModes.CheckItem", boost::bind(&LLFloaterIMSessionTab::onIMCompactExpandedMenuItemCheck, this, _2)); mEnableCallbackRegistrar.add("IMSession.Menu.ShowModes.CheckItem", @@ -93,8 +93,8 @@ LLFloaterIMSessionTab::LLFloaterIMSessionTab(const LLSD& session_id) // Right click menu handling mEnableCallbackRegistrar.add("Avatar.CheckItem", boost::bind(&LLFloaterIMSessionTab::checkContextMenuItem, this, _2)); mEnableCallbackRegistrar.add("Avatar.EnableItem", boost::bind(&LLFloaterIMSessionTab::enableContextMenuItem, this, _2)); - mCommitCallbackRegistrar.add("Avatar.DoToSelected", boost::bind(&LLFloaterIMSessionTab::doToSelected, this, _2)); - mCommitCallbackRegistrar.add("Group.DoToSelected", boost::bind(&cb_group_do_nothing)); + mCommitCallbackRegistrar.add("Avatar.DoToSelected", { boost::bind(&LLFloaterIMSessionTab::doToSelected, this, _2) }); + mCommitCallbackRegistrar.add("Group.DoToSelected", { boost::bind(&cb_group_do_nothing) }); mMinFloaterHeight = getMinHeight(); } diff --git a/indra/newview/llfloaterinspect.cpp b/indra/newview/llfloaterinspect.cpp index 4f993ca0e1..0f1eb0cef0 100644 --- a/indra/newview/llfloaterinspect.cpp +++ b/indra/newview/llfloaterinspect.cpp @@ -51,9 +51,9 @@ LLFloaterInspect::LLFloaterInspect(const LLSD& key) mOwnerNameCacheConnection(), mCreatorNameCacheConnection() { - mCommitCallbackRegistrar.add("Inspect.OwnerProfile", boost::bind(&LLFloaterInspect::onClickOwnerProfile, this)); - mCommitCallbackRegistrar.add("Inspect.CreatorProfile", boost::bind(&LLFloaterInspect::onClickCreatorProfile, this)); - mCommitCallbackRegistrar.add("Inspect.SelectObject", boost::bind(&LLFloaterInspect::onSelectObject, this)); + mCommitCallbackRegistrar.add("Inspect.OwnerProfile", { boost::bind(&LLFloaterInspect::onClickOwnerProfile, this) }); + mCommitCallbackRegistrar.add("Inspect.CreatorProfile", { boost::bind(&LLFloaterInspect::onClickCreatorProfile, this) }); + mCommitCallbackRegistrar.add("Inspect.SelectObject", { boost::bind(&LLFloaterInspect::onSelectObject, this) }); } bool LLFloaterInspect::postBuild() diff --git a/indra/newview/llfloaterlagmeter.cpp b/indra/newview/llfloaterlagmeter.cpp index 28fa8dea9a..26d7e3ead2 100644 --- a/indra/newview/llfloaterlagmeter.cpp +++ b/indra/newview/llfloaterlagmeter.cpp @@ -47,7 +47,7 @@ const std::string LAG_GOOD_IMAGE_NAME = "lag_status_good.tga"; LLFloaterLagMeter::LLFloaterLagMeter(const LLSD& key) : LLFloater(key) { - mCommitCallbackRegistrar.add("LagMeter.ClickShrink", boost::bind(&LLFloaterLagMeter::onClickShrink, this)); + mCommitCallbackRegistrar.add("LagMeter.ClickShrink", { boost::bind(&LLFloaterLagMeter::onClickShrink, this) }); } bool LLFloaterLagMeter::postBuild() diff --git a/indra/newview/llfloaterlinkreplace.cpp b/indra/newview/llfloaterlinkreplace.cpp index c961070787..71dcc3d114 100644 --- a/indra/newview/llfloaterlinkreplace.cpp +++ b/indra/newview/llfloaterlinkreplace.cpp @@ -45,7 +45,7 @@ LLFloaterLinkReplace::LLFloaterLinkReplace(const LLSD& key) mTargetUUID(LLUUID::null), mBatchSize(gSavedSettings.getU32("LinkReplaceBatchSize")) { - mEventTimer.stop(); + stop(); } LLFloaterLinkReplace::~LLFloaterLinkReplace() @@ -202,7 +202,7 @@ void LLFloaterLinkReplace::onStartClickedResponse(const LLSD& notification, cons mStartBtn->setEnabled(false); mRefreshBtn->setEnabled(false); - mEventTimer.start(); + start(); tick(); } else @@ -298,7 +298,7 @@ void LLFloaterLinkReplace::decreaseOpenItemCount() mStatusText->setText(getString("ReplaceFinished")); mStartBtn->setEnabled(true); mRefreshBtn->setEnabled(true); - mEventTimer.stop(); + stop(); LL_INFOS() << "Inventory link replace finished." << LL_ENDL; } else @@ -320,7 +320,7 @@ bool LLFloaterLinkReplace::tick() { if (!mRemainingInventoryItems.size()) { - mEventTimer.stop(); + stop(); break; } diff --git a/indra/newview/llfloaterlinkreplace.h b/indra/newview/llfloaterlinkreplace.h index 7f9f0b59e1..31df083a11 100644 --- a/indra/newview/llfloaterlinkreplace.h +++ b/indra/newview/llfloaterlinkreplace.h @@ -86,10 +86,10 @@ public: LLFloaterLinkReplace(const LLSD& key); virtual ~LLFloaterLinkReplace(); - bool postBuild(); - virtual void onOpen(const LLSD& key); + bool postBuild() override; + void onOpen(const LLSD& key) override; - virtual bool tick(); + bool tick() override; private: void checkEnableStart(); diff --git a/indra/newview/llfloaterluadebug.cpp b/indra/newview/llfloaterluadebug.cpp new file mode 100644 index 0000000000..dc989fe15d --- /dev/null +++ b/indra/newview/llfloaterluadebug.cpp @@ -0,0 +1,168 @@ +/** + * @file llfloaterluadebug.cpp + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llfloaterluadebug.h" + +#include "lllineeditor.h" +#include "lltexteditor.h" +#include "llviewermenufile.h" // LLFilePickerReplyThread + +#include "llagent.h" +#include "llappearancemgr.h" +#include "llfloaterreg.h" +#include "llfloaterimnearbychat.h" + +#include "llluamanager.h" +#include "llsdutil.h" +#include "lua_function.h" +#include "stringize.h" +#include "tempset.h" + + +LLFloaterLUADebug::LLFloaterLUADebug(const LLSD &key) + : LLFloater(key) +{ +} + + +bool LLFloaterLUADebug::postBuild() +{ + mResultOutput = getChild<LLTextEditor>("result_text"); + mLineInput = getChild<LLLineEditor>("lua_cmd"); + mScriptPath = getChild<LLLineEditor>("script_path"); + mOutConnection = LLEventPumps::instance().obtain("lua output") + .listen("LLFloaterLUADebug", + [mResultOutput=mResultOutput](const LLSD& data) + { + mResultOutput->pasteTextWithLinebreaks(data.asString()); + mResultOutput->addLineBreakChar(true); + return false; + }); + + getChild<LLButton>("execute_btn")->setClickedCallback(boost::bind(&LLFloaterLUADebug::onExecuteClicked, this)); + getChild<LLButton>("browse_btn")->setClickedCallback(boost::bind(&LLFloaterLUADebug::onBtnBrowse, this)); + getChild<LLButton>("run_btn")->setClickedCallback(boost::bind(&LLFloaterLUADebug::onBtnRun, this)); + mLineInput->setCommitCallback(boost::bind(&LLFloaterLUADebug::onExecuteClicked, this)); + mLineInput->setSelectAllonCommit(false); + + return TRUE; +} + +LLFloaterLUADebug::~LLFloaterLUADebug() +{} + +void LLFloaterLUADebug::onExecuteClicked() +{ + // Empirically, running Lua code that indirectly invokes the + // "LLNotifications" listener can result (via mysterious labyrinthine + // viewer UI byways) in a recursive call to this handler. We've seen Bad + // Things happen to the viewer with a second call to runScriptLine() with + // the same cmd on the same LuaState. + if (mExecuting) + { + LL_DEBUGS("Lua") << "recursive call to onExecuteClicked()" << LL_ENDL; + return; + } + TempSet executing(mExecuting, true); + mResultOutput->setValue(""); + + std::string cmd = mLineInput->getText(); + LLLUAmanager::runScriptLine(cmd, [this](int count, const LLSD& result) + { + completion(count, result); + }); +} + +void LLFloaterLUADebug::onBtnBrowse() +{ + LLFilePickerReplyThread::startPicker(boost::bind(&LLFloaterLUADebug::runSelectedScript, this, _1), LLFilePicker::FFLOAD_LUA, false); +} + +void LLFloaterLUADebug::onBtnRun() +{ + std::vector<std::string> filenames; + std::string filepath = mScriptPath->getText(); + if (!filepath.empty()) + { + filenames.push_back(filepath); + runSelectedScript(filenames); + } +} + +void LLFloaterLUADebug::runSelectedScript(const std::vector<std::string> &filenames) +{ + if (mExecuting) + { + LL_DEBUGS("Lua") << "recursive call to runSelectedScript()" << LL_ENDL; + return; + } + TempSet executing(mExecuting, true); + mResultOutput->setValue(""); + + std::string filepath = filenames[0]; + if (!filepath.empty()) + { + mScriptPath->setText(filepath); + LLLUAmanager::runScriptFile(filepath, false, [this](int count, const LLSD &result) + { + completion(count, result); + }); + } +} + +void LLFloaterLUADebug::completion(int count, const LLSD& result) +{ + if (count < 0) + { + // error: show error message + LLStyle::Params params; + params.readonly_color = LLUIColorTable::instance().getColor("LtRed"); + mResultOutput->appendText(result.asString(), false, params); + mResultOutput->endOfDoc(); + return; + } + if (count == 0) + { + // no results + mResultOutput->pasteTextWithLinebreaks(stringize("ok ", ++mAck)); + return; + } + if (count == 1) + { + // single result + mResultOutput->pasteTextWithLinebreaks(stringize(result)); + return; + } + // multiple results + const char* sep = ""; + for (const auto& item : llsd::inArray(result)) + { + mResultOutput->insertText(sep); + mResultOutput->pasteTextWithLinebreaks(stringize(item)); + sep = ", "; + } +} diff --git a/indra/newview/llfloaterluadebug.h b/indra/newview/llfloaterluadebug.h new file mode 100644 index 0000000000..e27c63b74d --- /dev/null +++ b/indra/newview/llfloaterluadebug.h @@ -0,0 +1,72 @@ +/** + * @file llfloaterluadebug.h + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLFLOATERLUADEBUG_H +#define LL_LLFLOATERLUADEBUG_H + +#include "llevents.h" +#include "llfloater.h" +#include "lua_function.h" + +extern "C" +{ +#include "luau/luacode.h" +#include "luau/lua.h" +#include "luau/luaconf.h" +#include "luau/lualib.h" +} + +class LLLineEditor; +class LLTextEditor; + +class LLFloaterLUADebug : + public LLFloater +{ + public: + LLFloaterLUADebug(const LLSD& key); + virtual ~LLFloaterLUADebug(); + + /*virtual*/ bool postBuild(); + + void onExecuteClicked(); + void onBtnBrowse(); + void onBtnRun(); + + void runSelectedScript(const std::vector<std::string> &filenames); + +private: + void completion(int count, const LLSD& result); + + LLTempBoundListener mOutConnection; + + LLTextEditor* mResultOutput; + LLLineEditor* mLineInput; + LLLineEditor* mScriptPath; + U32 mAck{ 0 }; + bool mExecuting{ false }; +}; + +#endif // LL_LLFLOATERLUADEBUG_H + diff --git a/indra/newview/llfloaterluascripts.cpp b/indra/newview/llfloaterluascripts.cpp new file mode 100644 index 0000000000..79623cdefb --- /dev/null +++ b/indra/newview/llfloaterluascripts.cpp @@ -0,0 +1,131 @@ +/** + * @file llfloaterluascriptsinfo.cpp + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2024, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llfloaterluascripts.h" +#include "llevents.h" +#include <filesystem> +#include "llluamanager.h" +#include "llscrolllistctrl.h" +#include "llviewerwindow.h" +#include "llwindow.h" +#include "llviewermenu.h" + +const F32 REFRESH_INTERVAL = 1.0f; + +LLFloaterLUAScripts::LLFloaterLUAScripts(const LLSD &key) + : LLFloater(key), + mUpdateTimer(new LLTimer()), + mContextMenuHandle() +{ + mCommitCallbackRegistrar.add("Script.OpenFolder", {[this](LLUICtrl*, const LLSD &userdata) + { + if (mScriptList->hasSelectedItem()) + { + std::string target_folder_path = std::filesystem::path((mScriptList->getFirstSelected()->getColumn(1)->getValue().asString())).parent_path().string(); + gViewerWindow->getWindow()->openFolder(target_folder_path); + } + }, cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Script.Terminate", {[this](LLUICtrl*, const LLSD &userdata) + { + if (mScriptList->hasSelectedItem()) + { + std::string coro_name = mScriptList->getSelectedValue(); + LLCoros::instance().killreq(coro_name); + } + }, cb_info::UNTRUSTED_BLOCK }); +} + + +bool LLFloaterLUAScripts::postBuild() +{ + mScriptList = getChild<LLScrollListCtrl>("scripts_list"); + mScriptList->setRightMouseDownCallback([this](LLUICtrl *ctrl, S32 x, S32 y, MASK mask) { onScrollListRightClicked(ctrl, x, y);}); + + LLContextMenu *menu = LLUICtrlFactory::getInstance()->createFromFile<LLContextMenu>( + "menu_lua_scripts.xml", gMenuHolder, LLViewerMenuHolderGL::child_registry_t::instance()); + if (menu) + { + mContextMenuHandle = menu->getHandle(); + } + + return TRUE; +} + +LLFloaterLUAScripts::~LLFloaterLUAScripts() +{ + auto menu = mContextMenuHandle.get(); + if (menu) + { + menu->die(); + mContextMenuHandle.markDead(); + } +} + +void LLFloaterLUAScripts::draw() +{ + if (mUpdateTimer->checkExpirationAndReset(REFRESH_INTERVAL)) + { + populateScriptList(); + } + LLFloater::draw(); +} + +void LLFloaterLUAScripts::populateScriptList() +{ + S32 prev_pos = mScriptList->getScrollPos(); + LLSD prev_selected = mScriptList->getSelectedValue(); + mScriptList->clearRows(); + mScriptList->updateColumns(true); + std::map<std::string, std::string> scripts = LLLUAmanager::getScriptNames(); + for (auto &it : scripts) + { + LLSD row; + row["value"] = it.first; + row["columns"][0]["value"] = std::filesystem::path((it.second)).stem().string(); + row["columns"][0]["column"] = "script_name"; + row["columns"][1]["value"] = it.second; + row["columns"][1]["column"] = "script_path"; + mScriptList->addElement(row); + } + mScriptList->setScrollPos(prev_pos); + mScriptList->setSelectedByValue(prev_selected, true); +} + +void LLFloaterLUAScripts::onScrollListRightClicked(LLUICtrl *ctrl, S32 x, S32 y) +{ + LLScrollListItem *item = mScriptList->hitItem(x, y); + if (item) + { + mScriptList->selectItemAt(x, y, MASK_NONE); + auto menu = mContextMenuHandle.get(); + if (menu) + { + menu->show(x, y); + LLMenuGL::showPopup(this, menu, x, y); + } + } +} diff --git a/indra/newview/llfloaterluascripts.h b/indra/newview/llfloaterluascripts.h new file mode 100644 index 0000000000..14ca42d6fb --- /dev/null +++ b/indra/newview/llfloaterluascripts.h @@ -0,0 +1,54 @@ +/** + * @file llfloaterluascriptsinfo.h + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2024, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLFLOATERLUASCRIPTS_H +#define LL_LLFLOATERLUASCRIPTS_H + +#include "llfloater.h" + +class LLScrollListCtrl; + +class LLFloaterLUAScripts : + public LLFloater +{ + public: + LLFloaterLUAScripts(const LLSD &key); + virtual ~LLFloaterLUAScripts(); + + bool postBuild(); + void draw(); + +private: + void populateScriptList(); + void onScrollListRightClicked(LLUICtrl *ctrl, S32 x, S32 y); + + std::unique_ptr<LLTimer> mUpdateTimer; + LLScrollListCtrl* mScriptList; + + LLHandle<LLContextMenu> mContextMenuHandle; +}; + +#endif // LL_LLFLOATERLUASCRIPTS_H + diff --git a/indra/newview/llfloatermarketplacelistings.cpp b/indra/newview/llfloatermarketplacelistings.cpp index f20fea01c5..e91745fff0 100644 --- a/indra/newview/llfloatermarketplacelistings.cpp +++ b/indra/newview/llfloatermarketplacelistings.cpp @@ -58,7 +58,7 @@ LLPanelMarketplaceListings::LLPanelMarketplaceListings() , mSortOrder(LLInventoryFilter::SO_FOLDERS_BY_NAME) , mFilterListingFoldersOnly(false) { - mCommitCallbackRegistrar.add("Marketplace.ViewSort.Action", boost::bind(&LLPanelMarketplaceListings::onViewSortMenuItemClicked, this, _2)); + mCommitCallbackRegistrar.add("Marketplace.ViewSort.Action", { boost::bind(&LLPanelMarketplaceListings::onViewSortMenuItemClicked, this, _2) }); mEnableCallbackRegistrar.add("Marketplace.ViewSort.CheckItem", boost::bind(&LLPanelMarketplaceListings::onViewSortMenuItemCheck, this, _2)); } diff --git a/indra/newview/llfloatermemleak.cpp b/indra/newview/llfloatermemleak.cpp index b4bb45c864..4ec8cbf71d 100644 --- a/indra/newview/llfloatermemleak.cpp +++ b/indra/newview/llfloatermemleak.cpp @@ -48,12 +48,12 @@ LLFloaterMemLeak::LLFloaterMemLeak(const LLSD& key) : LLFloater(key) { setTitle("Memory Leaking Simulation Floater"); - mCommitCallbackRegistrar.add("MemLeak.ChangeLeakingSpeed", boost::bind(&LLFloaterMemLeak::onChangeLeakingSpeed, this)); - mCommitCallbackRegistrar.add("MemLeak.ChangeMaxMemLeaking", boost::bind(&LLFloaterMemLeak::onChangeMaxMemLeaking, this)); - mCommitCallbackRegistrar.add("MemLeak.Start", boost::bind(&LLFloaterMemLeak::onClickStart, this)); - mCommitCallbackRegistrar.add("MemLeak.Stop", boost::bind(&LLFloaterMemLeak::onClickStop, this)); - mCommitCallbackRegistrar.add("MemLeak.Release", boost::bind(&LLFloaterMemLeak::onClickRelease, this)); - mCommitCallbackRegistrar.add("MemLeak.Close", boost::bind(&LLFloaterMemLeak::onClickClose, this)); + mCommitCallbackRegistrar.add("MemLeak.ChangeLeakingSpeed", { boost::bind(&LLFloaterMemLeak::onChangeLeakingSpeed, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("MemLeak.ChangeMaxMemLeaking", { boost::bind(&LLFloaterMemLeak::onChangeMaxMemLeaking, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("MemLeak.Start", { boost::bind(&LLFloaterMemLeak::onClickStart, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("MemLeak.Stop", { boost::bind(&LLFloaterMemLeak::onClickStop, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("MemLeak.Release", { boost::bind(&LLFloaterMemLeak::onClickRelease, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("MemLeak.Close", { boost::bind(&LLFloaterMemLeak::onClickClose, this), cb_info::UNTRUSTED_BLOCK }); } //---------------------------------------------- diff --git a/indra/newview/llfloatermyenvironment.cpp b/indra/newview/llfloatermyenvironment.cpp index 891e16a8ef..b8aa4c6f72 100644 --- a/indra/newview/llfloatermyenvironment.cpp +++ b/indra/newview/llfloatermyenvironment.cpp @@ -81,10 +81,10 @@ LLFloaterMyEnvironment::LLFloaterMyEnvironment(const LLSD& key) : mTypeFilter((0x01 << static_cast<U64>(LLSettingsType::ST_DAYCYCLE)) | (0x01 << static_cast<U64>(LLSettingsType::ST_SKY)) | (0x01 << static_cast<U64>(LLSettingsType::ST_WATER))), mSelectedAsset() { - mCommitCallbackRegistrar.add(ACTION_DOCREATE, [this](LLUICtrl *, const LLSD &userdata) { onDoCreate(userdata); }); - mCommitCallbackRegistrar.add(ACTION_DOEDIT, [this](LLUICtrl *, const LLSD &userdata) { mInventoryList->openSelected(); }); - mCommitCallbackRegistrar.add(ACTION_DOAPPLY, [this](LLUICtrl *, const LLSD &userdata) { onDoApply(userdata.asString()); }); - mCommitCallbackRegistrar.add(ACTION_COPYPASTE, [this](LLUICtrl *, const LLSD &userdata) { mInventoryList->doToSelected(userdata.asString()); }); + mCommitCallbackRegistrar.add(ACTION_DOCREATE, { [this](LLUICtrl *, const LLSD &userdata) { onDoCreate(userdata); } }); + mCommitCallbackRegistrar.add(ACTION_DOEDIT, { [this](LLUICtrl *, const LLSD &userdata) { mInventoryList->openSelected(); } }); + mCommitCallbackRegistrar.add(ACTION_DOAPPLY, { [this](LLUICtrl *, const LLSD &userdata) { onDoApply(userdata.asString()); } }); + mCommitCallbackRegistrar.add(ACTION_COPYPASTE, { [this](LLUICtrl *, const LLSD &userdata) { mInventoryList->doToSelected(userdata.asString()); } }); mEnableCallbackRegistrar.add(ENABLE_ACTION, [this](LLUICtrl *, const LLSD &userdata) { return canAction(userdata.asString()); }); mEnableCallbackRegistrar.add(ENABLE_CANAPPLY, [this](LLUICtrl *, const LLSD &userdata) { return canApply(userdata.asString()); }); diff --git a/indra/newview/llfloaternotificationsconsole.cpp b/indra/newview/llfloaternotificationsconsole.cpp index a819b30e30..883b285c06 100644 --- a/indra/newview/llfloaternotificationsconsole.cpp +++ b/indra/newview/llfloaternotificationsconsole.cpp @@ -151,7 +151,7 @@ bool LLNotificationChannelPanel::update(const LLSD& payload) LLFloaterNotificationConsole::LLFloaterNotificationConsole(const LLSD& key) : LLFloater(key) { - mCommitCallbackRegistrar.add("ClickAdd", boost::bind(&LLFloaterNotificationConsole::onClickAdd, this)); + mCommitCallbackRegistrar.add("ClickAdd", { boost::bind(&LLFloaterNotificationConsole::onClickAdd, this) }); } bool LLFloaterNotificationConsole::postBuild() diff --git a/indra/newview/llfloateropenobject.cpp b/indra/newview/llfloateropenobject.cpp index b06e35f65d..afd77ebd50 100644 --- a/indra/newview/llfloateropenobject.cpp +++ b/indra/newview/llfloateropenobject.cpp @@ -55,8 +55,8 @@ LLFloaterOpenObject::LLFloaterOpenObject(const LLSD& key) mPanelInventoryObject(NULL), mDirty(true) { - mCommitCallbackRegistrar.add("OpenObject.MoveToInventory", boost::bind(&LLFloaterOpenObject::onClickMoveToInventory, this)); - mCommitCallbackRegistrar.add("OpenObject.Cancel", boost::bind(&LLFloaterOpenObject::onClickCancel, this)); + mCommitCallbackRegistrar.add("OpenObject.MoveToInventory", { boost::bind(&LLFloaterOpenObject::onClickMoveToInventory, this), cb_info::UNTRUSTED_THROTTLE }); + mCommitCallbackRegistrar.add("OpenObject.Cancel", { boost::bind(&LLFloaterOpenObject::onClickCancel, this) }); } LLFloaterOpenObject::~LLFloaterOpenObject() diff --git a/indra/newview/llfloaterperformance.cpp b/indra/newview/llfloaterperformance.cpp index 315508f22b..851e15feed 100644 --- a/indra/newview/llfloaterperformance.cpp +++ b/indra/newview/llfloaterperformance.cpp @@ -67,7 +67,7 @@ protected: { LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; - registrar.add("Settings.SetRendering", boost::bind(&LLFloaterPerformance::onCustomAction, mFloaterPerformance, _2, mUUIDs.front())); + registrar.add("Settings.SetRendering", { boost::bind(&LLFloaterPerformance::onCustomAction, mFloaterPerformance, _2, mUUIDs.front()), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); enable_registrar.add("Settings.IsSelected", boost::bind(&LLFloaterPerformance::isActionChecked, mFloaterPerformance, _2, mUUIDs.front())); LLContextMenu* menu = createFromFile("menu_avatar_rendering_settings.xml"); diff --git a/indra/newview/llfloaterperms.cpp b/indra/newview/llfloaterperms.cpp index 7311f0deb6..86a99aba39 100644 --- a/indra/newview/llfloaterperms.cpp +++ b/indra/newview/llfloaterperms.cpp @@ -107,9 +107,9 @@ static bool mCapSent = false; LLFloaterPermsDefault::LLFloaterPermsDefault(const LLSD& seed) : LLFloater(seed) { - mCommitCallbackRegistrar.add("PermsDefault.Copy", boost::bind(&LLFloaterPermsDefault::onCommitCopy, this, _2)); - mCommitCallbackRegistrar.add("PermsDefault.OK", boost::bind(&LLFloaterPermsDefault::onClickOK, this)); - mCommitCallbackRegistrar.add("PermsDefault.Cancel", boost::bind(&LLFloaterPermsDefault::onClickCancel, this)); + mCommitCallbackRegistrar.add("PermsDefault.Copy", { boost::bind(&LLFloaterPermsDefault::onCommitCopy, this, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("PermsDefault.OK", { boost::bind(&LLFloaterPermsDefault::onClickOK, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("PermsDefault.Cancel", { boost::bind(&LLFloaterPermsDefault::onClickCancel, this), cb_info::UNTRUSTED_BLOCK }); } diff --git a/indra/newview/llfloaterpreference.cpp b/indra/newview/llfloaterpreference.cpp index e673752986..a8596c1b9f 100644 --- a/indra/newview/llfloaterpreference.cpp +++ b/indra/newview/llfloaterpreference.cpp @@ -312,43 +312,43 @@ LLFloaterPreference::LLFloaterPreference(const LLSD& key) registered_dialog = true; } - mCommitCallbackRegistrar.add("Pref.Cancel", boost::bind(&LLFloaterPreference::onBtnCancel, this, _2)); - mCommitCallbackRegistrar.add("Pref.OK", boost::bind(&LLFloaterPreference::onBtnOK, this, _2)); - - mCommitCallbackRegistrar.add("Pref.ClearCache", boost::bind(&LLFloaterPreference::onClickClearCache, this)); - mCommitCallbackRegistrar.add("Pref.WebClearCache", boost::bind(&LLFloaterPreference::onClickBrowserClearCache, this)); - mCommitCallbackRegistrar.add("Pref.SetCache", boost::bind(&LLFloaterPreference::onClickSetCache, this)); - mCommitCallbackRegistrar.add("Pref.ResetCache", boost::bind(&LLFloaterPreference::onClickResetCache, this)); - mCommitCallbackRegistrar.add("Pref.ClickSkin", boost::bind(&LLFloaterPreference::onClickSkin, this,_1, _2)); - mCommitCallbackRegistrar.add("Pref.SelectSkin", boost::bind(&LLFloaterPreference::onSelectSkin, this)); - mCommitCallbackRegistrar.add("Pref.SetSounds", boost::bind(&LLFloaterPreference::onClickSetSounds, this)); - mCommitCallbackRegistrar.add("Pref.ClickEnablePopup", boost::bind(&LLFloaterPreference::onClickEnablePopup, this)); - mCommitCallbackRegistrar.add("Pref.ClickDisablePopup", boost::bind(&LLFloaterPreference::onClickDisablePopup, this)); - mCommitCallbackRegistrar.add("Pref.LogPath", boost::bind(&LLFloaterPreference::onClickLogPath, this)); - mCommitCallbackRegistrar.add("Pref.RenderExceptions", boost::bind(&LLFloaterPreference::onClickRenderExceptions, this)); - mCommitCallbackRegistrar.add("Pref.AutoAdjustments", boost::bind(&LLFloaterPreference::onClickAutoAdjustments, this)); - mCommitCallbackRegistrar.add("Pref.HardwareDefaults", boost::bind(&LLFloaterPreference::setHardwareDefaults, this)); - mCommitCallbackRegistrar.add("Pref.AvatarImpostorsEnable", boost::bind(&LLFloaterPreference::onAvatarImpostorsEnable, this)); - mCommitCallbackRegistrar.add("Pref.UpdateIndirectMaxComplexity", boost::bind(&LLFloaterPreference::updateMaxComplexity, this)); - mCommitCallbackRegistrar.add("Pref.RenderOptionUpdate", boost::bind(&LLFloaterPreference::onRenderOptionEnable, this)); - mCommitCallbackRegistrar.add("Pref.WindowedMod", boost::bind(&LLFloaterPreference::onCommitWindowedMode, this)); - mCommitCallbackRegistrar.add("Pref.UpdateSliderText", boost::bind(&LLFloaterPreference::refreshUI,this)); - mCommitCallbackRegistrar.add("Pref.QualityPerformance", boost::bind(&LLFloaterPreference::onChangeQuality, this, _2)); - mCommitCallbackRegistrar.add("Pref.applyUIColor", boost::bind(&LLFloaterPreference::applyUIColor, this ,_1, _2)); - mCommitCallbackRegistrar.add("Pref.getUIColor", boost::bind(&LLFloaterPreference::getUIColor, this ,_1, _2)); - mCommitCallbackRegistrar.add("Pref.MaturitySettings", boost::bind(&LLFloaterPreference::onChangeMaturity, this)); - mCommitCallbackRegistrar.add("Pref.BlockList", boost::bind(&LLFloaterPreference::onClickBlockList, this)); - mCommitCallbackRegistrar.add("Pref.Proxy", boost::bind(&LLFloaterPreference::onClickProxySettings, this)); - mCommitCallbackRegistrar.add("Pref.TranslationSettings", boost::bind(&LLFloaterPreference::onClickTranslationSettings, this)); - mCommitCallbackRegistrar.add("Pref.AutoReplace", boost::bind(&LLFloaterPreference::onClickAutoReplace, this)); - mCommitCallbackRegistrar.add("Pref.PermsDefault", boost::bind(&LLFloaterPreference::onClickPermsDefault, this)); - mCommitCallbackRegistrar.add("Pref.RememberedUsernames", boost::bind(&LLFloaterPreference::onClickRememberedUsernames, this)); - mCommitCallbackRegistrar.add("Pref.SpellChecker", boost::bind(&LLFloaterPreference::onClickSpellChecker, this)); - mCommitCallbackRegistrar.add("Pref.Advanced", boost::bind(&LLFloaterPreference::onClickAdvanced, this)); + mCommitCallbackRegistrar.add("Pref.Cancel", { boost::bind(&LLFloaterPreference::onBtnCancel, this, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.OK", { boost::bind(&LLFloaterPreference::onBtnOK, this, _2), cb_info::UNTRUSTED_BLOCK }); + + mCommitCallbackRegistrar.add("Pref.ClearCache", { boost::bind(&LLFloaterPreference::onClickClearCache, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.WebClearCache", { boost::bind(&LLFloaterPreference::onClickBrowserClearCache, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.SetCache", { boost::bind(&LLFloaterPreference::onClickSetCache, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.ResetCache", { boost::bind(&LLFloaterPreference::onClickResetCache, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.ClickSkin", { boost::bind(&LLFloaterPreference::onClickSkin, this,_1, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.SelectSkin", { boost::bind(&LLFloaterPreference::onSelectSkin, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.SetSounds", { boost::bind(&LLFloaterPreference::onClickSetSounds, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.ClickEnablePopup", { boost::bind(&LLFloaterPreference::onClickEnablePopup, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.ClickDisablePopup", { boost::bind(&LLFloaterPreference::onClickDisablePopup, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.LogPath", { boost::bind(&LLFloaterPreference::onClickLogPath, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.RenderExceptions", { boost::bind(&LLFloaterPreference::onClickRenderExceptions, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.AutoAdjustments", { boost::bind(&LLFloaterPreference::onClickAutoAdjustments, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.HardwareDefaults", { boost::bind(&LLFloaterPreference::setHardwareDefaults, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.AvatarImpostorsEnable", { boost::bind(&LLFloaterPreference::onAvatarImpostorsEnable, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.UpdateIndirectMaxComplexity", { boost::bind(&LLFloaterPreference::updateMaxComplexity, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.RenderOptionUpdate", { boost::bind(&LLFloaterPreference::onRenderOptionEnable, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.WindowedMod", { boost::bind(&LLFloaterPreference::onCommitWindowedMode, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.UpdateSliderText", { boost::bind(&LLFloaterPreference::refreshUI,this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.QualityPerformance", { boost::bind(&LLFloaterPreference::onChangeQuality, this, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.applyUIColor", { boost::bind(&LLFloaterPreference::applyUIColor, this ,_1, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.getUIColor", { boost::bind(&LLFloaterPreference::getUIColor, this ,_1, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.MaturitySettings", { boost::bind(&LLFloaterPreference::onChangeMaturity, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.BlockList", { boost::bind(&LLFloaterPreference::onClickBlockList, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.Proxy", { boost::bind(&LLFloaterPreference::onClickProxySettings, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.TranslationSettings", { boost::bind(&LLFloaterPreference::onClickTranslationSettings, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.AutoReplace", { boost::bind(&LLFloaterPreference::onClickAutoReplace, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.PermsDefault", { boost::bind(&LLFloaterPreference::onClickPermsDefault, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.RememberedUsernames", { boost::bind(&LLFloaterPreference::onClickRememberedUsernames, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.SpellChecker", { boost::bind(&LLFloaterPreference::onClickSpellChecker, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.Advanced", { boost::bind(&LLFloaterPreference::onClickAdvanced, this), cb_info::UNTRUSTED_BLOCK }); sSkin = gSavedSettings.getString("SkinCurrent"); - mCommitCallbackRegistrar.add("Pref.ClickActionChange", boost::bind(&LLFloaterPreference::onClickActionChange, this)); + mCommitCallbackRegistrar.add("Pref.ClickActionChange", { boost::bind(&LLFloaterPreference::onClickActionChange, this), cb_info::UNTRUSTED_BLOCK }); gSavedSettings.getControl("NameTagShowUsernames")->getCommitSignal()->connect(boost::bind(&handleNameTagOptionChanged, _2)); gSavedSettings.getControl("NameTagShowFriends")->getCommitSignal()->connect(boost::bind(&handleNameTagOptionChanged, _2)); @@ -361,9 +361,9 @@ LLFloaterPreference::LLFloaterPreference(const LLSD& key) mComplexityChangedSignal = gSavedSettings.getControl("RenderAvatarMaxComplexity")->getCommitSignal()->connect(boost::bind(&LLFloaterPreference::updateComplexityText, this)); - mCommitCallbackRegistrar.add("Pref.ClearLog", boost::bind(&LLConversationLog::onClearLog, &LLConversationLog::instance())); - mCommitCallbackRegistrar.add("Pref.DeleteTranscripts", boost::bind(&LLFloaterPreference::onDeleteTranscripts, this)); - mCommitCallbackRegistrar.add("UpdateFilter", boost::bind(&LLFloaterPreference::onUpdateFilterTerm, this, false)); // <FS:ND/> Hook up for filtering + mCommitCallbackRegistrar.add("Pref.ClearLog", { boost::bind(&LLConversationLog::onClearLog, &LLConversationLog::instance()), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.DeleteTranscripts", { boost::bind(&LLFloaterPreference::onDeleteTranscripts, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("UpdateFilter", { boost::bind(&LLFloaterPreference::onUpdateFilterTerm, this, false), cb_info::UNTRUSTED_BLOCK }); // <FS:ND/> Hook up for filtering } void LLFloaterPreference::processProperties( void* pData, EAvatarProcessorType type ) @@ -1951,7 +1951,7 @@ public: :LLEventTimer(period), mCallback(cb) { - mEventTimer.stop(); + stop(); } virtual ~Updater(){} @@ -1959,15 +1959,15 @@ public: void update(const LLSD& new_value) { mNewValue = new_value; - mEventTimer.start(); + start(); } protected: - bool tick() + bool tick() override { mCallback(mNewValue); - mEventTimer.stop(); + stop(); return false; } @@ -1983,11 +1983,11 @@ LLPanelPreference::LLPanelPreference() : LLPanel(), mBandWidthUpdater(NULL) { - mCommitCallbackRegistrar.add("Pref.setControlFalse", boost::bind(&LLPanelPreference::setControlFalse,this, _2)); - mCommitCallbackRegistrar.add("Pref.updateMediaAutoPlayCheckbox", boost::bind(&LLPanelPreference::updateMediaAutoPlayCheckbox, this, _1)); - mCommitCallbackRegistrar.add("Pref.PrefDelete", boost::bind(&LLPanelPreference::deletePreset, this, _2)); - mCommitCallbackRegistrar.add("Pref.PrefSave", boost::bind(&LLPanelPreference::savePreset, this, _2)); - mCommitCallbackRegistrar.add("Pref.PrefLoad", boost::bind(&LLPanelPreference::loadPreset, this, _2)); + mCommitCallbackRegistrar.add("Pref.setControlFalse", { boost::bind(&LLPanelPreference::setControlFalse,this, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.updateMediaAutoPlayCheckbox", { boost::bind(&LLPanelPreference::updateMediaAutoPlayCheckbox, this, _1), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.PrefDelete", { boost::bind(&LLPanelPreference::deletePreset, this, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.PrefSave", { boost::bind(&LLPanelPreference::savePreset, this, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.PrefLoad", { boost::bind(&LLPanelPreference::loadPreset, this, _2), cb_info::UNTRUSTED_BLOCK }); } //virtual @@ -3087,9 +3087,9 @@ LLFloaterPreferenceProxy::LLFloaterPreferenceProxy(const LLSD& key) : LLFloater(key), mSocksSettingsDirty(false) { - mCommitCallbackRegistrar.add("Proxy.OK", boost::bind(&LLFloaterPreferenceProxy::onBtnOk, this)); - mCommitCallbackRegistrar.add("Proxy.Cancel", boost::bind(&LLFloaterPreferenceProxy::onBtnCancel, this)); - mCommitCallbackRegistrar.add("Proxy.Change", boost::bind(&LLFloaterPreferenceProxy::onChangeSocksSettings, this)); + mCommitCallbackRegistrar.add("Proxy.OK", { boost::bind(&LLFloaterPreferenceProxy::onBtnOk, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Proxy.Cancel", { boost::bind(&LLFloaterPreferenceProxy::onBtnCancel, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Proxy.Change", { boost::bind(&LLFloaterPreferenceProxy::onChangeSocksSettings, this), cb_info::UNTRUSTED_BLOCK }); } LLFloaterPreferenceProxy::~LLFloaterPreferenceProxy() diff --git a/indra/newview/llfloaterpreferencesgraphicsadvanced.cpp b/indra/newview/llfloaterpreferencesgraphicsadvanced.cpp index 1d48fe70f2..cf2123cfac 100644 --- a/indra/newview/llfloaterpreferencesgraphicsadvanced.cpp +++ b/indra/newview/llfloaterpreferencesgraphicsadvanced.cpp @@ -45,12 +45,12 @@ LLFloaterPreferenceGraphicsAdvanced::LLFloaterPreferenceGraphicsAdvanced(const LLSD& key) : LLFloater(key) { - mCommitCallbackRegistrar.add("Pref.RenderOptionUpdate", boost::bind(&LLFloaterPreferenceGraphicsAdvanced::onRenderOptionEnable, this)); - mCommitCallbackRegistrar.add("Pref.UpdateIndirectMaxNonImpostors", boost::bind(&LLFloaterPreferenceGraphicsAdvanced::updateMaxNonImpostors,this)); - mCommitCallbackRegistrar.add("Pref.UpdateIndirectMaxComplexity", boost::bind(&LLFloaterPreferenceGraphicsAdvanced::updateMaxComplexity,this)); + mCommitCallbackRegistrar.add("Pref.RenderOptionUpdate", { boost::bind(&LLFloaterPreferenceGraphicsAdvanced::onRenderOptionEnable, this) }); + mCommitCallbackRegistrar.add("Pref.UpdateIndirectMaxNonImpostors", { boost::bind(&LLFloaterPreferenceGraphicsAdvanced::updateMaxNonImpostors,this) }); + mCommitCallbackRegistrar.add("Pref.UpdateIndirectMaxComplexity", { boost::bind(&LLFloaterPreferenceGraphicsAdvanced::updateMaxComplexity,this) }); - mCommitCallbackRegistrar.add("Pref.Cancel", boost::bind(&LLFloaterPreferenceGraphicsAdvanced::onBtnCancel, this, _2)); - mCommitCallbackRegistrar.add("Pref.OK", boost::bind(&LLFloaterPreferenceGraphicsAdvanced::onBtnOK, this, _2)); + mCommitCallbackRegistrar.add("Pref.Cancel", { boost::bind(&LLFloaterPreferenceGraphicsAdvanced::onBtnCancel, this, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Pref.OK", { boost::bind(&LLFloaterPreferenceGraphicsAdvanced::onBtnOK, this, _2), cb_info::UNTRUSTED_BLOCK }); } LLFloaterPreferenceGraphicsAdvanced::~LLFloaterPreferenceGraphicsAdvanced() diff --git a/indra/newview/llfloaterpreferenceviewadvanced.cpp b/indra/newview/llfloaterpreferenceviewadvanced.cpp index 3481397daa..290742a4cd 100644 --- a/indra/newview/llfloaterpreferenceviewadvanced.cpp +++ b/indra/newview/llfloaterpreferenceviewadvanced.cpp @@ -37,7 +37,7 @@ LLFloaterPreferenceViewAdvanced::LLFloaterPreferenceViewAdvanced(const LLSD& key) : LLFloater(key) { - mCommitCallbackRegistrar.add("CommitSettings", boost::bind(&LLFloaterPreferenceViewAdvanced::onCommitSettings, this)); + mCommitCallbackRegistrar.add("CommitSettings", { boost::bind(&LLFloaterPreferenceViewAdvanced::onCommitSettings, this), cb_info::UNTRUSTED_BLOCK }); } LLFloaterPreferenceViewAdvanced::~LLFloaterPreferenceViewAdvanced() diff --git a/indra/newview/llfloaterregioninfo.cpp b/indra/newview/llfloaterregioninfo.cpp index 8070284e32..99ab75f713 100644 --- a/indra/newview/llfloaterregioninfo.cpp +++ b/indra/newview/llfloaterregioninfo.cpp @@ -259,8 +259,8 @@ bool LLFloaterRegionInfo::postBuild() panel = new LLPanelRegionGeneralInfo; mInfoPanels.push_back(panel); - panel->getCommitCallbackRegistrar().add("RegionInfo.ManageTelehub", boost::bind(&LLPanelRegionInfo::onClickManageTelehub, panel)); - panel->getCommitCallbackRegistrar().add("RegionInfo.ManageRestart", boost::bind(&LLPanelRegionInfo::onClickManageRestartSchedule, panel)); + panel->getCommitCallbackRegistrar().add("RegionInfo.ManageTelehub", { boost::bind(&LLPanelRegionInfo::onClickManageTelehub, panel) }); + panel->getCommitCallbackRegistrar().add("RegionInfo.ManageRestart", { boost::bind(&LLPanelRegionInfo::onClickManageRestartSchedule, panel) }); panel->buildFromFile("panel_region_general.xml"); mTab->addTabPanel(panel); diff --git a/indra/newview/llfloaterregionrestarting.h b/indra/newview/llfloaterregionrestarting.h index ee836dd00d..016c90f944 100644 --- a/indra/newview/llfloaterregionrestarting.h +++ b/indra/newview/llfloaterregionrestarting.h @@ -42,10 +42,10 @@ public: private: LLFloaterRegionRestarting(const LLSD& key); virtual ~LLFloaterRegionRestarting(); - virtual bool postBuild(); - virtual bool tick(); - virtual void refresh(); - virtual void draw(); + bool postBuild() override; + bool tick() override; + void refresh() override; + void draw() override; virtual void regionChange(); std::string mName; diff --git a/indra/newview/llfloaterscriptedprefs.cpp b/indra/newview/llfloaterscriptedprefs.cpp index fa31cd72c1..fbeb256a9c 100644 --- a/indra/newview/llfloaterscriptedprefs.cpp +++ b/indra/newview/llfloaterscriptedprefs.cpp @@ -36,8 +36,8 @@ LLFloaterScriptEdPrefs::LLFloaterScriptEdPrefs(const LLSD& key) : LLFloater(key) , mEditor(NULL) { - mCommitCallbackRegistrar.add("ScriptPref.applyUIColor", boost::bind(&LLFloaterScriptEdPrefs::applyUIColor, this ,_1, _2)); - mCommitCallbackRegistrar.add("ScriptPref.getUIColor", boost::bind(&LLFloaterScriptEdPrefs::getUIColor, this ,_1, _2)); + mCommitCallbackRegistrar.add("ScriptPref.applyUIColor", { boost::bind(&LLFloaterScriptEdPrefs::applyUIColor, this ,_1, _2) }); + mCommitCallbackRegistrar.add("ScriptPref.getUIColor", { boost::bind(&LLFloaterScriptEdPrefs::getUIColor, this ,_1, _2) }); } bool LLFloaterScriptEdPrefs::postBuild() diff --git a/indra/newview/llfloatersettingscolor.cpp b/indra/newview/llfloatersettingscolor.cpp index d9c382a1dc..1ca2412907 100644 --- a/indra/newview/llfloatersettingscolor.cpp +++ b/indra/newview/llfloatersettingscolor.cpp @@ -43,8 +43,8 @@ LLFloaterSettingsColor::LLFloaterSettingsColor(const LLSD& key) : LLFloater(key), mSettingList(NULL) { - mCommitCallbackRegistrar.add("CommitSettings", boost::bind(&LLFloaterSettingsColor::onCommitSettings, this)); - mCommitCallbackRegistrar.add("ClickDefault", boost::bind(&LLFloaterSettingsColor::onClickDefault, this)); + mCommitCallbackRegistrar.add("CommitSettings", { boost::bind(&LLFloaterSettingsColor::onCommitSettings, this) }); + mCommitCallbackRegistrar.add("ClickDefault", { boost::bind(&LLFloaterSettingsColor::onClickDefault, this) }); } LLFloaterSettingsColor::~LLFloaterSettingsColor() diff --git a/indra/newview/llfloatersettingsdebug.cpp b/indra/newview/llfloatersettingsdebug.cpp index 8cc01f6dc6..0a01fff36d 100644 --- a/indra/newview/llfloatersettingsdebug.cpp +++ b/indra/newview/llfloatersettingsdebug.cpp @@ -34,14 +34,16 @@ #include "llcolorswatch.h" #include "llviewercontrol.h" #include "lltexteditor.h" +#include "llclipboard.h" +#include "llsdutil.h" LLFloaterSettingsDebug::LLFloaterSettingsDebug(const LLSD& key) : LLFloater(key), mSettingList(NULL) { - mCommitCallbackRegistrar.add("CommitSettings", boost::bind(&LLFloaterSettingsDebug::onCommitSettings, this)); - mCommitCallbackRegistrar.add("ClickDefault", boost::bind(&LLFloaterSettingsDebug::onClickDefault, this)); + mCommitCallbackRegistrar.add("CommitSettings", { boost::bind(&LLFloaterSettingsDebug::onCommitSettings, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("ClickDefault", { boost::bind(&LLFloaterSettingsDebug::onClickDefault, this), cb_info::UNTRUSTED_BLOCK }); } LLFloaterSettingsDebug::~LLFloaterSettingsDebug() @@ -64,6 +66,8 @@ bool LLFloaterSettingsDebug::postBuild() mSettingNameText = getChild<LLTextBox>("setting_name_txt"); mComment = getChild<LLTextEditor>("comment_text"); + mLLSDVal = getChild<LLTextEditor>("llsd_text"); + mCopyBtn = getChild<LLButton>("copy_btn"); getChild<LLFilterEditor>("filter_input")->setCommitCallback(boost::bind(&LLFloaterSettingsDebug::setSearchFilter, this, _2)); @@ -71,6 +75,8 @@ bool LLFloaterSettingsDebug::postBuild() mSettingList->setCommitOnSelectionChange(true); mSettingList->setCommitCallback(boost::bind(&LLFloaterSettingsDebug::onSettingSelect, this)); + mCopyBtn->setCommitCallback([this](LLUICtrl *ctrl, const LLSD ¶m) { onClickCopy(); }); + updateList(); gSavedSettings.getControl("DebugSettingsHideDefault")->getCommitSignal()->connect(boost::bind(&LLFloaterSettingsDebug::updateList, this, false)); @@ -205,6 +211,7 @@ void LLFloaterSettingsDebug::updateControl(LLControlVariable* controlp) mSettingNameText->setVisible(true); mSettingNameText->setText(controlp->getName()); mSettingNameText->setToolTip(controlp->getName()); + mCopyBtn->setVisible(true); mComment->setVisible(true); std::string old_text = mComment->getText(); @@ -465,6 +472,17 @@ void LLFloaterSettingsDebug::updateControl(LLControlVariable* controlp) mColorSwatch->setValue(sd); break; } + case TYPE_LLSD: + { + mLLSDVal->setVisible(true); + std::string new_text = ll_pretty_print_sd(sd); + // Don't setText if not nessesary, it will reset scroll + if (mLLSDVal->getText() != new_text) + { + mLLSDVal->setText(new_text); + } + break; + } default: mComment->setText(std::string("unknown")); break; @@ -631,7 +649,14 @@ void LLFloaterSettingsDebug::hideUIControls() mValText->setVisible(false); mDefaultButton->setVisible(false); mBooleanCombo->setVisible(false); + mLLSDVal->setVisible(false); mSettingNameText->setVisible(false); + mCopyBtn->setVisible(false); mComment->setVisible(false); } +void LLFloaterSettingsDebug::onClickCopy() +{ + std::string setting_name = mSettingNameText->getText(); + LLClipboard::instance().copyToClipboard(utf8str_to_wstring(setting_name), 0, narrow(setting_name.size())); +} diff --git a/indra/newview/llfloatersettingsdebug.h b/indra/newview/llfloatersettingsdebug.h index b813cf4a74..8849766867 100644 --- a/indra/newview/llfloatersettingsdebug.h +++ b/indra/newview/llfloatersettingsdebug.h @@ -49,6 +49,7 @@ public: void onCommitSettings(); void onClickDefault(); + void onClickCopy(); bool matchesSearchFilter(std::string setting_name); bool isSettingHidden(LLControlVariable* control); @@ -77,7 +78,9 @@ protected: LLUICtrl* mBooleanCombo = nullptr; LLUICtrl* mValText = nullptr; LLUICtrl* mDefaultButton = nullptr; + LLTextEditor* mLLSDVal = nullptr; LLTextBox* mSettingNameText = nullptr; + LLButton* mCopyBtn = nullptr; LLColorSwatchCtrl* mColorSwatch = nullptr; diff --git a/indra/newview/llfloatertestinspectors.cpp b/indra/newview/llfloatertestinspectors.cpp index 7f8d122435..5b29dec6de 100644 --- a/indra/newview/llfloatertestinspectors.cpp +++ b/indra/newview/llfloatertestinspectors.cpp @@ -37,9 +37,9 @@ LLFloaterTestInspectors::LLFloaterTestInspectors(const LLSD& seed) : LLFloater(seed) { mCommitCallbackRegistrar.add("ShowAvatarInspector", - boost::bind(&LLFloaterTestInspectors::showAvatarInspector, this, _1, _2)); + { boost::bind(&LLFloaterTestInspectors::showAvatarInspector, this, _1, _2) }); mCommitCallbackRegistrar.add("ShowObjectInspector", - boost::bind(&LLFloaterTestInspectors::showObjectInspector, this, _1, _2)); + { boost::bind(&LLFloaterTestInspectors::showObjectInspector, this, _1, _2) }); } LLFloaterTestInspectors::~LLFloaterTestInspectors() diff --git a/indra/newview/llfloatertools.cpp b/indra/newview/llfloatertools.cpp index f6d8fcab36..5fe089aec5 100644 --- a/indra/newview/llfloatertools.cpp +++ b/indra/newview/llfloatertools.cpp @@ -387,21 +387,21 @@ LLFloaterTools::LLFloaterTools(const LLSD& key) mFactoryMap["Contents"] = LLCallbackMap(createPanelContents, this);//LLPanelContents mFactoryMap["land info panel"] = LLCallbackMap(createPanelLandInfo, this);//LLPanelLandInfo - mCommitCallbackRegistrar.add("BuildTool.setTool", boost::bind(&LLFloaterTools::setTool,this, _2)); - mCommitCallbackRegistrar.add("BuildTool.commitZoom", boost::bind(&commit_slider_zoom, _1)); - mCommitCallbackRegistrar.add("BuildTool.commitRadioFocus", boost::bind(&commit_radio_group_focus, _1)); - mCommitCallbackRegistrar.add("BuildTool.commitRadioMove", boost::bind(&commit_radio_group_move,_1)); - mCommitCallbackRegistrar.add("BuildTool.commitRadioEdit", boost::bind(&commit_radio_group_edit,_1)); - - mCommitCallbackRegistrar.add("BuildTool.gridMode", boost::bind(&commit_grid_mode,_1)); - mCommitCallbackRegistrar.add("BuildTool.selectComponent", boost::bind(&commit_select_component, this)); - mCommitCallbackRegistrar.add("BuildTool.gridOptions", boost::bind(&LLFloaterTools::onClickGridOptions,this)); - mCommitCallbackRegistrar.add("BuildTool.applyToSelection", boost::bind(&click_apply_to_selection, this)); - mCommitCallbackRegistrar.add("BuildTool.commitRadioLand", boost::bind(&commit_radio_group_land,_1)); - mCommitCallbackRegistrar.add("BuildTool.LandBrushForce", boost::bind(&commit_slider_dozer_force,_1)); - - mCommitCallbackRegistrar.add("BuildTool.LinkObjects", boost::bind(&LLSelectMgr::linkObjects, LLSelectMgr::getInstance())); - mCommitCallbackRegistrar.add("BuildTool.UnlinkObjects", boost::bind(&LLSelectMgr::unlinkObjects, LLSelectMgr::getInstance())); + mCommitCallbackRegistrar.add("BuildTool.setTool", { boost::bind(&LLFloaterTools::setTool,this, _2) }); + mCommitCallbackRegistrar.add("BuildTool.commitZoom", { boost::bind(&commit_slider_zoom, _1), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("BuildTool.commitRadioFocus", { boost::bind(&commit_radio_group_focus, _1), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("BuildTool.commitRadioMove", { boost::bind(&commit_radio_group_move,_1), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("BuildTool.commitRadioEdit", { boost::bind(&commit_radio_group_edit,_1), cb_info::UNTRUSTED_BLOCK }); + + mCommitCallbackRegistrar.add("BuildTool.gridMode", { boost::bind(&commit_grid_mode,_1), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("BuildTool.selectComponent", { boost::bind(&commit_select_component, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("BuildTool.gridOptions", { boost::bind(&LLFloaterTools::onClickGridOptions,this) }); + mCommitCallbackRegistrar.add("BuildTool.applyToSelection", { boost::bind(&click_apply_to_selection, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("BuildTool.commitRadioLand", { boost::bind(&commit_radio_group_land,_1), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("BuildTool.LandBrushForce", { boost::bind(&commit_slider_dozer_force,_1), cb_info::UNTRUSTED_BLOCK }); + + mCommitCallbackRegistrar.add("BuildTool.LinkObjects", { boost::bind(&LLSelectMgr::linkObjects, LLSelectMgr::getInstance()) }); + mCommitCallbackRegistrar.add("BuildTool.UnlinkObjects", { boost::bind(&LLSelectMgr::unlinkObjects, LLSelectMgr::getInstance()) }); mLandImpactsObserver = new LLLandImpactsObserver(); LLViewerParcelMgr::getInstance()->addObserver(mLandImpactsObserver); diff --git a/indra/newview/llfloatertopobjects.cpp b/indra/newview/llfloatertopobjects.cpp index 9bc8c63fa0..e379854160 100644 --- a/indra/newview/llfloatertopobjects.cpp +++ b/indra/newview/llfloatertopobjects.cpp @@ -76,16 +76,16 @@ LLFloaterTopObjects::LLFloaterTopObjects(const LLSD& key) mInitialized(false), mtotalScore(0.f) { - mCommitCallbackRegistrar.add("TopObjects.ShowBeacon", boost::bind(&LLFloaterTopObjects::onClickShowBeacon, this)); - mCommitCallbackRegistrar.add("TopObjects.ReturnSelected", boost::bind(&LLFloaterTopObjects::onReturnSelected, this)); - mCommitCallbackRegistrar.add("TopObjects.ReturnAll", boost::bind(&LLFloaterTopObjects::onReturnAll, this)); - mCommitCallbackRegistrar.add("TopObjects.Refresh", boost::bind(&LLFloaterTopObjects::onRefresh, this)); - mCommitCallbackRegistrar.add("TopObjects.GetByObjectName", boost::bind(&LLFloaterTopObjects::onGetByObjectName, this)); - mCommitCallbackRegistrar.add("TopObjects.GetByOwnerName", boost::bind(&LLFloaterTopObjects::onGetByOwnerName, this)); - mCommitCallbackRegistrar.add("TopObjects.GetByParcelName", boost::bind(&LLFloaterTopObjects::onGetByParcelName, this)); - mCommitCallbackRegistrar.add("TopObjects.CommitObjectsList",boost::bind(&LLFloaterTopObjects::onCommitObjectsList, this)); - - mCommitCallbackRegistrar.add("TopObjects.TeleportToSelected", boost::bind(&LLFloaterTopObjects::teleportToSelectedObject, this)); + mCommitCallbackRegistrar.add("TopObjects.ShowBeacon", { boost::bind(&LLFloaterTopObjects::onClickShowBeacon, this) }); + mCommitCallbackRegistrar.add("TopObjects.ReturnSelected", { boost::bind(&LLFloaterTopObjects::onReturnSelected, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("TopObjects.ReturnAll", { boost::bind(&LLFloaterTopObjects::onReturnAll, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("TopObjects.Refresh", { boost::bind(&LLFloaterTopObjects::onRefresh, this), cb_info::UNTRUSTED_THROTTLE }); + mCommitCallbackRegistrar.add("TopObjects.GetByObjectName", { boost::bind(&LLFloaterTopObjects::onGetByObjectName, this), cb_info::UNTRUSTED_THROTTLE }); + mCommitCallbackRegistrar.add("TopObjects.GetByOwnerName", { boost::bind(&LLFloaterTopObjects::onGetByOwnerName, this), cb_info::UNTRUSTED_THROTTLE }); + mCommitCallbackRegistrar.add("TopObjects.GetByParcelName", { boost::bind(&LLFloaterTopObjects::onGetByParcelName, this), cb_info::UNTRUSTED_THROTTLE }); + mCommitCallbackRegistrar.add("TopObjects.CommitObjectsList",{ boost::bind(&LLFloaterTopObjects::onCommitObjectsList, this), cb_info::UNTRUSTED_THROTTLE }); + + mCommitCallbackRegistrar.add("TopObjects.TeleportToSelected", { boost::bind(&LLFloaterTopObjects::teleportToSelectedObject, this), cb_info::UNTRUSTED_THROTTLE }); } LLFloaterTopObjects::~LLFloaterTopObjects() diff --git a/indra/newview/llfloatertoybox.cpp b/indra/newview/llfloatertoybox.cpp index f6257dbd3d..f56b2aa962 100644 --- a/indra/newview/llfloatertoybox.cpp +++ b/indra/newview/llfloatertoybox.cpp @@ -41,8 +41,8 @@ LLFloaterToybox::LLFloaterToybox(const LLSD& key) : LLFloater(key) , mToolBar(NULL) { - mCommitCallbackRegistrar.add("Toybox.RestoreDefaults", boost::bind(&LLFloaterToybox::onBtnRestoreDefaults, this)); - mCommitCallbackRegistrar.add("Toybox.ClearAll", boost::bind(&LLFloaterToybox::onBtnClearAll, this)); + mCommitCallbackRegistrar.add("Toybox.RestoreDefaults", { boost::bind(&LLFloaterToybox::onBtnRestoreDefaults, this) }); + mCommitCallbackRegistrar.add("Toybox.ClearAll", { boost::bind(&LLFloaterToybox::onBtnClearAll, this) }); } LLFloaterToybox::~LLFloaterToybox() diff --git a/indra/newview/llfloateruipreview.cpp b/indra/newview/llfloateruipreview.cpp index 990a299c50..12576c042b 100644 --- a/indra/newview/llfloateruipreview.cpp +++ b/indra/newview/llfloateruipreview.cpp @@ -253,7 +253,7 @@ class LLFadeEventTimer : public LLEventTimer { public: LLFadeEventTimer(F32 refresh, LLGUIPreviewLiveFile* parent); - bool tick(); + bool tick() override; LLGUIPreviewLiveFile* mParent; private: bool mFadingOut; // fades in then out; this is toggled in between diff --git a/indra/newview/llfloatervoiceeffect.cpp b/indra/newview/llfloatervoiceeffect.cpp index 9f7c9aba87..45b6616d1e 100644 --- a/indra/newview/llfloatervoiceeffect.cpp +++ b/indra/newview/llfloatervoiceeffect.cpp @@ -36,9 +36,9 @@ LLFloaterVoiceEffect::LLFloaterVoiceEffect(const LLSD& key) : LLFloater(key) { - mCommitCallbackRegistrar.add("VoiceEffect.Record", boost::bind(&LLFloaterVoiceEffect::onClickRecord, this)); - mCommitCallbackRegistrar.add("VoiceEffect.Play", boost::bind(&LLFloaterVoiceEffect::onClickPlay, this)); - mCommitCallbackRegistrar.add("VoiceEffect.Stop", boost::bind(&LLFloaterVoiceEffect::onClickStop, this)); + mCommitCallbackRegistrar.add("VoiceEffect.Record", { boost::bind(&LLFloaterVoiceEffect::onClickRecord, this) }); + mCommitCallbackRegistrar.add("VoiceEffect.Play", { boost::bind(&LLFloaterVoiceEffect::onClickPlay, this) }); + mCommitCallbackRegistrar.add("VoiceEffect.Stop", { boost::bind(&LLFloaterVoiceEffect::onClickStop, this) }); // mCommitCallbackRegistrar.add("VoiceEffect.Activate", boost::bind(&LLFloaterVoiceEffect::onClickActivate, this)); } diff --git a/indra/newview/llfloaterwebcontent.cpp b/indra/newview/llfloaterwebcontent.cpp index e1b6df6072..0cd20b7020 100644 --- a/indra/newview/llfloaterwebcontent.cpp +++ b/indra/newview/llfloaterwebcontent.cpp @@ -75,13 +75,13 @@ LLFloaterWebContent::LLFloaterWebContent( const Params& params ) mDisplayURL(""), mDevelopMode(params.dev_mode) // if called from "Develop" Menu, set a flag and change things to be more useful for devs { - mCommitCallbackRegistrar.add( "WebContent.Back", boost::bind( &LLFloaterWebContent::onClickBack, this )); - mCommitCallbackRegistrar.add( "WebContent.Forward", boost::bind( &LLFloaterWebContent::onClickForward, this )); - mCommitCallbackRegistrar.add( "WebContent.Reload", boost::bind( &LLFloaterWebContent::onClickReload, this )); - mCommitCallbackRegistrar.add( "WebContent.Stop", boost::bind( &LLFloaterWebContent::onClickStop, this )); - mCommitCallbackRegistrar.add( "WebContent.EnterAddress", boost::bind( &LLFloaterWebContent::onEnterAddress, this )); - mCommitCallbackRegistrar.add( "WebContent.PopExternal", boost::bind(&LLFloaterWebContent::onPopExternal, this)); - mCommitCallbackRegistrar.add( "WebContent.TestURL", boost::bind(&LLFloaterWebContent::onTestURL, this, _2)); + mCommitCallbackRegistrar.add( "WebContent.Back", { boost::bind( &LLFloaterWebContent::onClickBack, this ), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add( "WebContent.Forward", { boost::bind( &LLFloaterWebContent::onClickForward, this ), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add( "WebContent.Reload", { boost::bind( &LLFloaterWebContent::onClickReload, this ), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add( "WebContent.Stop", { boost::bind( &LLFloaterWebContent::onClickStop, this ), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add( "WebContent.EnterAddress", { boost::bind( &LLFloaterWebContent::onEnterAddress, this ), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add( "WebContent.PopExternal", { boost::bind(&LLFloaterWebContent::onPopExternal, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add( "WebContent.TestURL", { boost::bind(&LLFloaterWebContent::onTestURL, this, _2), cb_info::UNTRUSTED_BLOCK }); } bool LLFloaterWebContent::postBuild() diff --git a/indra/newview/llfloaterworldmap.cpp b/indra/newview/llfloaterworldmap.cpp index 30ed723db6..dda7266220 100755 --- a/indra/newview/llfloaterworldmap.cpp +++ b/indra/newview/llfloaterworldmap.cpp @@ -337,17 +337,17 @@ LLFloaterWorldMap::LLFloaterWorldMap(const LLSD& key) mFactoryMap["objects_mapview"] = LLCallbackMap(createWorldMapView, nullptr); - mCommitCallbackRegistrar.add("WMap.Coordinates", boost::bind(&LLFloaterWorldMap::onCoordinatesCommit, this)); - mCommitCallbackRegistrar.add("WMap.Location", boost::bind(&LLFloaterWorldMap::onLocationCommit, this)); - mCommitCallbackRegistrar.add("WMap.AvatarCombo", boost::bind(&LLFloaterWorldMap::onAvatarComboCommit, this)); - mCommitCallbackRegistrar.add("WMap.Landmark", boost::bind(&LLFloaterWorldMap::onLandmarkComboCommit, this)); - mCommitCallbackRegistrar.add("WMap.SearchResult", boost::bind(&LLFloaterWorldMap::onCommitSearchResult, this)); - mCommitCallbackRegistrar.add("WMap.GoHome", boost::bind(&LLFloaterWorldMap::onGoHome, this)); - mCommitCallbackRegistrar.add("WMap.Teleport", boost::bind(&LLFloaterWorldMap::onClickTeleportBtn, this)); - mCommitCallbackRegistrar.add("WMap.ShowTarget", boost::bind(&LLFloaterWorldMap::onShowTargetBtn, this)); - mCommitCallbackRegistrar.add("WMap.ShowAgent", boost::bind(&LLFloaterWorldMap::onShowAgentBtn, this)); - mCommitCallbackRegistrar.add("WMap.Clear", boost::bind(&LLFloaterWorldMap::onClearBtn, this)); - mCommitCallbackRegistrar.add("WMap.CopySLURL", boost::bind(&LLFloaterWorldMap::onCopySLURL, this)); + mCommitCallbackRegistrar.add("WMap.Coordinates", { boost::bind(&LLFloaterWorldMap::onCoordinatesCommit, this) }); + mCommitCallbackRegistrar.add("WMap.Location", { boost::bind(&LLFloaterWorldMap::onLocationCommit, this) }); + mCommitCallbackRegistrar.add("WMap.AvatarCombo", { boost::bind(&LLFloaterWorldMap::onAvatarComboCommit, this) }); + mCommitCallbackRegistrar.add("WMap.Landmark", { boost::bind(&LLFloaterWorldMap::onLandmarkComboCommit, this) }); + mCommitCallbackRegistrar.add("WMap.SearchResult", { boost::bind(&LLFloaterWorldMap::onCommitSearchResult, this) }); + mCommitCallbackRegistrar.add("WMap.GoHome", { boost::bind(&LLFloaterWorldMap::onGoHome, this) }); + mCommitCallbackRegistrar.add("WMap.Teleport", { boost::bind(&LLFloaterWorldMap::onClickTeleportBtn, this) }); + mCommitCallbackRegistrar.add("WMap.ShowTarget", { boost::bind(&LLFloaterWorldMap::onShowTargetBtn, this) }); + mCommitCallbackRegistrar.add("WMap.ShowAgent", { boost::bind(&LLFloaterWorldMap::onShowAgentBtn, this) }); + mCommitCallbackRegistrar.add("WMap.Clear", { boost::bind(&LLFloaterWorldMap::onClearBtn, this) }); + mCommitCallbackRegistrar.add("WMap.CopySLURL", { boost::bind(&LLFloaterWorldMap::onCopySLURL, this) }); gSavedSettings.getControl("PreferredMaturity")->getSignal()->connect(boost::bind(&LLFloaterWorldMap::onChangeMaturity, this)); } diff --git a/indra/newview/llflyoutcombobtn.cpp b/indra/newview/llflyoutcombobtn.cpp index 728f60e1c0..97cd637da1 100644 --- a/indra/newview/llflyoutcombobtn.cpp +++ b/indra/newview/llflyoutcombobtn.cpp @@ -41,7 +41,7 @@ LLFlyoutComboBtnCtrl::LLFlyoutComboBtnCtrl(LLPanel* parent, { // register action mapping before creating menu LLUICtrl::CommitCallbackRegistry::ScopedRegistrar save_registar; - save_registar.add("FlyoutCombo.Button.Action", [this](LLUICtrl *ctrl, const LLSD &data) { onFlyoutItemSelected(ctrl, data); }); + save_registar.add("FlyoutCombo.Button.Action", { [this](LLUICtrl *ctrl, const LLSD &data) { onFlyoutItemSelected(ctrl, data); } }); LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enabled_rgistar; enabled_rgistar.add("FlyoutCombo.Button.Check", [this](LLUICtrl *ctrl, const LLSD &data) { return onFlyoutItemCheck(ctrl, data); }); diff --git a/indra/newview/llgroupactions.cpp b/indra/newview/llgroupactions.cpp index ba9c9fa13f..1128939e29 100644 --- a/indra/newview/llgroupactions.cpp +++ b/indra/newview/llgroupactions.cpp @@ -37,6 +37,7 @@ #include "llfloatersidepanelcontainer.h" #include "llgroupmgr.h" #include "llfloaterimcontainer.h" +#include "llfloaterimsession.h" #include "llimview.h" // for gIMMgr #include "llnotificationsutil.h" #include "llstartup.h" @@ -46,7 +47,7 @@ // // Globals // -static GroupChatListener sGroupChatListener; +static LLGroupChatListener sGroupChatListener; class LLGroupHandler : public LLCommandHandler { @@ -519,7 +520,10 @@ void LLGroupActions::endIM(const LLUUID& group_id) LLUUID session_id = gIMMgr->computeSessionID(IM_SESSION_GROUP_START, group_id); if (session_id != LLUUID::null) { - gIMMgr->leaveSession(session_id); + if (LLFloaterIMSession *conversationFloater = LLFloaterIMSession::findInstance(session_id)) + { + LLFloater::onClickClose(conversationFloater); + } } } diff --git a/indra/newview/llgrouplist.cpp b/indra/newview/llgrouplist.cpp index 7659e5f082..14ac14cd85 100644 --- a/indra/newview/llgrouplist.cpp +++ b/indra/newview/llgrouplist.cpp @@ -141,7 +141,7 @@ void LLGroupList::enableForAgent(bool show_icons) gAgent.addListener(this, "new group"); // Set up context menu. - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + ScopedRegistrarHelper registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; registrar.add("People.Groups.Action", boost::bind(&LLGroupList::onContextMenuItemClick, this, _2)); diff --git a/indra/newview/llimview.h b/indra/newview/llimview.h index 99b19c9fa9..233fb075e8 100644 --- a/indra/newview/llimview.h +++ b/indra/newview/llimview.h @@ -51,7 +51,7 @@ class LLSessionTimeoutTimer : public LLEventTimer public: LLSessionTimeoutTimer(const LLUUID& session_id, F32 period) : LLEventTimer(period), mSessionId(session_id) {} virtual ~LLSessionTimeoutTimer() {}; - /* virtual */ bool tick(); + bool tick() override; private: LLUUID mSessionId; diff --git a/indra/newview/llinspectgroup.cpp b/indra/newview/llinspectgroup.cpp index db48061c56..c39380105b 100644 --- a/indra/newview/llinspectgroup.cpp +++ b/indra/newview/llinspectgroup.cpp @@ -97,11 +97,11 @@ LLInspectGroup::LLInspectGroup(const LLSD& sd) mGroupID() // set in onOpen() { mCommitCallbackRegistrar.add("InspectGroup.ViewProfile", - boost::bind(&LLInspectGroup::onClickViewProfile, this)); + { boost::bind(&LLInspectGroup::onClickViewProfile, this), cb_info::UNTRUSTED_THROTTLE }); mCommitCallbackRegistrar.add("InspectGroup.Join", - boost::bind(&LLInspectGroup::onClickJoin, this)); + { boost::bind(&LLInspectGroup::onClickJoin, this), cb_info::UNTRUSTED_THROTTLE }); mCommitCallbackRegistrar.add("InspectGroup.Leave", - boost::bind(&LLInspectGroup::onClickLeave, this)); + { boost::bind(&LLInspectGroup::onClickLeave, this), cb_info::UNTRUSTED_BLOCK }); // can't make the properties request until the widgets are constructed // as it might return immediately, so do it in postBuild. diff --git a/indra/newview/llinspectobject.cpp b/indra/newview/llinspectobject.cpp index eb2cdb8632..874ad41551 100644 --- a/indra/newview/llinspectobject.cpp +++ b/indra/newview/llinspectobject.cpp @@ -129,14 +129,14 @@ LLInspectObject::LLInspectObject(const LLSD& sd) { // can't make the properties request until the widgets are constructed // as it might return immediately, so do it in postBuild. - mCommitCallbackRegistrar.add("InspectObject.Buy", boost::bind(&LLInspectObject::onClickBuy, this)); - mCommitCallbackRegistrar.add("InspectObject.Pay", boost::bind(&LLInspectObject::onClickPay, this)); - mCommitCallbackRegistrar.add("InspectObject.TakeFreeCopy", boost::bind(&LLInspectObject::onClickTakeFreeCopy, this)); - mCommitCallbackRegistrar.add("InspectObject.Touch", boost::bind(&LLInspectObject::onClickTouch, this)); - mCommitCallbackRegistrar.add("InspectObject.Sit", boost::bind(&LLInspectObject::onClickSit, this)); - mCommitCallbackRegistrar.add("InspectObject.Open", boost::bind(&LLInspectObject::onClickOpen, this)); - mCommitCallbackRegistrar.add("InspectObject.MoreInfo", boost::bind(&LLInspectObject::onClickMoreInfo, this)); - mCommitCallbackRegistrar.add("InspectObject.ZoomIn", boost::bind(&LLInspectObject::onClickZoomIn, this)); + mCommitCallbackRegistrar.add("InspectObject.Buy", {boost::bind(&LLInspectObject::onClickBuy, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("InspectObject.Pay", {boost::bind(&LLInspectObject::onClickPay, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("InspectObject.TakeFreeCopy", {boost::bind(&LLInspectObject::onClickTakeFreeCopy, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("InspectObject.Touch", {boost::bind(&LLInspectObject::onClickTouch, this), cb_info::UNTRUSTED_BLOCK}); + mCommitCallbackRegistrar.add("InspectObject.Sit", {boost::bind(&LLInspectObject::onClickSit, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("InspectObject.Open", {boost::bind(&LLInspectObject::onClickOpen, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("InspectObject.MoreInfo", {boost::bind(&LLInspectObject::onClickMoreInfo, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("InspectObject.ZoomIn", {boost::bind(&LLInspectObject::onClickZoomIn, this), cb_info::UNTRUSTED_BLOCK}); } diff --git a/indra/newview/llinventoryfunctions.cpp b/indra/newview/llinventoryfunctions.cpp index dadd0590a9..fdf77ab8df 100644 --- a/indra/newview/llinventoryfunctions.cpp +++ b/indra/newview/llinventoryfunctions.cpp @@ -2758,6 +2758,19 @@ bool LLNameCategoryCollector::operator()( return false; } +bool LLNameItemCollector::operator()( + LLInventoryCategory* cat, LLInventoryItem* item) +{ + if(item) + { + if (!LLStringUtil::compareInsensitive(mName, item->getName())) + { + return true; + } + } + return false; +} + bool LLFindCOFValidItems::operator()(LLInventoryCategory* cat, LLInventoryItem* item) { diff --git a/indra/newview/llinventoryfunctions.h b/indra/newview/llinventoryfunctions.h index a25c0d5ad6..cd6f319f66 100644 --- a/indra/newview/llinventoryfunctions.h +++ b/indra/newview/llinventoryfunctions.h @@ -189,6 +189,8 @@ public: virtual ~LLInventoryCollectFunctor(){}; virtual bool operator()(LLInventoryCategory* cat, LLInventoryItem* item) = 0; + virtual bool exceedsLimit() { return false; } + static bool itemTransferCommonlyAllowed(const LLInventoryItem* item); }; @@ -392,6 +394,22 @@ protected: }; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Class LLNameItemCollector +// +// Collects items based on case-insensitive match of prefix +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +class LLNameItemCollector : public LLInventoryCollectFunctor +{ +public: + LLNameItemCollector(const std::string& name) : mName(name) {} + virtual ~LLNameItemCollector() {} + virtual bool operator()(LLInventoryCategory* cat, + LLInventoryItem* item); +protected: + std::string mName; +}; + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Class LLFindCOFValidItems // // Collects items that can be legitimately linked to in the COF. diff --git a/indra/newview/llinventorygallerymenu.cpp b/indra/newview/llinventorygallerymenu.cpp index dbf4821ca1..c84b9abba0 100644 --- a/indra/newview/llinventorygallerymenu.cpp +++ b/indra/newview/llinventorygallerymenu.cpp @@ -87,13 +87,15 @@ void modify_outfit(bool append, const LLUUID& cat_id, LLInventoryModel* model) LLContextMenu* LLInventoryGalleryContextMenu::createMenu() { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + LLUICtrl::ScopedRegistrarHelper registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; - registrar.add("Inventory.DoToSelected", boost::bind(&LLInventoryGalleryContextMenu::doToSelected, this, _2)); - registrar.add("Inventory.FileUploadLocation", boost::bind(&LLInventoryGalleryContextMenu::fileUploadLocation, this, _2)); - registrar.add("Inventory.EmptyTrash", boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyTrash", LLFolderType::FT_TRASH)); - registrar.add("Inventory.EmptyLostAndFound", boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyLostAndFound", LLFolderType::FT_LOST_AND_FOUND)); + registrar.add("Inventory.DoToSelected", boost::bind(&LLInventoryGalleryContextMenu::doToSelected, this, _2), LLUICtrl::cb_info::UNTRUSTED_BLOCK); + registrar.add("Inventory.FileUploadLocation", boost::bind(&LLInventoryGalleryContextMenu::fileUploadLocation, this, _2), LLUICtrl::cb_info::UNTRUSTED_BLOCK); + registrar.add("Inventory.EmptyTrash", + boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyTrash", LLFolderType::FT_TRASH), LLUICtrl::cb_info::UNTRUSTED_BLOCK); + registrar.add("Inventory.EmptyLostAndFound", + boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyLostAndFound", LLFolderType::FT_LOST_AND_FOUND), LLUICtrl::cb_info::UNTRUSTED_BLOCK); registrar.add("Inventory.DoCreate", [this](LLUICtrl*, const LLSD& data) { if (mRootFolder) @@ -104,10 +106,11 @@ LLContextMenu* LLInventoryGalleryContextMenu::createMenu() { mGallery->doCreate(mUUIDs.front(), data); } - }); + }, + LLUICtrl::cb_info::UNTRUSTED_BLOCK); std::set<LLUUID> uuids(mUUIDs.begin(), mUUIDs.end()); - registrar.add("Inventory.Share", boost::bind(&LLAvatarActions::shareWithAvatars, uuids, gFloaterView->getParentFloater(mGallery))); + registrar.add("Inventory.Share", boost::bind(&LLAvatarActions::shareWithAvatars, uuids, gFloaterView->getParentFloater(mGallery)), LLUICtrl::cb_info::UNTRUSTED_BLOCK); enable_registrar.add("Inventory.CanSetUploadLocation", boost::bind(&LLInventoryGalleryContextMenu::canSetUploadLocation, this, _2)); diff --git a/indra/newview/llinventorylistener.cpp b/indra/newview/llinventorylistener.cpp new file mode 100644 index 0000000000..79726c3e0b --- /dev/null +++ b/indra/newview/llinventorylistener.cpp @@ -0,0 +1,392 @@ +/** + * @file llinventorylistener.cpp + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2024, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llinventorylistener.h" + +#include "llappearancemgr.h" +#include "llinventoryfunctions.h" +#include "lltransutil.h" +#include "llwearableitemslist.h" +#include "resultset.h" +#include "stringize.h" +#include <algorithm> // std::min() + +constexpr S32 MAX_ITEM_LIMIT = 100; + +LLInventoryListener::LLInventoryListener() + : LLEventAPI("LLInventory", + "API for interactions with viewer Inventory items") +{ + add("getItemsInfo", + "Return information about items or folders defined in [\"item_ids\"]:\n" + "reply will contain [\"items\"] and [\"categories\"] result set keys", + &LLInventoryListener::getItemsInfo, + llsd::map("item_ids", LLSD(), "reply", LLSD())); + + add("getFolderTypeNames", + "Return the table of folder type names, contained in [\"names\"]\n", + &LLInventoryListener::getFolderTypeNames, + llsd::map("reply", LLSD())); + + add("getAssetTypeNames", + "Return the table of asset type names, contained in [\"names\"]\n", + &LLInventoryListener::getAssetTypeNames, + llsd::map("reply", LLSD())); + + add("getBasicFolderID", + "Return the UUID of the folder by specified folder type name, for example:\n" + "\"Textures\", \"My outfits\", \"Sounds\" and other basic folders which have associated type", + &LLInventoryListener::getBasicFolderID, + llsd::map("ft_name", LLSD(), "reply", LLSD())); + + add("getDirectDescendants", + "Return result set keys [\"categories\"] and [\"items\"] for the direct\n" + "descendants of the [\"folder_id\"]", + &LLInventoryListener::getDirectDescendants, + llsd::map("folder_id", LLSD(), "reply", LLSD())); + + add("collectDescendantsIf", + "Return result set keys [\"categories\"] and [\"items\"] for the descendants\n" + "of the [\"folder_id\"], if it passes specified filters:\n" + "[\"name\"] is a substring of object's name,\n" + "[\"desc\"] is a substring of object's description,\n" + "asset [\"type\"] corresponds to the string name of the object's asset type\n" + "[\"limit\"] sets item count limit in result set (default unlimited)\n" + "[\"filter_links\"]: EXCLUDE_LINKS - don't show links, ONLY_LINKS - only show links, INCLUDE_LINKS - show links too (default)", + &LLInventoryListener::collectDescendantsIf, + llsd::map("folder_id", LLSD(), "reply", LLSD())); + +/*==========================================================================*| + add("getSingle", + "Return LLSD [\"single\"] for a single folder or item from the specified\n" + "[\"result\"] key at the specified 0-relative [\"index\"].", + &LLInventoryListener::getSingle, + llsd::map("result", LLSD::Integer(), "index", LLSD::Integer(), + "reply", LLSD::String())); +|*==========================================================================*/ + + add("getSlice", + stringize( + "Return an LLSD array [\"slice\"] from the specified [\"result\"] key\n" + "starting at 0-relative [\"index\"] with (up to) [\"count\"] entries.\n" + "count is limited to ", MAX_ITEM_LIMIT, " (default and max)."), + &LLInventoryListener::getSlice, + llsd::map("result", LLSD::Integer(), "index", LLSD::Integer(), + "reply", LLSD::String())); + + add("closeResult", + "Release resources associated with specified [\"result\"] key,\n" + "or keys if [\"result\"] is an array.", + &LLInventoryListener::closeResult, + llsd::map("result", LLSD())); +} + +// This struct captures (possibly large) category results from +// getDirectDescendants() and collectDescendantsIf(). +struct CatResultSet: public LL::ResultSet +{ + CatResultSet(): LL::ResultSet("categories") {} + LLInventoryModel::cat_array_t mCategories; + + int getLength() const override { return narrow(mCategories.size()); } + LLSD getSingle(int index) const override + { + auto cat = mCategories[index]; + return llsd::map("name", cat->getName(), + "parent_id", cat->getParentUUID(), + "type", LLFolderType::lookup(cat->getPreferredType())); + } +}; + +// This struct captures (possibly large) item results from +// getDirectDescendants() and collectDescendantsIf(). +struct ItemResultSet: public LL::ResultSet +{ + ItemResultSet(): LL::ResultSet("items") {} + LLInventoryModel::item_array_t mItems; + + int getLength() const override { return narrow(mItems.size()); } + LLSD getSingle(int index) const override + { + auto item = mItems[index]; + return llsd::map("name", item->getName(), + "parent_id", item->getParentUUID(), + "desc", item->getDescription(), + "inv_type", LLInventoryType::lookup(item->getInventoryType()), + "asset_type", LLAssetType::lookup(item->getType()), + "creation_date", LLSD::Integer(item->getCreationDate()), + "asset_id", item->getAssetUUID(), + "is_link", item->getIsLinkType(), + "linked_id", item->getLinkedUUID()); + } +}; + +void LLInventoryListener::getItemsInfo(LLSD const &data) +{ + Response response(LLSD(), data); + + auto catresult = new CatResultSet; + auto itemresult = new ItemResultSet; + + uuid_vec_t ids = LLSDParam<uuid_vec_t>(data["item_ids"]); + for (auto &it : ids) + { + LLViewerInventoryItem* item = gInventory.getItem(it); + if (item) + { + itemresult->mItems.push_back(item); + } + else + { + LLViewerInventoryCategory *cat = gInventory.getCategory(it); + if (cat) + { + catresult->mCategories.push_back(cat); + } + } + } + // Each of categories and items is a { result set key, total length } pair. + response["categories"] = catresult->getKeyLength(); + response["items"] = itemresult->getKeyLength(); +} + +void LLInventoryListener::getFolderTypeNames(LLSD const &data) +{ + Response response(llsd::map("names", LLFolderType::getTypeNames()), data); +} + +void LLInventoryListener::getAssetTypeNames(LLSD const &data) +{ + Response response(llsd::map("names", LLAssetType::getTypeNames()), data); +} + +void LLInventoryListener::getBasicFolderID(LLSD const &data) +{ + Response response(llsd::map("id", gInventory.findCategoryUUIDForType(LLFolderType::lookup(data["ft_name"].asString()))), data); +} + + +void LLInventoryListener::getDirectDescendants(LLSD const &data) +{ + Response response(LLSD(), data); + LLInventoryModel::cat_array_t* cats; + LLInventoryModel::item_array_t* items; + gInventory.getDirectDescendentsOf(data["folder_id"], cats, items); + + auto catresult = new CatResultSet; + auto itemresult = new ItemResultSet; + + catresult->mCategories = *cats; + itemresult->mItems = *items; + + response["categories"] = catresult->getKeyLength(); + response["items"] = itemresult->getKeyLength(); +} + +struct LLFilteredCollector : public LLInventoryCollectFunctor +{ + enum EFilterLink + { + INCLUDE_LINKS, // show links too + EXCLUDE_LINKS, // don't show links + ONLY_LINKS // only show links + }; + + LLFilteredCollector(LLSD const &data); + virtual ~LLFilteredCollector() {} + virtual bool operator()(LLInventoryCategory *cat, LLInventoryItem *item) override; + virtual bool exceedsLimit() override + { + // mItemLimit == 0 means unlimited + return (mItemLimit && mItemLimit <= mItemCount); + } + + protected: + bool checkagainstType(LLInventoryCategory *cat, LLInventoryItem *item); + bool checkagainstNameDesc(LLInventoryCategory *cat, LLInventoryItem *item); + bool checkagainstLinks(LLInventoryCategory *cat, LLInventoryItem *item); + + LLAssetType::EType mType; + std::string mName; + std::string mDesc; + EFilterLink mLinkFilter; + + S32 mItemLimit; + S32 mItemCount; +}; + +void LLInventoryListener::collectDescendantsIf(LLSD const &data) +{ + Response response(LLSD(), data); + LLUUID folder_id(data["folder_id"].asUUID()); + LLViewerInventoryCategory *cat = gInventory.getCategory(folder_id); + if (!cat) + { + return response.error(stringize("Folder ", std::quoted(data["folder_id"].asString()), " was not found")); + } + auto catresult = new CatResultSet; + auto itemresult = new ItemResultSet; + + LLFilteredCollector collector = LLFilteredCollector(data); + + // Populate results directly into the catresult and itemresult arrays. + // TODO: sprinkle count-based coroutine yields into the real + // collectDescendentsIf() method so it doesn't steal too many cycles. + gInventory.collectDescendentsIf( + folder_id, + catresult->mCategories, + itemresult->mItems, + LLInventoryModel::EXCLUDE_TRASH, + collector); + + response["categories"] = catresult->getKeyLength(); + response["items"] = itemresult->getKeyLength(); +} + +/*==========================================================================*| +void LLInventoryListener::getSingle(LLSD const& data) +{ + auto result = LL::ResultSet::getInstance(data["result"]); + sendReply(llsd::map("single", result->getSingle(data["index"])), data); +} +|*==========================================================================*/ + +void LLInventoryListener::getSlice(LLSD const& data) +{ + auto result = LL::ResultSet::getInstance(data["result"]); + int count = data.has("count")? data["count"].asInteger() : MAX_ITEM_LIMIT; + LL_DEBUGS("Lua") << *result << ".getSlice(" << data["index"].asInteger() + << ", " << count << ')' << LL_ENDL; + auto pair{ result->getSliceStart(data["index"], std::min(count, MAX_ITEM_LIMIT)) }; + sendReply(llsd::map("slice", pair.first, "start", pair.second), data); +} + +void LLInventoryListener::closeResult(LLSD const& data) +{ + LLSD results = data["result"]; + if (results.isInteger()) + { + results = llsd::array(results); + } + for (const auto& result : llsd::inArray(results)) + { + auto ptr = LL::ResultSet::getInstance(result); + if (ptr) + { + delete ptr.get(); + } + } +} + +LLFilteredCollector::LLFilteredCollector(LLSD const &data) : + mType(LLAssetType::EType::AT_UNKNOWN), + mLinkFilter(INCLUDE_LINKS), + mItemLimit(0), + mItemCount(0) +{ + + mName = data["name"].asString(); + mDesc = data["desc"].asString(); + + if (data.has("type")) + { + mType = LLAssetType::lookup(data["type"]); + } + if (data.has("filter_links")) + { + if (data["filter_links"] == "EXCLUDE_LINKS") + { + mLinkFilter = EXCLUDE_LINKS; + } + else if (data["filter_links"] == "ONLY_LINKS") + { + mLinkFilter = ONLY_LINKS; + } + } + if (data["limit"].isInteger()) + { + mItemLimit = std::max(data["limit"].asInteger(), 1); + } +} + +bool LLFilteredCollector::operator()(LLInventoryCategory *cat, LLInventoryItem *item) +{ + bool passed = checkagainstType(cat, item); + passed = passed && checkagainstNameDesc(cat, item); + passed = passed && checkagainstLinks(cat, item); + + if (passed) + { + ++mItemCount; + } + return passed; +} + +bool LLFilteredCollector::checkagainstNameDesc(LLInventoryCategory *cat, LLInventoryItem *item) +{ + std::string name, desc; + bool passed(true); + if (cat) + { + if (!mDesc.empty()) return false; + name = cat->getName(); + } + if (item) + { + name = item->getName(); + passed = (mDesc.empty() || (item->getDescription().find(mDesc) != std::string::npos)); + } + + return passed && (mName.empty() || name.find(mName) != std::string::npos); +} + +bool LLFilteredCollector::checkagainstType(LLInventoryCategory *cat, LLInventoryItem *item) +{ + if (mType == LLAssetType::AT_UNKNOWN) + { + return true; + } + if (cat && (mType == LLAssetType::AT_CATEGORY)) + { + return true; + } + if (item && item->getType() == mType) + { + return true; + } + return false; +} + +bool LLFilteredCollector::checkagainstLinks(LLInventoryCategory *cat, LLInventoryItem *item) +{ + bool is_link = cat ? cat->getIsLinkType() : item->getIsLinkType(); + if (is_link && (mLinkFilter == EXCLUDE_LINKS)) + return false; + if (!is_link && (mLinkFilter == ONLY_LINKS)) + return false; + return true; +} diff --git a/indra/newview/llinventorylistener.h b/indra/newview/llinventorylistener.h new file mode 100644 index 0000000000..a05385f2c8 --- /dev/null +++ b/indra/newview/llinventorylistener.h @@ -0,0 +1,53 @@ +/** + * @file llinventorylistener.h + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2024, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + + +#ifndef LL_LLINVENTORYLISTENER_H +#define LL_LLINVENTORYLISTENER_H + +#include "lleventapi.h" +#include "llinventoryfunctions.h" + +class LLInventoryListener : public LLEventAPI +{ +public: + LLInventoryListener(); + +private: + void getItemsInfo(LLSD const &data); + void getFolderTypeNames(LLSD const &data); + void getAssetTypeNames(LLSD const &data); + void getBasicFolderID(LLSD const &data); + void getDirectDescendants(LLSD const &data); + void collectDescendantsIf(LLSD const &data); +/*==========================================================================*| + void getSingle(LLSD const& data); +|*==========================================================================*/ + void getSlice(LLSD const& data); + void closeResult(LLSD const& data); +}; + +#endif // LL_LLINVENTORYLISTENER_H + diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp index d57cb13362..70a0d078fe 100644 --- a/indra/newview/llinventorymodel.cpp +++ b/indra/newview/llinventorymodel.cpp @@ -1282,6 +1282,10 @@ void LLInventoryModel::collectDescendentsIf(const LLUUID& id, { for (auto& cat : *cat_array) { + if (add.exceedsLimit()) + { + break; + } if(add(cat,NULL)) { cats.push_back(cat); @@ -1297,6 +1301,10 @@ void LLInventoryModel::collectDescendentsIf(const LLUUID& id, { for (auto& item : *item_array) { + if (add.exceedsLimit()) + { + break; + } if(add(NULL, item)) { items.push_back(item); @@ -4824,6 +4832,17 @@ std::string LLInventoryModel::getFullPath(const LLInventoryObject *obj) const return result; } +/* +const LLInventoryObject* LLInventoryModel::findByFullPath(const std::string& path) +{ + vector<std::string> path_elts; + boost::algorithm::split(path_elts, path, boost::is_any_of("/")); + for(path_elts, auto e) + { + } +} +*/ + ///---------------------------------------------------------------------------- /// Local function definitions ///---------------------------------------------------------------------------- diff --git a/indra/newview/llinventorypanel.cpp b/indra/newview/llinventorypanel.cpp index e4d1010231..d08cd91382 100644 --- a/indra/newview/llinventorypanel.cpp +++ b/indra/newview/llinventorypanel.cpp @@ -177,15 +177,15 @@ LLInventoryPanel::LLInventoryPanel(const LLInventoryPanel::Params& p) : } // context menu callbacks - mCommitCallbackRegistrar.add("Inventory.DoToSelected", boost::bind(&LLInventoryPanel::doToSelected, this, _2)); - mCommitCallbackRegistrar.add("Inventory.EmptyTrash", boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyTrash", LLFolderType::FT_TRASH)); - mCommitCallbackRegistrar.add("Inventory.EmptyLostAndFound", boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyLostAndFound", LLFolderType::FT_LOST_AND_FOUND)); - mCommitCallbackRegistrar.add("Inventory.DoCreate", boost::bind(&LLInventoryPanel::doCreate, this, _2)); - mCommitCallbackRegistrar.add("Inventory.AttachObject", boost::bind(&LLInventoryPanel::attachObject, this, _2)); - mCommitCallbackRegistrar.add("Inventory.BeginIMSession", boost::bind(&LLInventoryPanel::beginIMSession, this)); - mCommitCallbackRegistrar.add("Inventory.Share", boost::bind(&LLAvatarActions::shareWithAvatars, this)); - mCommitCallbackRegistrar.add("Inventory.FileUploadLocation", boost::bind(&LLInventoryPanel::fileUploadLocation, this, _2)); - mCommitCallbackRegistrar.add("Inventory.OpenNewFolderWindow", boost::bind(&LLInventoryPanel::openSingleViewInventory, this, LLUUID())); + mCommitCallbackRegistrar.add("Inventory.DoToSelected", { boost::bind(&LLInventoryPanel::doToSelected, this, _2), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.EmptyTrash", { boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyTrash", LLFolderType::FT_TRASH), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.EmptyLostAndFound", { boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyLostAndFound", LLFolderType::FT_LOST_AND_FOUND), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.DoCreate", { boost::bind(&LLInventoryPanel::doCreate, this, _2), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.AttachObject", { boost::bind(&LLInventoryPanel::attachObject, this, _2), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.BeginIMSession", { boost::bind(&LLInventoryPanel::beginIMSession, this), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.Share", { boost::bind(&LLAvatarActions::shareWithAvatars, this), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.FileUploadLocation", { boost::bind(&LLInventoryPanel::fileUploadLocation, this, _2), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.OpenNewFolderWindow", { boost::bind(&LLInventoryPanel::openSingleViewInventory, this, LLUUID()), LLUICtrl::cb_info::UNTRUSTED_THROTTLE }); } LLFolderView * LLInventoryPanel::createFolderRoot(LLUUID root_id ) @@ -2214,9 +2214,9 @@ LLInventorySingleFolderPanel::LLInventorySingleFolderPanel(const Params& params) getFilter().setEmptyLookupMessage("InventorySingleFolderNoMatches"); getFilter().setDefaultEmptyLookupMessage("InventorySingleFolderEmpty"); - mCommitCallbackRegistrar.replace("Inventory.DoToSelected", boost::bind(&LLInventorySingleFolderPanel::doToSelected, this, _2)); - mCommitCallbackRegistrar.replace("Inventory.DoCreate", boost::bind(&LLInventorySingleFolderPanel::doCreate, this, _2)); - mCommitCallbackRegistrar.replace("Inventory.Share", boost::bind(&LLInventorySingleFolderPanel::doShare, this)); + mCommitCallbackRegistrar.replace("Inventory.DoToSelected", { boost::bind(&LLInventorySingleFolderPanel::doToSelected, this, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.replace("Inventory.DoCreate", { boost::bind(&LLInventorySingleFolderPanel::doCreate, this, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.replace("Inventory.Share", { boost::bind(&LLInventorySingleFolderPanel::doShare, this), cb_info::UNTRUSTED_BLOCK }); } LLInventorySingleFolderPanel::~LLInventorySingleFolderPanel() diff --git a/indra/newview/lllocalbitmaps.cpp b/indra/newview/lllocalbitmaps.cpp index 31c9eb8966..81b2d23cf7 100644 --- a/indra/newview/lllocalbitmaps.cpp +++ b/indra/newview/lllocalbitmaps.cpp @@ -996,17 +996,12 @@ LLLocalBitmapTimer::~LLLocalBitmapTimer() void LLLocalBitmapTimer::startTimer() { - mEventTimer.start(); + start(); } void LLLocalBitmapTimer::stopTimer() { - mEventTimer.stop(); -} - -bool LLLocalBitmapTimer::isRunning() -{ - return mEventTimer.getStarted(); + stop(); } bool LLLocalBitmapTimer::tick() diff --git a/indra/newview/lllocalbitmaps.h b/indra/newview/lllocalbitmaps.h index e169f96e70..4bad9ff9a5 100644 --- a/indra/newview/lllocalbitmaps.h +++ b/indra/newview/lllocalbitmaps.h @@ -120,8 +120,7 @@ class LLLocalBitmapTimer : public LLEventTimer public: void startTimer(); void stopTimer(); - bool isRunning(); - bool tick(); + bool tick() override; }; diff --git a/indra/newview/lllocalgltfmaterials.cpp b/indra/newview/lllocalgltfmaterials.cpp index fab18f2d26..3af1433a40 100644 --- a/indra/newview/lllocalgltfmaterials.cpp +++ b/indra/newview/lllocalgltfmaterials.cpp @@ -294,17 +294,12 @@ LLLocalGLTFMaterialTimer::~LLLocalGLTFMaterialTimer() void LLLocalGLTFMaterialTimer::startTimer() { - mEventTimer.start(); + start(); } void LLLocalGLTFMaterialTimer::stopTimer() { - mEventTimer.stop(); -} - -bool LLLocalGLTFMaterialTimer::isRunning() -{ - return mEventTimer.getStarted(); + stop(); } bool LLLocalGLTFMaterialTimer::tick() diff --git a/indra/newview/lllocalgltfmaterials.h b/indra/newview/lllocalgltfmaterials.h index b806b54508..9c2d1bb287 100644 --- a/indra/newview/lllocalgltfmaterials.h +++ b/indra/newview/lllocalgltfmaterials.h @@ -89,8 +89,7 @@ public: public: void startTimer(); void stopTimer(); - bool isRunning(); - bool tick(); + bool tick() override; }; class LLLocalGLTFMaterialMgr : public LLSingleton<LLLocalGLTFMaterialMgr> diff --git a/indra/newview/lllocationinputctrl.cpp b/indra/newview/lllocationinputctrl.cpp index 54dd5792a0..2c91fe06e9 100644 --- a/indra/newview/lllocationinputctrl.cpp +++ b/indra/newview/lllocationinputctrl.cpp @@ -377,7 +377,7 @@ LLLocationInputCtrl::LLLocationInputCtrl(const LLLocationInputCtrl::Params& p) addChild(mParcelIcon[SEE_AVATARS_ICON]); // Register callbacks and load the location field context menu (NB: the order matters). - LLUICtrl::CommitCallbackRegistry::currentRegistrar().add("Navbar.Action", boost::bind(&LLLocationInputCtrl::onLocationContextMenuItemClicked, this, _2)); + LLUICtrl::CommitCallbackRegistry::currentRegistrar().add("Navbar.Action", { boost::bind(&LLLocationInputCtrl::onLocationContextMenuItemClicked, this, _2) }); LLUICtrl::EnableCallbackRegistry::currentRegistrar().add("Navbar.EnableMenuItem", boost::bind(&LLLocationInputCtrl::onLocationContextMenuItemEnabled, this, _2)); setPrearrangeCallback(boost::bind(&LLLocationInputCtrl::onLocationPrearrange, this, _2)); diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp new file mode 100644 index 0000000000..6a725e785f --- /dev/null +++ b/indra/newview/llluamanager.cpp @@ -0,0 +1,483 @@ +/** + * @file llluamanager.cpp + * @brief classes and functions for interfacing with LUA. + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + * + */ + +#include "llviewerprecompiledheaders.h" +#include "llluamanager.h" + +#include "fsyspath.h" +#include "llcoros.h" +#include "llerror.h" +#include "lleventcoro.h" +#include "llsdutil.h" +#include "llviewercontrol.h" +#include "lua_function.h" +#include "lualistener.h" +#include "stringize.h" + +#include <boost/algorithm/string/replace.hpp> + +#include "luau/luacode.h" +#include "luau/lua.h" +#include "luau/luaconf.h" +#include "luau/lualib.h" + +#include <sstream> +#include <string_view> +#include <vector> + +S32 LLLUAmanager::sAutorunScriptCount = 0; +S32 LLLUAmanager::sScriptCount = 0; +std::map<std::string, std::string> LLLUAmanager::sScriptNames; + +lua_function(sleep, "sleep(seconds): pause the running coroutine") +{ + lua_checkdelta(L, -1); + lua_Number seconds = lua_tonumber(L, -1); + lua_pop(L, 1); + llcoro::suspendUntilTimeout(narrow(seconds)); + LuaState::getParent(L).set_interrupts_counter(0); + return 0; +}; + +// This function consumes ALL Lua stack arguments and returns concatenated +// message string +std::string lua_print_msg(lua_State* L, const std::string_view& level) +{ + // On top of existing Lua arguments, we're going to push tostring() and + // duplicate each existing stack entry so we can stringize each one. + lluau_checkstack(L, 2); + luaL_where(L, 1); + // start with the 'where' info at the top of the stack + std::ostringstream out; + out << lua_tostring(L, -1); + lua_pop(L, 1); + const char* sep = ""; // 'where' info ends with ": " + // now iterate over arbitrary args, calling Lua tostring() on each and + // concatenating with separators + for (int p = 1, top = lua_gettop(L); p <= top; ++p) + { + out << sep; + sep = " "; + // push Lua tostring() function -- note, semantically different from + // lua_tostring()! + lua_getglobal(L, "tostring"); + // Now the stack is arguments 1 .. N, plus tostring(). + // Push a copy of the argument at index p. + lua_pushvalue(L, p); + // pop tostring() and arg-p, pushing tostring(arg-p) + // (ignore potential error code from lua_pcall() because, if there was + // an error, we expect the stack top to be an error message -- which + // we'll print) + lua_pcall(L, 1, 1, 0); + out << lua_tostring(L, -1); + lua_pop(L, 1); + } + // pop everything + lua_settop(L, 0); + // capture message string + std::string msg{ out.str() }; + // put message out there for any interested party (*koff* LLFloaterLUADebug *koff*) + LLEventPumps::instance().obtain("lua output").post(stringize(level, ": ", msg)); + + llcoro::suspend(); + return msg; +} + +lua_function(print_debug, "print_debug(args...): DEBUG level logging") +{ + LL_DEBUGS("Lua") << lua_print_msg(L, "DEBUG") << LL_ENDL; + return 0; +} + +// also used for print(); see LuaState constructor +lua_function(print_info, "print_info(args...): INFO level logging") +{ + LL_INFOS("Lua") << lua_print_msg(L, "INFO") << LL_ENDL; + return 0; +} + +lua_function(print_warning, "print_warning(args...): WARNING level logging") +{ + LL_WARNS("Lua") << lua_print_msg(L, "WARN") << LL_ENDL; + return 0; +} + +lua_function(post_on, "post_on(pumpname, data): post specified data to specified LLEventPump") +{ + lua_checkdelta(L, -2); + std::string pumpname{ lua_tostdstring(L, 1) }; + LLSD data{ lua_tollsd(L, 2) }; + lua_pop(L, 2); + LL_DEBUGS("Lua") << "post_on('" << pumpname << "', " << data << ")" << LL_ENDL; + LLEventPumps::instance().obtain(pumpname).post(data); + return 0; +} + +lua_function(get_event_pumps, + "get_event_pumps():\n" + "Returns replypump, commandpump: names of LLEventPumps specific to this chunk.\n" + "Events posted to replypump are queued for get_event_next().\n" + "post_on(commandpump, ...) to engage LLEventAPI operations (see helpleap()).") +{ + lua_checkdelta(L, 2); + lluau_checkstack(L, 2); + auto& listener{ LuaState::obtainListener(L) }; + // return the reply pump name and the command pump name on caller's lua_State + lua_pushstdstring(L, listener.getReplyName()); + lua_pushstdstring(L, listener.getCommandName()); + return 2; +} + +lua_function(get_event_next, + "get_event_next():\n" + "Returns the next (pumpname, data) pair from the replypump whose name\n" + "is returned by get_event_pumps(). Blocks the calling chunk until an\n" + "event becomes available.") +{ + lua_checkdelta(L, 2); + lluau_checkstack(L, 2); + auto& listener{ LuaState::obtainListener(L) }; + const auto& [pump, data]{ listener.getNext() }; + lua_pushstdstring(L, pump); + lua_pushllsd(L, data); + LuaState::getParent(L).set_interrupts_counter(0); + return 2; +} + +LLCoros::Future<LLLUAmanager::script_result> +LLLUAmanager::startScriptFile(const std::string& filename) +{ + // Despite returning from startScriptFile(), we need this Promise to + // remain alive until the callback has fired. + auto promise{ std::make_shared<LLCoros::Promise<script_result>>() }; + runScriptFile(filename, false, + [promise](int count, LLSD result) + { promise->set_value({ count, result }); }); + return LLCoros::getFuture(*promise); +} + +LLLUAmanager::script_result LLLUAmanager::waitScriptFile(const std::string& filename) +{ + return startScriptFile(filename).get(); +} + +void LLLUAmanager::runScriptFile(const std::string &filename, bool autorun, + script_result_fn result_cb, script_finished_fn finished_cb) +{ + // A script_result_fn will be called when LuaState::expr() completes. + LLCoros::instance().launch(filename, [filename, autorun, result_cb, finished_cb]() + { + ScriptObserver observer(LLCoros::getName(), filename); + llifstream in_file; + in_file.open(filename.c_str()); + + if (in_file.is_open()) + { + if (autorun) + { + sAutorunScriptCount++; + } + sScriptCount++; + + // A script_finished_fn is used to initialize the LuaState. + // It will be called when the LuaState is destroyed. + LuaState L(finished_cb); + std::string text{std::istreambuf_iterator<char>(in_file), {}}; + auto [count, result] = L.expr(filename, text); + if (result_cb) + { + result_cb(count, result); + } + } + else + { + auto msg{ stringize("unable to open script file '", filename, "'") }; + LL_WARNS("Lua") << msg << LL_ENDL; + if (result_cb) + { + result_cb(-1, msg); + } + } + }); +} + +LLCoros::Future<LLLUAmanager::script_result> +LLLUAmanager::startScriptLine(const std::string& chunk) +{ + // Despite returning from startScriptLine(), we need this Promise to + // remain alive until the callback has fired. + auto promise{ std::make_shared<LLCoros::Promise<script_result>>() }; + runScriptLine(chunk, + [promise](int count, LLSD result) + { promise->set_value({ count, result }); }); + return LLCoros::getFuture(*promise); +} + +LLLUAmanager::script_result LLLUAmanager::waitScriptLine(const std::string& chunk) +{ + return startScriptLine(chunk).get(); +} + +void LLLUAmanager::runScriptLine(const std::string& chunk, script_result_fn result_cb, + script_finished_fn finished_cb) +{ + // find a suitable abbreviation for the chunk string + std::string shortchunk{ chunk }; + const size_t shortlen = 40; + std::string::size_type eol = shortchunk.find_first_of("\r\n"); + if (eol != std::string::npos) + shortchunk = shortchunk.substr(0, eol); + if (shortchunk.length() > shortlen) + shortchunk = stringize(shortchunk.substr(0, shortlen), "..."); + + std::string desc{ "lua: " + shortchunk }; + LLCoros::instance().launch(desc, [desc, chunk, result_cb, finished_cb]() + { + // A script_finished_fn is used to initialize the LuaState. + // It will be called when the LuaState is destroyed. + LuaState L(finished_cb); + auto [count, result] = L.expr(desc, chunk); + if (result_cb) + { + result_cb(count, result); + } + }); +} + +std::string read_file(const std::string &name) +{ + llifstream in_file; + in_file.open(name.c_str()); + + if (in_file.is_open()) + { + return std::string{std::istreambuf_iterator<char>(in_file), {}}; + } + + return {}; +} + +lua_function(require, "require(module_name) : load module_name.lua from known places") +{ + lua_checkdelta(L); + std::string name = lua_tostdstring(L, 1); + lua_pop(L, 1); + + // resolveRequire() does not return in case of error. + LLRequireResolver::resolveRequire(L, name); + + // resolveRequire() returned the newly-loaded module on the stack top. + // Return it. + return 1; +} + +// push loaded module or throw Lua error +void LLRequireResolver::resolveRequire(lua_State *L, std::string path) +{ + LLRequireResolver resolver(L, std::move(path)); + // findModule() pushes the loaded module or throws a Lua error. + resolver.findModule(); +} + +LLRequireResolver::LLRequireResolver(lua_State *L, const std::string& path) : + mPathToResolve(fsyspath(path).lexically_normal()), + L(L) +{ + mSourceDir = lluau::source_path(L).parent_path(); + + if (mPathToResolve.is_absolute()) + luaL_argerrorL(L, 1, "cannot require a full path"); +} + +// push the loaded module or throw a Lua error +void LLRequireResolver::findModule() +{ + // If mPathToResolve is absolute, this replaces mSourceDir. + auto absolutePath = (mSourceDir / mPathToResolve).u8string(); + + // Push _MODULES table on stack for checking and saving to the cache + luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1); + // Remove that stack entry no matter how we exit + LuaRemover rm_MODULES(L, -1); + + // Check if the module is already in _MODULES table, read from file + // otherwise. + // findModuleImpl() pushes module if found, nothing if not, may throw Lua + // error. + if (findModuleImpl(absolutePath)) + return; + + // not already cached - prep error message just in case + auto fail{ + [L=L, path=mPathToResolve.u8string()]() + { luaL_error(L, "could not find require('%s')", path.data()); }}; + + if (mPathToResolve.is_absolute()) + { + // no point searching known directories for an absolute path + fail(); + } + + LLSD lib_paths(gSavedSettings.getLLSD("LuaRequirePath")); + LL_DEBUGS("Lua") << "LuaRequirePath = " << lib_paths << LL_ENDL; + for (const auto& path : llsd::inArray(lib_paths)) + { + // if path is already absolute, operator/() preserves it + auto abspath(fsyspath(gDirUtilp->getAppRODataDir()) / path.asString()); + std::string absolutePathOpt = (abspath / mPathToResolve).u8string(); + + if (absolutePathOpt.empty()) + luaL_error(L, "error requiring module '%s'", mPathToResolve.u8string().data()); + + if (findModuleImpl(absolutePathOpt)) + return; + } + + // not found + fail(); +} + +// expects _MODULES table on stack top (and leaves it there) +// - if found, pushes loaded module and returns true +// - not found, pushes nothing and returns false +// - may throw Lua error +bool LLRequireResolver::findModuleImpl(const std::string& absolutePath) +{ + std::string possibleSuffixedPaths[] = {absolutePath + ".luau", absolutePath + ".lua"}; + + for (const auto& suffixedPath : possibleSuffixedPaths) + { + // Check _MODULES cache for module + lua_getfield(L, -1, suffixedPath.data()); + if (!lua_isnil(L, -1)) + { + return true; + } + lua_pop(L, 1); + + // Try to read the matching file + std::string source = read_file(suffixedPath); + if (!source.empty()) + { + // Try to run the loaded source. This will leave either a string + // error message or the module contents on the stack top. + runModule(suffixedPath, source); + + // If the stack top is an error message string, raise it. + if (lua_isstring(L, -1)) + lua_error(L); + + // duplicate the new module: _MODULES newmodule newmodule + lua_pushvalue(L, -1); + // store _MODULES[found path] = newmodule + lua_setfield(L, -3, suffixedPath.data()); + + return true; + } + } + + return false; +} + +// push string error message or new module +void LLRequireResolver::runModule(const std::string& desc, const std::string& code) +{ + // Here we just loaded a new module 'code', need to run it and get its result. + lua_State *ML = lua_mainthread(L); + + { + // If loadstring() returns (! LUA_OK) then there's an error message on + // the stack. If it returns LUA_OK then the newly-loaded module code + // is on the stack. + LL_DEBUGS("Lua") << "Loading module " << desc << LL_ENDL; + if (lluau::loadstring(ML, desc, code) != LUA_OK) + { + // error message on stack top + LL_DEBUGS("Lua") << "Error loading module " << desc << ": " + << lua_tostring(ML, -1) << LL_ENDL; + lua_pushliteral(ML, "loadstring: "); + // stack contains error, "loadstring: " + // swap: insert stack top at position -2 + lua_insert(ML, -2); + // stack contains "loadstring: ", error + lua_concat(ML, 2); + // stack contains "loadstring: " + error + } + else // module code on stack top + { + // push debug module + lua_getglobal(ML, "debug"); + // push debug.traceback + lua_getfield(ML, -1, "traceback"); + // stack contains module code, debug, debug.traceback + // ditch debug + lua_replace(ML, -2); + // stack contains module code, debug.traceback + // swap: insert stack top at position -2 + lua_insert(ML, -2); + // stack contains debug.traceback, module code + LL_DEBUGS("Lua") << "Loaded module " << desc << ", running" << LL_ENDL; + // no arguments, one return value + // pass debug.traceback as the error function + int status = lua_pcall(ML, 0, 1, -2); + // lua_pcall() has popped the module code and replaced it with its + // return value. Regardless of status or the type of the stack + // top, get rid of debug.traceback on the stack. + lua_remove(ML, -2); + + if (status == LUA_OK) + { + auto top{ lua_gettop(ML) }; + std::string type{ (top == 0)? "nothing" + : lua_typename(ML, lua_type(ML, -1)) }; + LL_DEBUGS("Lua") << "Module " << desc << " returned " << type << LL_ENDL; + if ((top == 0) || ! (lua_istable(ML, -1) || lua_isfunction(ML, -1))) + { + lua_pushfstring(ML, "module %s must return a table or function, not %s", + desc.data(), type.data()); + } + } + else if (status == LUA_YIELD) + { + LL_DEBUGS("Lua") << "Module " << desc << " yielded" << LL_ENDL; + lua_pushfstring(ML, "module %s can not yield", desc.data()); + } + else + { + llassert(lua_isstring(ML, -1)); + LL_DEBUGS("Lua") << "Module " << desc << " error: " + << lua_tostring(ML, -1) << LL_ENDL; + } + } + } + // There's now a return value (string error message or module) on top of ML. + // Move return value to L's stack. + if (ML != L) + { + lua_xmove(ML, L, 1); + } +} diff --git a/indra/newview/llluamanager.h b/indra/newview/llluamanager.h new file mode 100644 index 0000000000..df76ddd3e2 --- /dev/null +++ b/indra/newview/llluamanager.h @@ -0,0 +1,125 @@ +/** + * @file llluamanager.h + * @brief classes and functions for interfacing with LUA. + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLLUAMANAGER_H +#define LL_LLLUAMANAGER_H + +#include "fsyspath.h" +#include "llcoros.h" +#include "llsd.h" +#include <functional> +#include <string> +#include <utility> // std::pair + +#include "luau/lua.h" +#include "luau/lualib.h" + +class LuaState; + +class LLLUAmanager +{ + friend class ScriptObserver; + +public: + // Pass a callback with this signature to obtain the error message, if + // any, from running a script or source string. Empty msg means success. + typedef std::function<void(std::string msg)> script_finished_fn; + // Pass a callback with this signature to obtain the result, if any, of + // running a script or source string. + // count < 0 means error, and result.asString() is the error message. + // count == 0 with result.isUndefined() means the script returned no results. + // count == 1 means the script returned one result. + // count > 1 with result.isArray() means the script returned multiple + // results, represented as the entries of the result array. + typedef std::function<void(int count, const LLSD& result)> script_result_fn; + // same semantics as script_result_fn parameters + typedef std::pair<int, LLSD> script_result; + + static void runScriptFile(const std::string &filename, bool autorun = false, script_result_fn result_cb = {}, + script_finished_fn finished_cb = {}); + // Start running a Lua script file, returning an LLCoros::Future whose + // get() method will pause the calling coroutine until it can deliver the + // (count, result) pair described above. Between startScriptFile() and + // Future::get(), the caller and the Lua script coroutine will run + // concurrently. + static LLCoros::Future<script_result> startScriptFile(const std::string& filename); + // Run a Lua script file, and pause the calling coroutine until it completes. + // The return value is the (count, result) pair described above. + static script_result waitScriptFile(const std::string& filename); + + static void runScriptLine(const std::string &chunk, script_result_fn result_cb = {}, + script_finished_fn finished_cb = {}); + // Start running a Lua chunk, returning an LLCoros::Future whose + // get() method will pause the calling coroutine until it can deliver the + // (count, result) pair described above. Between startScriptLine() and + // Future::get(), the caller and the Lua script coroutine will run + // concurrently. + static LLCoros::Future<script_result> startScriptLine(const std::string& chunk); + // Run a Lua chunk, and pause the calling coroutine until it completes. + // The return value is the (count, result) pair described above. + static script_result waitScriptLine(const std::string& chunk); + + static const std::map<std::string, std::string> getScriptNames() { return sScriptNames; } + + static S32 sAutorunScriptCount; + static S32 sScriptCount; + + private: + static std::map<std::string, std::string> sScriptNames; +}; + +class LLRequireResolver +{ + public: + static void resolveRequire(lua_State *L, std::string path); + + private: + fsyspath mPathToResolve; + fsyspath mSourceDir; + + LLRequireResolver(lua_State *L, const std::string& path); + + void findModule(); + lua_State *L; + + bool findModuleImpl(const std::string& absolutePath); + void runModule(const std::string& desc, const std::string& code); +}; + +// RAII class to guarantee that a script entry is erased even when coro is terminated +class ScriptObserver +{ + public: + ScriptObserver(const std::string &coro_name, const std::string &filename) : mCoroName(coro_name) + { + LLLUAmanager::sScriptNames[mCoroName] = filename; + } + ~ScriptObserver() { LLLUAmanager::sScriptNames.erase(mCoroName); } + + private: + std::string mCoroName; +}; +#endif diff --git a/indra/newview/llmediactrl.cpp b/indra/newview/llmediactrl.cpp index b39a976ebd..c635ab91c2 100644 --- a/indra/newview/llmediactrl.cpp +++ b/indra/newview/llmediactrl.cpp @@ -345,8 +345,8 @@ bool LLMediaCtrl::handleRightMouseDown( S32 x, S32 y, MASK mask ) auto menu = mContextMenuHandle.get(); if (!menu) { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registar; - registar.add("Open.WebInspector", boost::bind(&LLMediaCtrl::onOpenWebInspector, this)); + LLUICtrl::ScopedRegistrarHelper registrar; + registrar.add("Open.WebInspector", boost::bind(&LLMediaCtrl::onOpenWebInspector, this)); // stinson 05/05/2014 : use this as the parent of the context menu if the static menu // container has yet to be created diff --git a/indra/newview/llmediadataclient.h b/indra/newview/llmediadataclient.h index ca035e79e0..a5e6e43e3f 100644 --- a/indra/newview/llmediadataclient.h +++ b/indra/newview/llmediadataclient.h @@ -219,7 +219,7 @@ protected: { public: RetryTimer(F32 time, Request::ptr_t); - virtual bool tick(); + bool tick() override; private: // back-pointer Request::ptr_t mRequest; @@ -286,7 +286,7 @@ private: { public: QueueTimer(F32 time, LLMediaDataClient *mdc); - virtual bool tick(); + bool tick() override; private: // back-pointer LLPointer<LLMediaDataClient> mMDC; diff --git a/indra/newview/llnetmap.cpp b/indra/newview/llnetmap.cpp index 3f370b1ab5..957948b3fe 100644 --- a/indra/newview/llnetmap.cpp +++ b/indra/newview/llnetmap.cpp @@ -128,7 +128,7 @@ LLNetMap::~LLNetMap() bool LLNetMap::postBuild() { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar commitRegistrar; + LLUICtrl::ScopedRegistrarHelper commitRegistrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enableRegistrar; enableRegistrar.add("Minimap.Zoom.Check", boost::bind(&LLNetMap::isZoomChecked, this, _2)); diff --git a/indra/newview/lloutfitgallery.cpp b/indra/newview/lloutfitgallery.cpp index 72fb9464d8..dba294cef4 100644 --- a/indra/newview/lloutfitgallery.cpp +++ b/indra/newview/lloutfitgallery.cpp @@ -1157,7 +1157,7 @@ void LLOutfitGalleryItem::setDefaultImage() LLContextMenu* LLOutfitGalleryContextMenu::createMenu() { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + LLUICtrl::ScopedRegistrarHelper registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; LLUUID selected_id = mUUIDs.front(); @@ -1169,8 +1169,8 @@ LLContextMenu* LLOutfitGalleryContextMenu::createMenu() boost::bind(&LLAppearanceMgr::takeOffOutfit, &LLAppearanceMgr::instance(), selected_id)); registrar.add("Outfit.Edit", boost::bind(editOutfit)); registrar.add("Outfit.Rename", boost::bind(renameOutfit, selected_id)); - registrar.add("Outfit.Delete", boost::bind(LLOutfitGallery::onRemoveOutfit, selected_id)); - registrar.add("Outfit.Create", boost::bind(&LLOutfitGalleryContextMenu::onCreate, this, _2)); + registrar.add("Outfit.Delete", boost::bind(LLOutfitGallery::onRemoveOutfit, selected_id), LLUICtrl::cb_info::UNTRUSTED_BLOCK); + registrar.add("Outfit.Create", boost::bind(&LLOutfitGalleryContextMenu::onCreate, this, _2), LLUICtrl::cb_info::UNTRUSTED_BLOCK); registrar.add("Outfit.Thumbnail", boost::bind(&LLOutfitGalleryContextMenu::onThumbnail, this, selected_id)); registrar.add("Outfit.Save", boost::bind(&LLOutfitGalleryContextMenu::onSave, this, selected_id)); enable_registrar.add("Outfit.OnEnable", boost::bind(&LLOutfitGalleryContextMenu::onEnable, this, _2)); diff --git a/indra/newview/lloutfitslist.cpp b/indra/newview/lloutfitslist.cpp index 0f5f7aebf8..40b42b165b 100644 --- a/indra/newview/lloutfitslist.cpp +++ b/indra/newview/lloutfitslist.cpp @@ -1053,7 +1053,7 @@ void LLOutfitListBase::deselectOutfit(const LLUUID& category_id) LLContextMenu* LLOutfitContextMenu::createMenu() { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + LLUICtrl::ScopedRegistrarHelper registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; LLUUID selected_id = mUUIDs.front(); @@ -1064,8 +1064,8 @@ LLContextMenu* LLOutfitContextMenu::createMenu() registrar.add("Outfit.TakeOff", boost::bind(&LLAppearanceMgr::takeOffOutfit, &LLAppearanceMgr::instance(), selected_id)); registrar.add("Outfit.Edit", boost::bind(editOutfit)); - registrar.add("Outfit.Rename", boost::bind(renameOutfit, selected_id)); - registrar.add("Outfit.Delete", boost::bind(&LLOutfitListBase::removeSelected, mOutfitList)); + registrar.add("Outfit.Rename", boost::bind(renameOutfit, selected_id), LLUICtrl::cb_info::UNTRUSTED_BLOCK); + registrar.add("Outfit.Delete", boost::bind(&LLOutfitListBase::removeSelected, mOutfitList), LLUICtrl::cb_info::UNTRUSTED_BLOCK); registrar.add("Outfit.Thumbnail", boost::bind(&LLOutfitContextMenu::onThumbnail, this, selected_id)); registrar.add("Outfit.Save", boost::bind(&LLOutfitContextMenu::onSave, this, selected_id)); @@ -1163,7 +1163,7 @@ LLOutfitListGearMenuBase::LLOutfitListGearMenuBase(LLOutfitListBase* olist) { llassert_always(mOutfitList); - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + LLUICtrl::ScopedRegistrarHelper registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; registrar.add("Gear.Wear", boost::bind(&LLOutfitListGearMenuBase::onWear, this)); diff --git a/indra/newview/llpanelblockedlist.cpp b/indra/newview/llpanelblockedlist.cpp index 7d55ba3265..01d4f426de 100644 --- a/indra/newview/llpanelblockedlist.cpp +++ b/indra/newview/llpanelblockedlist.cpp @@ -62,7 +62,7 @@ const std::string BLOCKED_PARAM_NAME = "blocked_to_select"; LLPanelBlockedList::LLPanelBlockedList() : LLPanel() { - mCommitCallbackRegistrar.add("Block.Action", boost::bind(&LLPanelBlockedList::onCustomAction, this, _2)); + mCommitCallbackRegistrar.add("Block.Action", { boost::bind(&LLPanelBlockedList::onCustomAction, this, _2), cb_info::UNTRUSTED_BLOCK }); mEnableCallbackRegistrar.add("Block.Check", boost::bind(&LLPanelBlockedList::isActionChecked, this, _2)); } diff --git a/indra/newview/llpaneleditwearable.cpp b/indra/newview/llpaneleditwearable.cpp index 282b6d4a0a..a90c17f56e 100644 --- a/indra/newview/llpaneleditwearable.cpp +++ b/indra/newview/llpaneleditwearable.cpp @@ -642,8 +642,8 @@ LLPanelEditWearable::LLPanelEditWearable() , mWearablePtr(NULL) , mWearableItem(NULL) { - mCommitCallbackRegistrar.add("ColorSwatch.Commit", boost::bind(&LLPanelEditWearable::onColorSwatchCommit, this, _1)); - mCommitCallbackRegistrar.add("TexturePicker.Commit", boost::bind(&LLPanelEditWearable::onTexturePickerCommit, this, _1)); + mCommitCallbackRegistrar.add("ColorSwatch.Commit", { boost::bind(&LLPanelEditWearable::onColorSwatchCommit, this, _1) }); + mCommitCallbackRegistrar.add("TexturePicker.Commit", { boost::bind(&LLPanelEditWearable::onTexturePickerCommit, this, _1) }); } //virtual diff --git a/indra/newview/llpanelface.cpp b/indra/newview/llpanelface.cpp index 544b6fbc9c..5f8a6fa537 100644 --- a/indra/newview/llpanelface.cpp +++ b/indra/newview/llpanelface.cpp @@ -470,7 +470,7 @@ LLPanelFace::LLPanelFace() mNeedMediaTitle(true) { USE_TEXTURE = LLTrans::getString("use_texture"); - mCommitCallbackRegistrar.add("PanelFace.menuDoToSelected", boost::bind(&LLPanelFace::menuDoToSelected, this, _2)); + mCommitCallbackRegistrar.add("PanelFace.menuDoToSelected", { boost::bind(&LLPanelFace::menuDoToSelected, this, _2) }); mEnableCallbackRegistrar.add("PanelFace.menuEnable", boost::bind(&LLPanelFace::menuEnableItem, this, _2)); } diff --git a/indra/newview/llpanellandmarks.cpp b/indra/newview/llpanellandmarks.cpp index fb7ccbfe4c..5a0cd1a823 100644 --- a/indra/newview/llpanellandmarks.cpp +++ b/indra/newview/llpanellandmarks.cpp @@ -458,10 +458,10 @@ void LLLandmarksPanel::initLandmarksPanel(LLPlacesInventoryPanel* inventory_list // List Commands Handlers void LLLandmarksPanel::initListCommandsHandlers() { - mCommitCallbackRegistrar.add("Places.LandmarksGear.Add.Action", boost::bind(&LLLandmarksPanel::onAddAction, this, _2)); - mCommitCallbackRegistrar.add("Places.LandmarksGear.CopyPaste.Action", boost::bind(&LLLandmarksPanel::onClipboardAction, this, _2)); - mCommitCallbackRegistrar.add("Places.LandmarksGear.Custom.Action", boost::bind(&LLLandmarksPanel::onCustomAction, this, _2)); - mCommitCallbackRegistrar.add("Places.LandmarksGear.Folding.Action", boost::bind(&LLLandmarksPanel::onFoldingAction, this, _2)); + mCommitCallbackRegistrar.add("Places.LandmarksGear.Add.Action", { boost::bind(&LLLandmarksPanel::onAddAction, this, _2) }); + mCommitCallbackRegistrar.add("Places.LandmarksGear.CopyPaste.Action", { boost::bind(&LLLandmarksPanel::onClipboardAction, this, _2) }); + mCommitCallbackRegistrar.add("Places.LandmarksGear.Custom.Action", { boost::bind(&LLLandmarksPanel::onCustomAction, this, _2) }); + mCommitCallbackRegistrar.add("Places.LandmarksGear.Folding.Action", { boost::bind(&LLLandmarksPanel::onFoldingAction, this, _2)} ); mEnableCallbackRegistrar.add("Places.LandmarksGear.Check", boost::bind(&LLLandmarksPanel::isActionChecked, this, _2)); mEnableCallbackRegistrar.add("Places.LandmarksGear.Enable", boost::bind(&LLLandmarksPanel::isActionEnabled, this, _2)); mGearLandmarkMenu = LLUICtrlFactory::getInstance()->createFromFile<LLToggleableMenu>("menu_places_gear_landmark.xml", gMenuHolder, LLViewerMenuHolderGL::child_registry_t::instance()); diff --git a/indra/newview/llpanellogin.cpp b/indra/newview/llpanellogin.cpp index ed80c8b732..cae11a942c 100644 --- a/indra/newview/llpanellogin.cpp +++ b/indra/newview/llpanellogin.cpp @@ -81,15 +81,16 @@ bool LLPanelLogin::sCredentialSet = false; LLPointer<LLCredential> load_user_credentials(std::string &user_key) { - if (gSecAPIHandler->hasCredentialMap("login_list", LLGridManager::getInstance()->getGrid())) + std::string grid{ LLGridManager::instance().getGrid() }; + if (gSecAPIHandler->hasCredentialMap("login_list", grid)) { // user_key should be of "name Resident" format - return gSecAPIHandler->loadFromCredentialMap("login_list", LLGridManager::getInstance()->getGrid(), user_key); + return gSecAPIHandler->loadFromCredentialMap("login_list", grid, user_key); } else { // legacy (or legacy^2, since it also tries to load from settings) - return gSecAPIHandler->loadCredential(LLGridManager::getInstance()->getGrid()); + return gSecAPIHandler->loadCredential(grid); } } @@ -223,7 +224,8 @@ LLPanelLogin::LLPanelLogin(const LLRect &rect, sendChildToBack(getChildView("forgot_password_text")); sendChildToBack(getChildView("sign_up_text")); - std::string current_grid = LLGridManager::getInstance()->getGrid(); + LLGridManager& gridmgr{ LLGridManager::instance() }; + std::string current_grid = gridmgr.getGrid(); if (!mFirstLoginThisInstall) { LLComboBox* favorites_combo = getChild<LLComboBox>("start_location_combo"); @@ -237,7 +239,7 @@ LLPanelLogin::LLPanelLogin(const LLRect &rect, // Load all of the grids, sorted, and then add a bar and the current grid at the top server_choice_combo->removeall(); - std::map<std::string, std::string> known_grids = LLGridManager::getInstance()->getKnownGrids(); + std::map<std::string, std::string> known_grids = gridmgr.getKnownGrids(); for (std::map<std::string, std::string>::iterator grid_choice = known_grids.begin(); grid_choice != known_grids.end(); grid_choice++) @@ -251,7 +253,7 @@ LLPanelLogin::LLPanelLogin(const LLRect &rect, server_choice_combo->sortByName(); LL_DEBUGS("AppInit") << "adding current " << current_grid << LL_ENDL; - server_choice_combo->add(LLGridManager::getInstance()->getGridLabel(), + server_choice_combo->add(gridmgr.getGridLabel(), current_grid, ADD_TOP); server_choice_combo->selectFirstItem(); @@ -760,6 +762,7 @@ void LLPanelLogin::onUpdateStartSLURL(const LLSLURL& new_start_slurl) LL_DEBUGS("AppInit")<<new_start_slurl.asString()<<LL_ENDL; LLComboBox* location_combo = sInstance->getChild<LLComboBox>("start_location_combo"); + LLGridManager& gridmgr{ LLGridManager::instance() }; /* * Determine whether or not the new_start_slurl modifies the grid. * @@ -774,17 +777,17 @@ void LLPanelLogin::onUpdateStartSLURL(const LLSLURL& new_start_slurl) { case LLSLURL::LOCATION: { - std::string slurl_grid = LLGridManager::getInstance()->getGrid(new_start_slurl.getGrid()); + std::string slurl_grid = gridmgr.getGrid(new_start_slurl.getGrid()); if ( ! slurl_grid.empty() ) // is that a valid grid? { - if ( slurl_grid != LLGridManager::getInstance()->getGrid() ) // new grid? + if ( slurl_grid != gridmgr.getGrid() ) // new grid? { // the slurl changes the grid, so update everything to match - LLGridManager::getInstance()->setGridChoice(slurl_grid); + gridmgr.setGridChoice(slurl_grid); // update the grid selector to match the slurl LLComboBox* server_combo = sInstance->getChild<LLComboBox>("server_combo"); - std::string server_label(LLGridManager::getInstance()->getGridLabel(slurl_grid)); + std::string server_label(gridmgr.getGridLabel(slurl_grid)); server_combo->setSimple(server_label); updateServer(); // to change the links and splash screen @@ -831,8 +834,7 @@ void LLPanelLogin::autologinToLocation(const LLSLURL& slurl) if ( LLPanelLogin::sInstance != NULL ) { - void* unused_parameter = 0; - LLPanelLogin::sInstance->onClickConnect(unused_parameter); + LLPanelLogin::sInstance->onClickConnect(false); } } @@ -872,7 +874,8 @@ void LLPanelLogin::loadLoginPage() { if (!sInstance) return; - LLURI login_page = LLURI(LLGridManager::getInstance()->getLoginPage()); + LLGridManager& gridmgr{ LLGridManager::instance() }; + LLURI login_page = LLURI(gridmgr.getLoginPage()); LLSD params(login_page.queryMap()); LL_DEBUGS("AppInit") << "login_page: " << login_page << LL_ENDL; @@ -899,7 +902,7 @@ void LLPanelLogin::loadLoginPage() params["channel"] = LLVersionInfo::instance().getChannel(); // Grid - params["grid"] = LLGridManager::getInstance()->getGridId(); + params["grid"] = gridmgr.getGridId(); // add OS info params["os"] = LLOSInfo::instance().getOSStringSimple(); @@ -916,7 +919,7 @@ void LLPanelLogin::loadLoginPage() login_page.path(), params)); - gViewerWindow->setMenuBackgroundColor(false, !LLGridManager::getInstance()->isInProductionGrid()); + gViewerWindow->setMenuBackgroundColor(false, !gridmgr.isInProductionGrid()); LLMediaCtrl* web_browser = sInstance->getChild<LLMediaCtrl>("login_html"); if (web_browser->getCurrentNavUrl() != login_uri.asString()) @@ -949,9 +952,10 @@ void LLPanelLogin::onClickConnect(bool commit_fields) // the grid definitions may come from a user-supplied grids.xml, so they may not be good LL_DEBUGS("AppInit")<<"grid "<<combo_val.asString()<<LL_ENDL; + LLGridManager& gridmgr{ LLGridManager::instance() }; try { - LLGridManager::getInstance()->setGridChoice(combo_val.asString()); + gridmgr.setGridChoice(combo_val.asString()); } catch (LLInvalidGridName ex) { @@ -984,7 +988,7 @@ void LLPanelLogin::onClickConnect(bool commit_fields) std::string identifier_type; cred->identifierType(identifier_type); LLSD allowed_credential_types; - LLGridManager::getInstance()->getLoginIdentifierTypes(allowed_credential_types); + gridmgr.getLoginIdentifierTypes(allowed_credential_types); // check the typed in credential type against the credential types expected by the server. for(LLSD::array_iterator i = allowed_credential_types.beginArray(); @@ -1133,6 +1137,7 @@ void LLPanelLogin::updateServer() { if (sInstance) { + LLGridManager& gridmgr{ LLGridManager::instance() }; try { // if they've selected another grid, we should load the credentials @@ -1152,7 +1157,7 @@ void LLPanelLogin::updateServer() // populate dropbox and setFields // Note: following call is related to initializeLoginInfo() - LLPointer<LLCredential> credential = gSecAPIHandler->loadCredential(LLGridManager::getInstance()->getGrid()); + LLPointer<LLCredential> credential = gSecAPIHandler->loadCredential(gridmgr.getGrid()); sInstance->populateUserList(credential); // restore creds @@ -1165,12 +1170,12 @@ void LLPanelLogin::updateServer() { // populate dropbox and setFields // Note: following call is related to initializeLoginInfo() - LLPointer<LLCredential> credential = gSecAPIHandler->loadCredential(LLGridManager::getInstance()->getGrid()); + LLPointer<LLCredential> credential = gSecAPIHandler->loadCredential(gridmgr.getGrid()); sInstance->populateUserList(credential); } // update the login panel links - bool system_grid = LLGridManager::getInstance()->isSystemGrid(); + bool system_grid = gridmgr.isSystemGrid(); // Want to vanish not only create_new_account_btn, but also the // title text over it, so turn on/off the whole layout_panel element. @@ -1219,11 +1224,12 @@ void LLPanelLogin::populateUserList(LLPointer<LLCredential> credential) getChild<LLUICtrl>("password_edit")->setValue(std::string()); mUsernameLength = 0; mPasswordLength = 0; + std::string grid{ LLGridManager::instance().getGrid() }; - if (gSecAPIHandler->hasCredentialMap("login_list", LLGridManager::getInstance()->getGrid())) + if (gSecAPIHandler->hasCredentialMap("login_list", grid)) { LLSecAPIHandler::credential_map_t credencials; - gSecAPIHandler->loadCredentialMap("login_list", LLGridManager::getInstance()->getGrid(), credencials); + gSecAPIHandler->loadCredentialMap("login_list", grid, credencials); LLSecAPIHandler::credential_map_t::iterator cr_iter = credencials.begin(); LLSecAPIHandler::credential_map_t::iterator cr_end = credencials.end(); @@ -1278,7 +1284,8 @@ void LLPanelLogin::onSelectServer() LLComboBox* server_combo = getChild<LLComboBox>("server_combo"); LLSD server_combo_val = server_combo->getSelectedValue(); LL_INFOS("AppInit") << "grid "<<server_combo_val.asString()<< LL_ENDL; - LLGridManager::getInstance()->setGridChoice(server_combo_val.asString()); + auto& gridmgr{ LLGridManager::instance() }; + gridmgr.setGridChoice(server_combo_val.asString()); addFavoritesToStartLocation(); /* @@ -1308,7 +1315,7 @@ void LLPanelLogin::onSelectServer() LLSLURL slurl(location); // generata a slurl from the location combo contents if (location.empty() || (slurl.getType() == LLSLURL::LOCATION - && slurl.getGrid() != LLGridManager::getInstance()->getGrid()) + && slurl.getGrid() != gridmgr.getGrid()) ) { // the grid specified by the location is not this one, so clear the combo @@ -1338,7 +1345,7 @@ bool LLPanelLogin::getShowFavorites() } // static -std::string LLPanelLogin::getUserName(LLPointer<LLCredential> &cred) +std::string LLPanelLogin::getUserName(const LLPointer<LLCredential> &cred) { if (cred.isNull()) { diff --git a/indra/newview/llpanellogin.h b/indra/newview/llpanellogin.h index a1bf25fb05..ce708e7a0e 100644 --- a/indra/newview/llpanellogin.h +++ b/indra/newview/llpanellogin.h @@ -85,7 +85,7 @@ public: static bool getShowFavorites(); // extract name from cred in a format apropriate for username field - static std::string getUserName(LLPointer<LLCredential> &cred); + static std::string getUserName(const LLPointer<LLCredential> &cred); private: friend class LLPanelLoginListener; diff --git a/indra/newview/llpanelloginlistener.cpp b/indra/newview/llpanelloginlistener.cpp index fb6f034b7f..aeaf2d1d00 100644 --- a/indra/newview/llpanelloginlistener.cpp +++ b/indra/newview/llpanelloginlistener.cpp @@ -32,9 +32,19 @@ #include "llpanelloginlistener.h" // STL headers // std headers +#include <iomanip> +#include <memory> // external library headers // other Linden headers +#include "llcombobox.h" #include "llpanellogin.h" +#include "llsdutil.h" +#include "llsecapi.h" +#include "llslurl.h" +#include "llstartup.h" +#include "lluictrl.h" +#include "llviewernetwork.h" +#include "stringize.h" LLPanelLoginListener::LLPanelLoginListener(LLPanelLogin* instance): LLEventAPI("LLPanelLogin", "Access to LLPanelLogin methods"), @@ -43,9 +53,195 @@ LLPanelLoginListener::LLPanelLoginListener(LLPanelLogin* instance): add("onClickConnect", "Pretend user clicked the \"Log In\" button", &LLPanelLoginListener::onClickConnect); + add("login", + "Login to Second Life with saved credentials.\n" + "Pass [\"username\"] to select credentials previously saved with that username.\n" + "Pass [\"slurl\"] to specify target location.\n" + "Pass [\"grid\"] to select one of the valid grids in grids.xml.", + &LLPanelLoginListener::login, + llsd::map("reply", LLSD::String())); + add("savedLogins", + "Return \"logins\" as a list of {} saved login names.\n" + "Pass [\"grid\"] to select one of the valid grids in grids.xml.", + &LLPanelLoginListener::savedLogins, + llsd::map("reply", LLSD::String())); } void LLPanelLoginListener::onClickConnect(const LLSD&) const { mPanel->onClickConnect(false); } + +namespace +{ + + std::string gridkey(const std::string& grid) + { + if (grid.find('.') != std::string::npos) + { + return grid; + } + else + { + return stringize("util.", grid, ".lindenlab.com"); + } + } + +} // anonymous namespace + +void LLPanelLoginListener::login(const LLSD& args) const +{ + // Although we require a "reply" key, don't instantiate a Response object: + // if things go well, response to our invoker will come later, once the + // viewer gets the response from the login server. + std::string username(args["username"]), slurlstr(args["slurl"]), grid(args["grid"]); + LL_DEBUGS("AppInit") << "Login with saved credentials"; + if (! username.empty()) + { + LL_CONT << " for user " << std::quoted(username); + } + if (! slurlstr.empty()) + { + LL_CONT << " to location " << std::quoted(slurlstr); + } + if (! grid.empty()) + { + LL_CONT << " on grid " << std::quoted(grid); + } + LL_CONT << LL_ENDL; + + // change grid first, allowing slurl to override + auto& gridmgr{ LLGridManager::instance() }; + if (! grid.empty()) + { + grid = gridkey(grid); + // setGridChoice() can throw LLInvalidGridName -- but if so, let it + // propagate, trusting that LLEventAPI will catch it and send an + // appropriate reply. + auto server_combo = mPanel->getChild<LLComboBox>("server_combo"); + server_combo->setSimple(gridmgr.getGridLabel(grid)); + LLPanelLogin::updateServer(); + } + if (! slurlstr.empty()) + { + LLSLURL slurl(slurlstr); + if (! slurl.isSpatial()) + { + // don't bother with LLTHROW() because we expect LLEventAPI to + // catch and report back to invoker + throw DispatchError(stringize("Invalid start SLURL ", std::quoted(slurlstr))); + } + // Bypass LLStartUp::setStartSLURL() because, after validating as + // above, it bounces right back to LLPanelLogin::onUpdateStartSLURL(). + // It also sets the "NextLoginLocation" user setting, but as with grid, + // we don't yet know whether that's desirable for scripted login. + LLPanelLogin::onUpdateStartSLURL(slurl); + } + if (! username.empty()) + { + // Transform (e.g.) "Nat Linden" to the internal form expected by + // loadFromCredentialMap(), e.g. "nat_linden" + LLStringUtil::toLower(username); + LLStringUtil::replaceChar(username, ' ', '_'); + LLStringUtil::replaceChar(username, '.', '_'); + + // call gridmgr.getGrid() because our caller didn't necessarily pass + // ["grid"] -- or it might have been overridden by the SLURL + auto cred = gSecAPIHandler->loadFromCredentialMap("login_list", + gridmgr.getGrid(), + username); + LLPanelLogin::setFields(cred); + } + + if (mPanel->getChild<LLUICtrl>("username_combo")->getValue().asString().empty() || + mPanel->getChild<LLUICtrl>("password_edit") ->getValue().asString().empty()) + { + // as above, let LLEventAPI report back to invoker + throw DispatchError(stringize("Unrecognized username ", std::quoted(username))); + } + + // Here we're about to trigger actual login, which is all we can do in + // this method. All subsequent responses must come via the "login" + // LLEventPump. + LLEventPumps::instance().obtain("login").listen( + "Lua login", + [args] + (const LLBoundListener& conn, const LLSD& update) + { + LLSD response{ llsd::map("ok", false) }; + if (update["change"] == "connect") + { + // success! + response["ok"] = true; + } + else if (update["change"] == "disconnect") + { + // Does this ever actually happen? + // LLLoginInstance::handleDisconnect() says it's a placeholder. + response["error"] = "login disconnect"; + } + else if (update["change"] == "fail.login") + { + // why? + // LLLoginInstance::handleLoginFailure() has special code to + // handle "tos", "update" and "mfa_challenge". Do not respond + // automatically because these things are SUPPOSED to engage + // the user. + // Copy specific response fields because there's an enormous + // chunk of stuff that comes back on success. + LLSD data{ update["data"] }; + const char* fields[] = { + "reason", + "error_code" + }; + for (auto field : fields) + { + response[field] = data[field]; + } + response["error"] = update["message"]; + } + else + { + // Ignore all other "change" values: LLLogin sends multiple update + // events. Only a few of them (above) indicate completion. + return; + } + // For all the completion cases, disconnect from the "login" + // LLEventPump. + conn.disconnect(); + // and at last, send response to the original invoker + sendReply(response, args); + }); + + // Turn the Login crank. + mPanel->onClickConnect(false); +} + +LLSD LLPanelLoginListener::savedLogins(const LLSD& args) const +{ + LL_INFOS() << "called with " << args << LL_ENDL; + std::string grid = (args.has("grid")? gridkey(args["grid"].asString()) + : LLGridManager::instance().getGrid()); + if (! gSecAPIHandler->hasCredentialMap("login_list", grid)) + { + LL_INFOS() << "no creds for " << grid << LL_ENDL; + return llsd::map("error", + stringize("No saved logins for grid ", std::quoted(grid))); + } + LLSecAPIHandler::credential_map_t creds; + gSecAPIHandler->loadCredentialMap("login_list", grid, creds); + auto result = llsd::map( + "logins", + llsd::toArray( + creds, + [](const auto& pair) + { + return llsd::map( + "name", + LLPanelLogin::getUserName(pair.second), + "key", + pair.first); + })); + LL_INFOS() << "returning creds " << result << LL_ENDL; + return result; +} diff --git a/indra/newview/llpanelloginlistener.h b/indra/newview/llpanelloginlistener.h index b552ccd5b0..f2f4d70ff9 100644 --- a/indra/newview/llpanelloginlistener.h +++ b/indra/newview/llpanelloginlistener.h @@ -40,6 +40,8 @@ public: private: void onClickConnect(const LLSD&) const; + void login(const LLSD&) const; + LLSD savedLogins(const LLSD&) const; LLPanelLogin* mPanel; }; diff --git a/indra/newview/llpanelmaininventory.cpp b/indra/newview/llpanelmaininventory.cpp index 377af4384a..8bc5989b89 100644 --- a/indra/newview/llpanelmaininventory.cpp +++ b/indra/newview/llpanelmaininventory.cpp @@ -125,14 +125,16 @@ LLPanelMainInventory::LLPanelMainInventory(const LLPanel::Params& p) mGalleryRootUpdatedConnection() { // Menu Callbacks (non contex menus) - mCommitCallbackRegistrar.add("Inventory.DoToSelected", boost::bind(&LLPanelMainInventory::doToSelected, this, _2)); - mCommitCallbackRegistrar.add("Inventory.CloseAllFolders", boost::bind(&LLPanelMainInventory::closeAllFolders, this)); - mCommitCallbackRegistrar.add("Inventory.EmptyTrash", boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyTrash", LLFolderType::FT_TRASH)); - mCommitCallbackRegistrar.add("Inventory.EmptyLostAndFound", boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyLostAndFound", LLFolderType::FT_LOST_AND_FOUND)); - mCommitCallbackRegistrar.add("Inventory.DoCreate", boost::bind(&LLPanelMainInventory::doCreate, this, _2)); - mCommitCallbackRegistrar.add("Inventory.ShowFilters", boost::bind(&LLPanelMainInventory::toggleFindOptions, this)); - mCommitCallbackRegistrar.add("Inventory.ResetFilters", boost::bind(&LLPanelMainInventory::resetFilters, this)); - mCommitCallbackRegistrar.add("Inventory.SetSortBy", boost::bind(&LLPanelMainInventory::setSortBy, this, _2)); + mCommitCallbackRegistrar.add("Inventory.DoToSelected", { boost::bind(&LLPanelMainInventory::doToSelected, this, _2), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.CloseAllFolders", { boost::bind(&LLPanelMainInventory::closeAllFolders, this) }); + mCommitCallbackRegistrar.add("Inventory.EmptyTrash", { boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, + "ConfirmEmptyTrash", LLFolderType::FT_TRASH), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.EmptyLostAndFound", { boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, + "ConfirmEmptyLostAndFound", LLFolderType::FT_LOST_AND_FOUND), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.DoCreate", { boost::bind(&LLPanelMainInventory::doCreate, this, _2) }); + mCommitCallbackRegistrar.add("Inventory.ShowFilters", { boost::bind(&LLPanelMainInventory::toggleFindOptions, this) }); + mCommitCallbackRegistrar.add("Inventory.ResetFilters", { boost::bind(&LLPanelMainInventory::resetFilters, this) }); + mCommitCallbackRegistrar.add("Inventory.SetSortBy", { boost::bind(&LLPanelMainInventory::setSortBy, this, _2) }); mEnableCallbackRegistrar.add("Inventory.EnvironmentEnabled", [](LLUICtrl *, const LLSD &) { return LLPanelMainInventory::hasSettingsInventory(); }); mEnableCallbackRegistrar.add("Inventory.MaterialsEnabled", [](LLUICtrl *, const LLSD &) { return LLPanelMainInventory::hasMaterialsInventory(); }); @@ -1516,7 +1518,7 @@ void LLPanelMainInventory::initListCommandsHandlers() mBackBtn->setCommitCallback(boost::bind(&LLPanelMainInventory::onBackFolderClicked, this)); mForwardBtn->setCommitCallback(boost::bind(&LLPanelMainInventory::onForwardFolderClicked, this)); - mCommitCallbackRegistrar.add("Inventory.GearDefault.Custom.Action", boost::bind(&LLPanelMainInventory::onCustomAction, this, _2)); + mCommitCallbackRegistrar.add("Inventory.GearDefault.Custom.Action", { boost::bind(&LLPanelMainInventory::onCustomAction, this, _2), cb_info::UNTRUSTED_BLOCK }); mEnableCallbackRegistrar.add("Inventory.GearDefault.Check", boost::bind(&LLPanelMainInventory::isActionChecked, this, _2)); mEnableCallbackRegistrar.add("Inventory.GearDefault.Enable", boost::bind(&LLPanelMainInventory::isActionEnabled, this, _2)); mEnableCallbackRegistrar.add("Inventory.GearDefault.Visible", boost::bind(&LLPanelMainInventory::isActionVisible, this, _2)); diff --git a/indra/newview/llpanelmediasettingssecurity.cpp b/indra/newview/llpanelmediasettingssecurity.cpp index 6e4e9f426d..7041f7945d 100644 --- a/indra/newview/llpanelmediasettingssecurity.cpp +++ b/indra/newview/llpanelmediasettingssecurity.cpp @@ -49,8 +49,8 @@ LLPanelMediaSettingsSecurity::LLPanelMediaSettingsSecurity() : mParent( NULL ) { - mCommitCallbackRegistrar.add("Media.whitelistAdd", boost::bind(&LLPanelMediaSettingsSecurity::onBtnAdd, this)); - mCommitCallbackRegistrar.add("Media.whitelistDelete", boost::bind(&LLPanelMediaSettingsSecurity::onBtnDel, this)); + mCommitCallbackRegistrar.add("Media.whitelistAdd", { boost::bind(&LLPanelMediaSettingsSecurity::onBtnAdd, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Media.whitelistDelete", { boost::bind(&LLPanelMediaSettingsSecurity::onBtnDel, this), cb_info::UNTRUSTED_BLOCK }); // build dialog from XML buildFromFile( "panel_media_settings_security.xml"); diff --git a/indra/newview/llpanelnearbymedia.cpp b/indra/newview/llpanelnearbymedia.cpp index 2dd4866da3..86a88d6793 100644 --- a/indra/newview/llpanelnearbymedia.cpp +++ b/indra/newview/llpanelnearbymedia.cpp @@ -89,24 +89,24 @@ LLPanelNearByMedia::LLPanelNearByMedia() gSavedSettings.getControl("ParcelMediaAutoPlayEnable")->getSignal()->connect(boost::bind(&LLPanelNearByMedia::handleMediaAutoPlayChanged, this, _2)); - mCommitCallbackRegistrar.add("MediaListCtrl.EnableAll", boost::bind(&LLPanelNearByMedia::onClickEnableAll, this)); - mCommitCallbackRegistrar.add("MediaListCtrl.DisableAll", boost::bind(&LLPanelNearByMedia::onClickDisableAll, this)); - mCommitCallbackRegistrar.add("MediaListCtrl.GoMediaPrefs", boost::bind(&LLPanelNearByMedia::onAdvancedButtonClick, this)); - mCommitCallbackRegistrar.add("MediaListCtrl.MoreLess", boost::bind(&LLPanelNearByMedia::onMoreLess, this)); - mCommitCallbackRegistrar.add("SelectedMediaCtrl.Stop", boost::bind(&LLPanelNearByMedia::onClickSelectedMediaStop, this)); - mCommitCallbackRegistrar.add("SelectedMediaCtrl.Play", boost::bind(&LLPanelNearByMedia::onClickSelectedMediaPlay, this)); - mCommitCallbackRegistrar.add("SelectedMediaCtrl.Pause", boost::bind(&LLPanelNearByMedia::onClickSelectedMediaPause, this)); - mCommitCallbackRegistrar.add("SelectedMediaCtrl.Mute", boost::bind(&LLPanelNearByMedia::onClickSelectedMediaMute, this)); - mCommitCallbackRegistrar.add("SelectedMediaCtrl.Volume", boost::bind(&LLPanelNearByMedia::onCommitSelectedMediaVolume, this)); - mCommitCallbackRegistrar.add("SelectedMediaCtrl.Zoom", boost::bind(&LLPanelNearByMedia::onClickSelectedMediaZoom, this)); - mCommitCallbackRegistrar.add("SelectedMediaCtrl.Unzoom", boost::bind(&LLPanelNearByMedia::onClickSelectedMediaUnzoom, this)); + mCommitCallbackRegistrar.add("MediaListCtrl.EnableAll", { boost::bind(&LLPanelNearByMedia::onClickEnableAll, this) }); + mCommitCallbackRegistrar.add("MediaListCtrl.DisableAll", { boost::bind(&LLPanelNearByMedia::onClickDisableAll, this) }); + mCommitCallbackRegistrar.add("MediaListCtrl.GoMediaPrefs", { boost::bind(&LLPanelNearByMedia::onAdvancedButtonClick, this) }); + mCommitCallbackRegistrar.add("MediaListCtrl.MoreLess", { boost::bind(&LLPanelNearByMedia::onMoreLess, this) }); + mCommitCallbackRegistrar.add("SelectedMediaCtrl.Stop", { boost::bind(&LLPanelNearByMedia::onClickSelectedMediaStop, this) }); + mCommitCallbackRegistrar.add("SelectedMediaCtrl.Play", { boost::bind(&LLPanelNearByMedia::onClickSelectedMediaPlay, this) }); + mCommitCallbackRegistrar.add("SelectedMediaCtrl.Pause", { boost::bind(&LLPanelNearByMedia::onClickSelectedMediaPause, this) }); + mCommitCallbackRegistrar.add("SelectedMediaCtrl.Mute", { boost::bind(&LLPanelNearByMedia::onClickSelectedMediaMute, this) }); + mCommitCallbackRegistrar.add("SelectedMediaCtrl.Volume", { boost::bind(&LLPanelNearByMedia::onCommitSelectedMediaVolume, this) }); + mCommitCallbackRegistrar.add("SelectedMediaCtrl.Zoom", { boost::bind(&LLPanelNearByMedia::onClickSelectedMediaZoom, this) }); + mCommitCallbackRegistrar.add("SelectedMediaCtrl.Unzoom", { boost::bind(&LLPanelNearByMedia::onClickSelectedMediaUnzoom, this) }); // Context menu handler. mCommitCallbackRegistrar.add("SelectedMediaCtrl.Action", - [this](LLUICtrl* ctrl, const LLSD& data) + {[this](LLUICtrl* ctrl, const LLSD& data) { onMenuAction(data); - }); + }}); mEnableCallbackRegistrar.add("SelectedMediaCtrl.Visible", [this](LLUICtrl* ctrl, const LLSD& data) { diff --git a/indra/newview/llpanelobject.cpp b/indra/newview/llpanelobject.cpp index 0a3a2e753a..76eb2fd017 100644 --- a/indra/newview/llpanelobject.cpp +++ b/indra/newview/llpanelobject.cpp @@ -296,7 +296,7 @@ LLPanelObject::LLPanelObject() mHasClipboardRot(false), mSizeChanged(false) { - mCommitCallbackRegistrar.add("PanelObject.menuDoToSelected", boost::bind(&LLPanelObject::menuDoToSelected, this, _2)); + mCommitCallbackRegistrar.add("PanelObject.menuDoToSelected", { boost::bind(&LLPanelObject::menuDoToSelected, this, _2) }); mEnableCallbackRegistrar.add("PanelObject.menuEnable", boost::bind(&LLPanelObject::menuEnableItem, this, _2)); } diff --git a/indra/newview/llpanelobjectinventory.cpp b/indra/newview/llpanelobjectinventory.cpp index ef7986603b..dcd7fdf30d 100644 --- a/indra/newview/llpanelobjectinventory.cpp +++ b/indra/newview/llpanelobjectinventory.cpp @@ -1332,14 +1332,16 @@ LLPanelObjectInventory::LLPanelObjectInventory(const LLPanelObjectInventory::Par mShowRootFolder(p.show_root_folder) { // Setup context menu callbacks - mCommitCallbackRegistrar.add("Inventory.DoToSelected", boost::bind(&LLPanelObjectInventory::doToSelected, this, _2)); - mCommitCallbackRegistrar.add("Inventory.EmptyTrash", boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyTrash", LLFolderType::FT_TRASH)); - mCommitCallbackRegistrar.add("Inventory.EmptyLostAndFound", boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyLostAndFound", LLFolderType::FT_LOST_AND_FOUND)); - mCommitCallbackRegistrar.add("Inventory.DoCreate", boost::bind(&do_nothing)); - mCommitCallbackRegistrar.add("Inventory.AttachObject", boost::bind(&do_nothing)); - mCommitCallbackRegistrar.add("Inventory.BeginIMSession", boost::bind(&do_nothing)); - mCommitCallbackRegistrar.add("Inventory.Share", boost::bind(&LLAvatarActions::shareWithAvatars, this)); - mCommitCallbackRegistrar.add("Inventory.FileUploadLocation", boost::bind(&do_nothing)); + mCommitCallbackRegistrar.add("Inventory.DoToSelected", { boost::bind(&LLPanelObjectInventory::doToSelected, this, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.EmptyTrash", + { boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyTrash", LLFolderType::FT_TRASH), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.EmptyLostAndFound", + { boost::bind(&LLInventoryModel::emptyFolderType, &gInventory, "ConfirmEmptyLostAndFound", LLFolderType::FT_LOST_AND_FOUND), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.DoCreate", { boost::bind(&do_nothing) }); + mCommitCallbackRegistrar.add("Inventory.AttachObject", { boost::bind(&do_nothing) }); + mCommitCallbackRegistrar.add("Inventory.BeginIMSession", { boost::bind(&do_nothing) }); + mCommitCallbackRegistrar.add("Inventory.Share", { boost::bind(&LLAvatarActions::shareWithAvatars, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.FileUploadLocation", { boost::bind(&do_nothing) }); } // Destroys the object diff --git a/indra/newview/llpaneloutfitedit.cpp b/indra/newview/llpaneloutfitedit.cpp index 4cd4afaa5a..60017db51d 100644 --- a/indra/newview/llpaneloutfitedit.cpp +++ b/indra/newview/llpaneloutfitedit.cpp @@ -157,7 +157,7 @@ public: { LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; - registrar.add("Wearable.Create", boost::bind(onCreate, _2)); + registrar.add("Wearable.Create", { boost::bind(onCreate, _2), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); llassert(LLMenuGL::sMenuContainer != NULL); LLToggleableMenu* menu = LLUICtrlFactory::getInstance()->createFromFile<LLToggleableMenu>( @@ -226,7 +226,7 @@ public: LLHandle<LLView> flat_list_handle = flat_list->getHandle(); LLHandle<LLPanel> inventory_panel_handle = inventory_panel->getHandle(); - registrar.add("AddWearable.Gear.Sort", boost::bind(onSort, flat_list_handle, inventory_panel_handle, _2)); + registrar.add("AddWearable.Gear.Sort", { boost::bind(onSort, flat_list_handle, inventory_panel_handle, _2) }); enable_registrar.add("AddWearable.Gear.Check", boost::bind(onCheck, flat_list_handle, inventory_panel_handle, _2)); enable_registrar.add("AddWearable.Gear.Visible", boost::bind(onVisible, inventory_panel_handle, _2)); diff --git a/indra/newview/llpanelpeople.cpp b/indra/newview/llpanelpeople.cpp index 25672db318..ca1a4e258d 100644 --- a/indra/newview/llpanelpeople.cpp +++ b/indra/newview/llpanelpeople.cpp @@ -308,10 +308,10 @@ public: : LLEventTimer(period), LLPanelPeople::Updater(cb) { - mEventTimer.stop(); + stop(); } - virtual bool tick() // from LLEventTimer + bool tick() override // from LLEventTimer { return false; } @@ -348,13 +348,13 @@ public: LLAvatarTracker::instance().removeObserver(this); } - /*virtual*/ void changed(U32 mask) + void changed(U32 mask) override { if (mIsActive) { // events can arrive quickly in bulk - we need not process EVERY one of them - // so we wait a short while to let others pile-in, and process them in aggregate. - mEventTimer.start(); + start(); } // save-up all the mask-bits which have come-in @@ -362,7 +362,7 @@ public: } - /*virtual*/ bool tick() + bool tick() override { if (!mIsActive) return false; @@ -372,14 +372,13 @@ public: } // Stop updates. - mEventTimer.stop(); + stop(); mMask = 0; return false; } - // virtual - void setActive(bool active) + void setActive(bool active) override { mIsActive = active; if (active) @@ -488,22 +487,22 @@ public: setActive(false); } - /*virtual*/ void setActive(bool val) + void setActive(bool val) override { if (val) { // update immediately and start regular updates update(); - mEventTimer.start(); + start(); } else { // stop regular updates - mEventTimer.stop(); + stop(); } } - /*virtual*/ bool tick() + bool tick() override { update(); return false; @@ -543,18 +542,18 @@ LLPanelPeople::LLPanelPeople() mRecentListUpdater = new LLRecentListUpdater(boost::bind(&LLPanelPeople::updateRecentList, this)); mButtonsUpdater = new LLButtonsUpdater(boost::bind(&LLPanelPeople::updateButtons, this)); - mCommitCallbackRegistrar.add("People.AddFriend", boost::bind(&LLPanelPeople::onAddFriendButtonClicked, this)); - mCommitCallbackRegistrar.add("People.AddFriendWizard", boost::bind(&LLPanelPeople::onAddFriendWizButtonClicked, this)); - mCommitCallbackRegistrar.add("People.DelFriend", boost::bind(&LLPanelPeople::onDeleteFriendButtonClicked, this)); - mCommitCallbackRegistrar.add("People.Group.Minus", boost::bind(&LLPanelPeople::onGroupMinusButtonClicked, this)); - mCommitCallbackRegistrar.add("People.Chat", boost::bind(&LLPanelPeople::onChatButtonClicked, this)); - mCommitCallbackRegistrar.add("People.Gear", boost::bind(&LLPanelPeople::onGearButtonClicked, this, _1)); - - mCommitCallbackRegistrar.add("People.Group.Plus.Action", boost::bind(&LLPanelPeople::onGroupPlusMenuItemClicked, this, _2)); - mCommitCallbackRegistrar.add("People.Friends.ViewSort.Action", boost::bind(&LLPanelPeople::onFriendsViewSortMenuItemClicked, this, _2)); - mCommitCallbackRegistrar.add("People.Nearby.ViewSort.Action", boost::bind(&LLPanelPeople::onNearbyViewSortMenuItemClicked, this, _2)); - mCommitCallbackRegistrar.add("People.Groups.ViewSort.Action", boost::bind(&LLPanelPeople::onGroupsViewSortMenuItemClicked, this, _2)); - mCommitCallbackRegistrar.add("People.Recent.ViewSort.Action", boost::bind(&LLPanelPeople::onRecentViewSortMenuItemClicked, this, _2)); + mCommitCallbackRegistrar.add("People.AddFriend", { boost::bind(&LLPanelPeople::onAddFriendButtonClicked, this) }); + mCommitCallbackRegistrar.add("People.AddFriendWizard", { boost::bind(&LLPanelPeople::onAddFriendWizButtonClicked, this) }); + mCommitCallbackRegistrar.add("People.DelFriend", { boost::bind(&LLPanelPeople::onDeleteFriendButtonClicked, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("People.Group.Minus", { boost::bind(&LLPanelPeople::onGroupMinusButtonClicked, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("People.Chat", { boost::bind(&LLPanelPeople::onChatButtonClicked, this) }); + mCommitCallbackRegistrar.add("People.Gear", { boost::bind(&LLPanelPeople::onGearButtonClicked, this, _1) }); + + mCommitCallbackRegistrar.add("People.Group.Plus.Action", { boost::bind(&LLPanelPeople::onGroupPlusMenuItemClicked, this, _2), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("People.Friends.ViewSort.Action", { boost::bind(&LLPanelPeople::onFriendsViewSortMenuItemClicked, this, _2) }); + mCommitCallbackRegistrar.add("People.Nearby.ViewSort.Action", { boost::bind(&LLPanelPeople::onNearbyViewSortMenuItemClicked, this, _2) }); + mCommitCallbackRegistrar.add("People.Groups.ViewSort.Action", { boost::bind(&LLPanelPeople::onGroupsViewSortMenuItemClicked, this, _2) }); + mCommitCallbackRegistrar.add("People.Recent.ViewSort.Action", { boost::bind(&LLPanelPeople::onRecentViewSortMenuItemClicked, this, _2) }); mEnableCallbackRegistrar.add("People.Friends.ViewSort.CheckItem", boost::bind(&LLPanelPeople::onFriendsViewSortMenuItemCheck, this, _2)); mEnableCallbackRegistrar.add("People.Recent.ViewSort.CheckItem", boost::bind(&LLPanelPeople::onRecentViewSortMenuItemCheck, this, _2)); diff --git a/indra/newview/llpanelpeoplemenus.cpp b/indra/newview/llpanelpeoplemenus.cpp index f8a73ddb46..283be752ed 100644 --- a/indra/newview/llpanelpeoplemenus.cpp +++ b/indra/newview/llpanelpeoplemenus.cpp @@ -58,7 +58,7 @@ NearbyPeopleContextMenu gNearbyPeopleContextMenu; LLContextMenu* PeopleContextMenu::createMenu() { // set up the callbacks for all of the avatar menu items - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + LLUICtrl::ScopedRegistrarHelper registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; LLContextMenu* menu; @@ -68,21 +68,21 @@ LLContextMenu* PeopleContextMenu::createMenu() const LLUUID& id = mUUIDs.front(); registrar.add("Avatar.Profile", boost::bind(&LLAvatarActions::showProfile, id)); - registrar.add("Avatar.AddFriend", boost::bind(&LLAvatarActions::requestFriendshipDialog, id)); - registrar.add("Avatar.RemoveFriend", boost::bind(&LLAvatarActions::removeFriendDialog, id)); + registrar.add("Avatar.AddFriend", boost::bind(&LLAvatarActions::requestFriendshipDialog, id), LLUICtrl::cb_info::UNTRUSTED_THROTTLE); + registrar.add("Avatar.RemoveFriend", boost::bind(&LLAvatarActions::removeFriendDialog, id), LLUICtrl::cb_info::UNTRUSTED_BLOCK); registrar.add("Avatar.IM", boost::bind(&LLAvatarActions::startIM, id)); - registrar.add("Avatar.Call", boost::bind(&LLAvatarActions::startCall, id)); - registrar.add("Avatar.OfferTeleport", boost::bind(&PeopleContextMenu::offerTeleport, this)); + registrar.add("Avatar.Call", boost::bind(&LLAvatarActions::startCall, id), LLUICtrl::cb_info::UNTRUSTED_BLOCK); + registrar.add("Avatar.OfferTeleport", boost::bind(&PeopleContextMenu::offerTeleport, this), LLUICtrl::cb_info::UNTRUSTED_BLOCK); registrar.add("Avatar.ZoomIn", boost::bind(&handle_zoom_to_object, id)); registrar.add("Avatar.ShowOnMap", boost::bind(&LLAvatarActions::showOnMap, id)); - registrar.add("Avatar.Share", boost::bind(&LLAvatarActions::share, id)); - registrar.add("Avatar.Pay", boost::bind(&LLAvatarActions::pay, id)); + registrar.add("Avatar.Share", boost::bind(&LLAvatarActions::share, id), LLUICtrl::cb_info::UNTRUSTED_BLOCK); + registrar.add("Avatar.Pay", boost::bind(&LLAvatarActions::pay, id), LLUICtrl::cb_info::UNTRUSTED_BLOCK); registrar.add("Avatar.BlockUnblock", boost::bind(&LLAvatarActions::toggleBlock, id)); - registrar.add("Avatar.InviteToGroup", boost::bind(&LLAvatarActions::inviteToGroup, id)); + registrar.add("Avatar.InviteToGroup", boost::bind(&LLAvatarActions::inviteToGroup, id), LLUICtrl::cb_info::UNTRUSTED_BLOCK); registrar.add("Avatar.TeleportRequest", boost::bind(&PeopleContextMenu::requestTeleport, this)); - registrar.add("Avatar.Calllog", boost::bind(&LLAvatarActions::viewChatHistory, id)); - registrar.add("Avatar.Freeze", boost::bind(&LLAvatarActions::freezeAvatar, id)); - registrar.add("Avatar.Eject", boost::bind(&PeopleContextMenu::eject, this)); + registrar.add("Avatar.Calllog", boost::bind(&LLAvatarActions::viewChatHistory, id), LLUICtrl::cb_info::UNTRUSTED_THROTTLE); + registrar.add("Avatar.Freeze", boost::bind(&LLAvatarActions::freezeAvatar, id), LLUICtrl::cb_info::UNTRUSTED_THROTTLE); + registrar.add("Avatar.Eject", boost::bind(&PeopleContextMenu::eject, this), LLUICtrl::cb_info::UNTRUSTED_THROTTLE); enable_registrar.add("Avatar.EnableItem", boost::bind(&PeopleContextMenu::enableContextMenuItem, this, _2)); diff --git a/indra/newview/llpanelplaces.cpp b/indra/newview/llpanelplaces.cpp index 7deb1d9fd4..5fb20d4b69 100644 --- a/indra/newview/llpanelplaces.cpp +++ b/indra/newview/llpanelplaces.cpp @@ -315,7 +315,7 @@ bool LLPanelPlaces::postBuild() , _7 // EAcceptance* accept )); - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + ScopedRegistrarHelper registrar; registrar.add("Places.OverflowMenu.Action", boost::bind(&LLPanelPlaces::onOverflowMenuItemClicked, this, _2)); LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; enable_registrar.add("Places.OverflowMenu.Enable", boost::bind(&LLPanelPlaces::onOverflowMenuItemEnable, this, _2)); diff --git a/indra/newview/llpanelpresetscamerapulldown.cpp b/indra/newview/llpanelpresetscamerapulldown.cpp index 5b0aa28223..d1497719e0 100644 --- a/indra/newview/llpanelpresetscamerapulldown.cpp +++ b/indra/newview/llpanelpresetscamerapulldown.cpp @@ -48,8 +48,8 @@ // Default constructor LLPanelPresetsCameraPulldown::LLPanelPresetsCameraPulldown() { - mCommitCallbackRegistrar.add("Presets.toggleCameraFloater", boost::bind(&LLPanelPresetsCameraPulldown::onViewButtonClick, this, _2)); - mCommitCallbackRegistrar.add("PresetsCamera.RowClick", boost::bind(&LLPanelPresetsCameraPulldown::onRowClick, this, _2)); + mCommitCallbackRegistrar.add("Presets.toggleCameraFloater", { boost::bind(&LLPanelPresetsCameraPulldown::onViewButtonClick, this, _2) }); + mCommitCallbackRegistrar.add("PresetsCamera.RowClick", { boost::bind(&LLPanelPresetsCameraPulldown::onRowClick, this, _2) }); buildFromFile( "panel_presets_camera_pulldown.xml"); } diff --git a/indra/newview/llpanelpresetspulldown.cpp b/indra/newview/llpanelpresetspulldown.cpp index 3b0f1273df..3d564ef540 100644 --- a/indra/newview/llpanelpresetspulldown.cpp +++ b/indra/newview/llpanelpresetspulldown.cpp @@ -50,9 +50,9 @@ LLPanelPresetsPulldown::LLPanelPresetsPulldown() { mHoverTimer.stop(); - mCommitCallbackRegistrar.add("Presets.GoGraphicsPrefs", boost::bind(&LLPanelPresetsPulldown::onGraphicsButtonClick, this, _2)); - mCommitCallbackRegistrar.add("Presets.GoAutofpsPrefs", boost::bind(&LLPanelPresetsPulldown::onAutofpsButtonClick, this, _2)); - mCommitCallbackRegistrar.add("Presets.RowClick", boost::bind(&LLPanelPresetsPulldown::onRowClick, this, _2)); + mCommitCallbackRegistrar.add("Presets.GoGraphicsPrefs", { boost::bind(&LLPanelPresetsPulldown::onGraphicsButtonClick, this, _2) }); + mCommitCallbackRegistrar.add("Presets.GoAutofpsPrefs", { boost::bind(&LLPanelPresetsPulldown::onAutofpsButtonClick, this, _2) }); + mCommitCallbackRegistrar.add("Presets.RowClick", { boost::bind(&LLPanelPresetsPulldown::onRowClick, this, _2) }); buildFromFile( "panel_presets_pulldown.xml"); } diff --git a/indra/newview/llpanelprimmediacontrols.cpp b/indra/newview/llpanelprimmediacontrols.cpp index 4e905ae0fd..9bef09527b 100644 --- a/indra/newview/llpanelprimmediacontrols.cpp +++ b/indra/newview/llpanelprimmediacontrols.cpp @@ -102,28 +102,28 @@ LLPanelPrimMediaControls::LLPanelPrimMediaControls() : mSecureURL(false), mMediaPlaySliderCtrlMouseDownValue(0.0) { - mCommitCallbackRegistrar.add("MediaCtrl.Close", boost::bind(&LLPanelPrimMediaControls::onClickClose, this)); - mCommitCallbackRegistrar.add("MediaCtrl.Back", boost::bind(&LLPanelPrimMediaControls::onClickBack, this)); - mCommitCallbackRegistrar.add("MediaCtrl.Forward", boost::bind(&LLPanelPrimMediaControls::onClickForward, this)); - mCommitCallbackRegistrar.add("MediaCtrl.Home", boost::bind(&LLPanelPrimMediaControls::onClickHome, this)); - mCommitCallbackRegistrar.add("MediaCtrl.Stop", boost::bind(&LLPanelPrimMediaControls::onClickStop, this)); - mCommitCallbackRegistrar.add("MediaCtrl.MediaStop", boost::bind(&LLPanelPrimMediaControls::onClickMediaStop, this)); - mCommitCallbackRegistrar.add("MediaCtrl.Reload", boost::bind(&LLPanelPrimMediaControls::onClickReload, this)); - mCommitCallbackRegistrar.add("MediaCtrl.Play", boost::bind(&LLPanelPrimMediaControls::onClickPlay, this)); - mCommitCallbackRegistrar.add("MediaCtrl.Pause", boost::bind(&LLPanelPrimMediaControls::onClickPause, this)); - mCommitCallbackRegistrar.add("MediaCtrl.Open", boost::bind(&LLPanelPrimMediaControls::onClickOpen, this)); - mCommitCallbackRegistrar.add("MediaCtrl.Zoom", boost::bind(&LLPanelPrimMediaControls::onClickZoom, this)); - mCommitCallbackRegistrar.add("MediaCtrl.CommitURL", boost::bind(&LLPanelPrimMediaControls::onCommitURL, this)); - mCommitCallbackRegistrar.add("MediaCtrl.MouseDown", boost::bind(&LLPanelPrimMediaControls::onMediaPlaySliderCtrlMouseDown, this)); - mCommitCallbackRegistrar.add("MediaCtrl.MouseUp", boost::bind(&LLPanelPrimMediaControls::onMediaPlaySliderCtrlMouseUp, this)); - mCommitCallbackRegistrar.add("MediaCtrl.CommitVolumeUp", boost::bind(&LLPanelPrimMediaControls::onCommitVolumeUp, this)); - mCommitCallbackRegistrar.add("MediaCtrl.CommitVolumeDown", boost::bind(&LLPanelPrimMediaControls::onCommitVolumeDown, this)); - mCommitCallbackRegistrar.add("MediaCtrl.Volume", boost::bind(&LLPanelPrimMediaControls::onCommitVolumeSlider, this)); - mCommitCallbackRegistrar.add("MediaCtrl.ToggleMute", boost::bind(&LLPanelPrimMediaControls::onToggleMute, this)); - mCommitCallbackRegistrar.add("MediaCtrl.ShowVolumeSlider", boost::bind(&LLPanelPrimMediaControls::showVolumeSlider, this)); - mCommitCallbackRegistrar.add("MediaCtrl.HideVolumeSlider", boost::bind(&LLPanelPrimMediaControls::hideVolumeSlider, this)); - mCommitCallbackRegistrar.add("MediaCtrl.SkipBack", boost::bind(&LLPanelPrimMediaControls::onClickSkipBack, this)); - mCommitCallbackRegistrar.add("MediaCtrl.SkipForward", boost::bind(&LLPanelPrimMediaControls::onClickSkipForward, this)); + mCommitCallbackRegistrar.add("MediaCtrl.Close", { boost::bind(&LLPanelPrimMediaControls::onClickClose, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.Back", { boost::bind(&LLPanelPrimMediaControls::onClickBack, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.Forward", { boost::bind(&LLPanelPrimMediaControls::onClickForward, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.Home", { boost::bind(&LLPanelPrimMediaControls::onClickHome, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.Stop", { boost::bind(&LLPanelPrimMediaControls::onClickStop, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.MediaStop", { boost::bind(&LLPanelPrimMediaControls::onClickMediaStop, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.Reload", { boost::bind(&LLPanelPrimMediaControls::onClickReload, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.Play", { boost::bind(&LLPanelPrimMediaControls::onClickPlay, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.Pause", { boost::bind(&LLPanelPrimMediaControls::onClickPause, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.Open", { boost::bind(&LLPanelPrimMediaControls::onClickOpen, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.Zoom", { boost::bind(&LLPanelPrimMediaControls::onClickZoom, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.CommitURL", { boost::bind(&LLPanelPrimMediaControls::onCommitURL, this), LLUICtrl::cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("MediaCtrl.MouseDown", { boost::bind(&LLPanelPrimMediaControls::onMediaPlaySliderCtrlMouseDown, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.MouseUp", { boost::bind(&LLPanelPrimMediaControls::onMediaPlaySliderCtrlMouseUp, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.CommitVolumeUp", { boost::bind(&LLPanelPrimMediaControls::onCommitVolumeUp, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.CommitVolumeDown", { boost::bind(&LLPanelPrimMediaControls::onCommitVolumeDown, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.Volume", { boost::bind(&LLPanelPrimMediaControls::onCommitVolumeSlider, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.ToggleMute", { boost::bind(&LLPanelPrimMediaControls::onToggleMute, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.ShowVolumeSlider", { boost::bind(&LLPanelPrimMediaControls::showVolumeSlider, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.HideVolumeSlider", { boost::bind(&LLPanelPrimMediaControls::hideVolumeSlider, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.SkipBack", { boost::bind(&LLPanelPrimMediaControls::onClickSkipBack, this) }); + mCommitCallbackRegistrar.add("MediaCtrl.SkipForward", { boost::bind(&LLPanelPrimMediaControls::onClickSkipForward, this) }); buildFromFile( "panel_prim_media_controls.xml"); mInactivityTimer.reset(); diff --git a/indra/newview/llpanelprofile.cpp b/indra/newview/llpanelprofile.cpp index 08605f7cf4..2086706bf8 100644 --- a/indra/newview/llpanelprofile.cpp +++ b/indra/newview/llpanelprofile.cpp @@ -772,7 +772,7 @@ void LLPanelProfileSecondLife::onOpen(const LLSD& key) } // Init menu, menu needs to be created in scope of a registar to work correctly. - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar commit; + ScopedRegistrarHelper commit; commit.add("Profile.Commit", [this](LLUICtrl*, const LLSD& userdata) { onCommitMenu(userdata); }); LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable; diff --git a/indra/newview/llpanelsnapshotinventory.cpp b/indra/newview/llpanelsnapshotinventory.cpp index 96b17acc40..5a7dd92b98 100644 --- a/indra/newview/llpanelsnapshotinventory.cpp +++ b/indra/newview/llpanelsnapshotinventory.cpp @@ -119,8 +119,8 @@ LLSnapshotModel::ESnapshotType LLPanelSnapshotInventoryBase::getSnapshotType() LLPanelSnapshotInventory::LLPanelSnapshotInventory() { - mCommitCallbackRegistrar.add("Inventory.Save", boost::bind(&LLPanelSnapshotInventory::onSend, this)); - mCommitCallbackRegistrar.add("Inventory.Cancel", boost::bind(&LLPanelSnapshotInventory::cancel, this)); + mCommitCallbackRegistrar.add("Inventory.Save", { boost::bind(&LLPanelSnapshotInventory::onSend, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.Cancel", { boost::bind(&LLPanelSnapshotInventory::cancel, this), cb_info::UNTRUSTED_BLOCK }); } // virtual @@ -190,8 +190,8 @@ void LLPanelSnapshotInventoryBase::onSend() LLPanelOutfitSnapshotInventory::LLPanelOutfitSnapshotInventory() { - mCommitCallbackRegistrar.add("Inventory.SaveOutfitPhoto", boost::bind(&LLPanelOutfitSnapshotInventory::onSend, this)); - mCommitCallbackRegistrar.add("Inventory.SaveOutfitCancel", boost::bind(&LLPanelOutfitSnapshotInventory::cancel, this)); + mCommitCallbackRegistrar.add("Inventory.SaveOutfitPhoto", { boost::bind(&LLPanelOutfitSnapshotInventory::onSend, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("Inventory.SaveOutfitCancel", { boost::bind(&LLPanelOutfitSnapshotInventory::cancel, this), cb_info::UNTRUSTED_BLOCK }); } // virtual diff --git a/indra/newview/llpanelsnapshotlocal.cpp b/indra/newview/llpanelsnapshotlocal.cpp index 366030c0fa..aea002bb91 100644 --- a/indra/newview/llpanelsnapshotlocal.cpp +++ b/indra/newview/llpanelsnapshotlocal.cpp @@ -75,7 +75,7 @@ static LLPanelInjector<LLPanelSnapshotLocal> panel_class("llpanelsnapshotlocal") LLPanelSnapshotLocal::LLPanelSnapshotLocal() { mLocalFormat = gSavedSettings.getS32("SnapshotFormat"); - mCommitCallbackRegistrar.add("Local.Cancel", boost::bind(&LLPanelSnapshotLocal::cancel, this)); + mCommitCallbackRegistrar.add("Local.Cancel", { boost::bind(&LLPanelSnapshotLocal::cancel, this) }); } // virtual diff --git a/indra/newview/llpanelsnapshotoptions.cpp b/indra/newview/llpanelsnapshotoptions.cpp index 962d3bba16..51fad92720 100644 --- a/indra/newview/llpanelsnapshotoptions.cpp +++ b/indra/newview/llpanelsnapshotoptions.cpp @@ -65,10 +65,10 @@ static LLPanelInjector<LLPanelSnapshotOptions> panel_class("llpanelsnapshotoptio LLPanelSnapshotOptions::LLPanelSnapshotOptions() { - mCommitCallbackRegistrar.add("Snapshot.SaveToProfile", boost::bind(&LLPanelSnapshotOptions::onSaveToProfile, this)); - mCommitCallbackRegistrar.add("Snapshot.SaveToEmail", boost::bind(&LLPanelSnapshotOptions::onSaveToEmail, this)); - mCommitCallbackRegistrar.add("Snapshot.SaveToInventory", boost::bind(&LLPanelSnapshotOptions::onSaveToInventory, this)); - mCommitCallbackRegistrar.add("Snapshot.SaveToComputer", boost::bind(&LLPanelSnapshotOptions::onSaveToComputer, this)); + mCommitCallbackRegistrar.add("Snapshot.SaveToProfile", { boost::bind(&LLPanelSnapshotOptions::onSaveToProfile, this) }); + mCommitCallbackRegistrar.add("Snapshot.SaveToEmail", { boost::bind(&LLPanelSnapshotOptions::onSaveToEmail, this) }); + mCommitCallbackRegistrar.add("Snapshot.SaveToInventory", { boost::bind(&LLPanelSnapshotOptions::onSaveToInventory, this) }); + mCommitCallbackRegistrar.add("Snapshot.SaveToComputer", { boost::bind(&LLPanelSnapshotOptions::onSaveToComputer, this) }); } LLPanelSnapshotOptions::~LLPanelSnapshotOptions() diff --git a/indra/newview/llpanelsnapshotpostcard.cpp b/indra/newview/llpanelsnapshotpostcard.cpp index 23e8789e3f..ac6d954091 100644 --- a/indra/newview/llpanelsnapshotpostcard.cpp +++ b/indra/newview/llpanelsnapshotpostcard.cpp @@ -86,8 +86,8 @@ static LLPanelInjector<LLPanelSnapshotPostcard> panel_class("llpanelsnapshotpost LLPanelSnapshotPostcard::LLPanelSnapshotPostcard() : mHasFirstMsgFocus(false) { - mCommitCallbackRegistrar.add("Postcard.Send", boost::bind(&LLPanelSnapshotPostcard::onSend, this)); - mCommitCallbackRegistrar.add("Postcard.Cancel", boost::bind(&LLPanelSnapshotPostcard::cancel, this)); + mCommitCallbackRegistrar.add("Postcard.Send", { boost::bind(&LLPanelSnapshotPostcard::onSend, this), cb_info::UNTRUSTED_THROTTLE }); + mCommitCallbackRegistrar.add("Postcard.Cancel", { boost::bind(&LLPanelSnapshotPostcard::cancel, this), cb_info::UNTRUSTED_THROTTLE }); } diff --git a/indra/newview/llpanelsnapshotprofile.cpp b/indra/newview/llpanelsnapshotprofile.cpp index aa257dea9e..35318461e8 100644 --- a/indra/newview/llpanelsnapshotprofile.cpp +++ b/indra/newview/llpanelsnapshotprofile.cpp @@ -68,8 +68,8 @@ static LLPanelInjector<LLPanelSnapshotProfile> panel_class("llpanelsnapshotprofi LLPanelSnapshotProfile::LLPanelSnapshotProfile() { - mCommitCallbackRegistrar.add("PostToProfile.Send", boost::bind(&LLPanelSnapshotProfile::onSend, this)); - mCommitCallbackRegistrar.add("PostToProfile.Cancel", boost::bind(&LLPanelSnapshotProfile::cancel, this)); + mCommitCallbackRegistrar.add("PostToProfile.Send", { boost::bind(&LLPanelSnapshotProfile::onSend, this), cb_info::UNTRUSTED_BLOCK }); + mCommitCallbackRegistrar.add("PostToProfile.Cancel", { boost::bind(&LLPanelSnapshotProfile::cancel, this), cb_info::UNTRUSTED_BLOCK }); } // virtual diff --git a/indra/newview/llpanelteleporthistory.cpp b/indra/newview/llpanelteleporthistory.cpp index 902412d359..ebfdafdfe2 100644 --- a/indra/newview/llpanelteleporthistory.cpp +++ b/indra/newview/llpanelteleporthistory.cpp @@ -402,7 +402,7 @@ LLTeleportHistoryPanel::~LLTeleportHistoryPanel() bool LLTeleportHistoryPanel::postBuild() { - mCommitCallbackRegistrar.add("TeleportHistory.GearMenu.Action", boost::bind(&LLTeleportHistoryPanel::onGearMenuAction, this, _2)); + mCommitCallbackRegistrar.add("TeleportHistory.GearMenu.Action", { boost::bind(&LLTeleportHistoryPanel::onGearMenuAction, this, _2), cb_info::UNTRUSTED_THROTTLE }); mEnableCallbackRegistrar.add("TeleportHistory.GearMenu.Enable", boost::bind(&LLTeleportHistoryPanel::isActionEnabled, this, _2)); // init menus before list, since menus are passed to list @@ -939,10 +939,9 @@ void LLTeleportHistoryPanel::onAccordionTabRightClick(LLView *view, S32 x, S32 y // set up the callbacks for all of the avatar menu items // (N.B. callbacks don't take const refs as mID is local scope) - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; - - registrar.add("TeleportHistory.TabOpen", boost::bind(&LLTeleportHistoryPanel::onAccordionTabOpen, this, tab)); - registrar.add("TeleportHistory.TabClose", boost::bind(&LLTeleportHistoryPanel::onAccordionTabClose, this, tab)); + ScopedRegistrarHelper registrar; + registrar.add("TeleportHistory.TabOpen", boost::bind(&LLTeleportHistoryPanel::onAccordionTabOpen, this, tab)); + registrar.add("TeleportHistory.TabClose", boost::bind(&LLTeleportHistoryPanel::onAccordionTabClose, this, tab)); // create the context menu from the XUI llassert(LLMenuGL::sMenuContainer != NULL); diff --git a/indra/newview/llpaneltopinfobar.cpp b/indra/newview/llpaneltopinfobar.cpp index e7ac11d570..d8a9de596d 100644 --- a/indra/newview/llpaneltopinfobar.cpp +++ b/indra/newview/llpaneltopinfobar.cpp @@ -133,7 +133,7 @@ bool LLPanelTopInfoBar::handleRightMouseDown(S32 x, S32 y, MASK mask) if(!LLUICtrl::CommitCallbackRegistry::getValue("TopInfoBar.Action")) { LLUICtrl::CommitCallbackRegistry::currentRegistrar() - .add("TopInfoBar.Action", boost::bind(&LLPanelTopInfoBar::onContextMenuItemClicked, this, _2)); + .add("TopInfoBar.Action", { boost::bind(&LLPanelTopInfoBar::onContextMenuItemClicked, this, _2) }); } show_topinfobar_context_menu(this, x, y); return true; diff --git a/indra/newview/llpanelvoiceeffect.cpp b/indra/newview/llpanelvoiceeffect.cpp index a0129b2cb1..47f898e3ed 100644 --- a/indra/newview/llpanelvoiceeffect.cpp +++ b/indra/newview/llpanelvoiceeffect.cpp @@ -42,7 +42,7 @@ static LLPanelInjector<LLPanelVoiceEffect> t_panel_voice_effect("panel_voice_eff LLPanelVoiceEffect::LLPanelVoiceEffect() : mVoiceEffectCombo(NULL) { - mCommitCallbackRegistrar.add("Voice.CommitVoiceEffect", boost::bind(&LLPanelVoiceEffect::onCommitVoiceEffect, this)); + mCommitCallbackRegistrar.add("Voice.CommitVoiceEffect", { boost::bind(&LLPanelVoiceEffect::onCommitVoiceEffect, this), cb_info::UNTRUSTED_BLOCK }); } LLPanelVoiceEffect::~LLPanelVoiceEffect() diff --git a/indra/newview/llpanelvolume.cpp b/indra/newview/llpanelvolume.cpp index 951dc45a78..adbf2ebf6e 100644 --- a/indra/newview/llpanelvolume.cpp +++ b/indra/newview/llpanelvolume.cpp @@ -223,7 +223,7 @@ LLPanelVolume::LLPanelVolume() { setMouseOpaque(false); - mCommitCallbackRegistrar.add("PanelVolume.menuDoToSelected", boost::bind(&LLPanelVolume::menuDoToSelected, this, _2)); + mCommitCallbackRegistrar.add("PanelVolume.menuDoToSelected", { boost::bind(&LLPanelVolume::menuDoToSelected, this, _2) }); mEnableCallbackRegistrar.add("PanelVolume.menuEnable", boost::bind(&LLPanelVolume::menuEnableItem, this, _2)); } diff --git a/indra/newview/llpanelvolumepulldown.cpp b/indra/newview/llpanelvolumepulldown.cpp index 046bcd7f59..05b21d4d48 100644 --- a/indra/newview/llpanelvolumepulldown.cpp +++ b/indra/newview/llpanelvolumepulldown.cpp @@ -48,10 +48,10 @@ // Default constructor LLPanelVolumePulldown::LLPanelVolumePulldown() { - mCommitCallbackRegistrar.add("Vol.setControlFalse", boost::bind(&LLPanelVolumePulldown::setControlFalse, this, _2)); - mCommitCallbackRegistrar.add("Vol.SetSounds", boost::bind(&LLPanelVolumePulldown::onClickSetSounds, this)); - mCommitCallbackRegistrar.add("Vol.updateCheckbox", boost::bind(&LLPanelVolumePulldown::updateCheckbox, this, _1, _2)); - mCommitCallbackRegistrar.add("Vol.GoAudioPrefs", boost::bind(&LLPanelVolumePulldown::onAdvancedButtonClick, this, _2)); + mCommitCallbackRegistrar.add("Vol.setControlFalse", { boost::bind(&LLPanelVolumePulldown::setControlFalse, this, _2) }); + mCommitCallbackRegistrar.add("Vol.SetSounds", { boost::bind(&LLPanelVolumePulldown::onClickSetSounds, this) }); + mCommitCallbackRegistrar.add("Vol.updateCheckbox", { boost::bind(&LLPanelVolumePulldown::updateCheckbox, this, _1, _2) }); + mCommitCallbackRegistrar.add("Vol.GoAudioPrefs", { boost::bind(&LLPanelVolumePulldown::onAdvancedButtonClick, this, _2) }); buildFromFile( "panel_volume_pulldown.xml"); } diff --git a/indra/newview/llpanelwearing.cpp b/indra/newview/llpanelwearing.cpp index c1534c9abd..1056f73d58 100644 --- a/indra/newview/llpanelwearing.cpp +++ b/indra/newview/llpanelwearing.cpp @@ -62,7 +62,7 @@ public: LLWearingGearMenu(LLPanelWearing* panel_wearing) : mMenu(NULL), mPanelWearing(panel_wearing) { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + LLUICtrl::ScopedRegistrarHelper registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; registrar.add("Gear.TouchAttach", boost::bind(&LLWearingGearMenu::handleMultiple, this, handle_attachment_touch)); @@ -103,7 +103,7 @@ class LLWearingContextMenu : public LLListContextMenu protected: /* virtual */ LLContextMenu* createMenu() { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + LLUICtrl::ScopedRegistrarHelper registrar; registrar.add("Wearing.TouchAttach", boost::bind(handleMultiple, handle_attachment_touch, mUUIDs)); registrar.add("Wearing.EditItem", boost::bind(handleMultiple, handle_item_edit, mUUIDs)); @@ -180,7 +180,7 @@ public: protected: /* virtual */ LLContextMenu* createMenu() { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + LLUICtrl::ScopedRegistrarHelper registrar; registrar.add("Wearing.EditItem", boost::bind(&LLPanelWearing::onEditAttachment, mPanelWearing)); registrar.add("Wearing.Detach", boost::bind(&LLPanelWearing::onRemoveAttachment, mPanelWearing)); diff --git a/indra/newview/llpreviewanim.cpp b/indra/newview/llpreviewanim.cpp index 7d11c09738..01a3902516 100644 --- a/indra/newview/llpreviewanim.cpp +++ b/indra/newview/llpreviewanim.cpp @@ -46,7 +46,7 @@ const S32 ADVANCED_VPAD = 3; LLPreviewAnim::LLPreviewAnim(const LLSD& key) : LLPreview( key ) { - mCommitCallbackRegistrar.add("PreviewAnim.Play", boost::bind(&LLPreviewAnim::play, this, _2)); + mCommitCallbackRegistrar.add("PreviewAnim.Play", { boost::bind(&LLPreviewAnim::play, this, _2) }); } // virtual diff --git a/indra/newview/llpreviewscript.cpp b/indra/newview/llpreviewscript.cpp index 02a4c7fb26..fa99432603 100644 --- a/indra/newview/llpreviewscript.cpp +++ b/indra/newview/llpreviewscript.cpp @@ -503,7 +503,7 @@ bool LLScriptEdCore::postBuild() LLSyntaxIdLSL::getInstance()->initialize(); processKeywords(); - mCommitCallbackRegistrar.add("FontSize.Set", boost::bind(&LLScriptEdCore::onChangeFontSize, this, _2)); + mCommitCallbackRegistrar.add("FontSize.Set", { boost::bind(&LLScriptEdCore::onChangeFontSize, this, _2) }); mEnableCallbackRegistrar.add("FontSize.Check", boost::bind(&LLScriptEdCore::isFontSizeChecked, this, _2)); LLToggleableMenu *context_menu = LLUICtrlFactory::getInstance()->createFromFile<LLToggleableMenu>( diff --git a/indra/newview/llsaveoutfitcombobtn.cpp b/indra/newview/llsaveoutfitcombobtn.cpp index 6418c310c1..bc1bdfdcda 100644 --- a/indra/newview/llsaveoutfitcombobtn.cpp +++ b/indra/newview/llsaveoutfitcombobtn.cpp @@ -39,7 +39,7 @@ LLSaveOutfitComboBtn::LLSaveOutfitComboBtn(LLPanel* parent, bool saveAsDefaultAc mParent(parent), mSaveAsDefaultAction(saveAsDefaultAction) { // register action mapping before creating menu - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar save_registar; + LLUICtrl::ScopedRegistrarHelper save_registar; save_registar.add("Outfit.Save.Action", boost::bind( &LLSaveOutfitComboBtn::saveOutfit, this, false)); save_registar.add("Outfit.SaveAs.Action", boost::bind( diff --git a/indra/newview/llsecapi.h b/indra/newview/llsecapi.h index ceea11cc34..31c6fa35f4 100644 --- a/indra/newview/llsecapi.h +++ b/indra/newview/llsecapi.h @@ -312,7 +312,7 @@ public: mIdentifier = identifier; mAuthenticator = authenticator; } - virtual LLSD getIdentifier() { return mIdentifier; } + virtual LLSD getIdentifier() const { return mIdentifier; } virtual void identifierType(std::string& idType); virtual LLSD getAuthenticator() { return mAuthenticator; } virtual void authenticatorType(std::string& authType); diff --git a/indra/newview/llsetkeybinddialog.cpp b/indra/newview/llsetkeybinddialog.cpp index 5dbd579b45..021a9feb73 100644 --- a/indra/newview/llsetkeybinddialog.cpp +++ b/indra/newview/llsetkeybinddialog.cpp @@ -46,14 +46,12 @@ public: :LLEventTimer(period), mMask(mask), mCallback(cb) - { - mEventTimer.start(); - } + {} virtual ~Updater(){} protected: - bool tick() + bool tick() override { mCallback(mMask); // Deletes itseft after execution diff --git a/indra/newview/llspeakers.h b/indra/newview/llspeakers.h index ad2461f60f..077a949c05 100644 --- a/indra/newview/llspeakers.h +++ b/indra/newview/llspeakers.h @@ -37,7 +37,7 @@ class LLSpeakerMgr; class LLAvatarName; // data for a given participant in a voice channel -class LLSpeaker : public LLRefCount, public LLOldEvents::LLObservable, public LLHandleProvider<LLSpeaker>, public boost::signals2::trackable +class LLSpeaker : public LLRefCount, public LLOldEvents::LLObservable, public LLHandleProvider<LLSpeaker> { public: typedef enum e_speaker_type @@ -159,7 +159,7 @@ public: * * If action callback is not specified returns true. Instance will be deleted by LLEventTimer::updateClass(). */ - virtual bool tick(); + bool tick() override; /** * Clears the callback. diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index b32b80331a..6e4a652989 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -403,10 +403,10 @@ bool idle_startup() static bool first_call = true; if (first_call) { + first_call = false; // Other phases get handled when startup state changes, // need to capture the initial state as well. LLStartUp::getPhases().startPhase(LLStartUp::getStartupStateString()); - first_call = false; } gViewerWindow->showCursor(); diff --git a/indra/newview/lltoast.cpp b/indra/newview/lltoast.cpp index 84503e66a5..43b7294b9a 100644 --- a/indra/newview/lltoast.cpp +++ b/indra/newview/lltoast.cpp @@ -43,34 +43,21 @@ LLToastLifeTimer::LLToastLifeTimer(LLToast* toast, F32 period) { } -/*virtual*/ bool LLToastLifeTimer::tick() { - if (mEventTimer.hasExpired()) - { - mToast->expire(); - } + mToast->expire(); return false; } -void LLToastLifeTimer::stop() -{ - mEventTimer.stop(); -} - -void LLToastLifeTimer::start() -{ - mEventTimer.start(); -} - void LLToastLifeTimer::restart() { - mEventTimer.reset(); + // start() discards any previously-running mTimer + start(); } bool LLToastLifeTimer::getStarted() { - return mEventTimer.getStarted(); + return isRunning(); } void LLToastLifeTimer::setPeriod(F32 period) @@ -78,13 +65,6 @@ void LLToastLifeTimer::setPeriod(F32 period) mPeriod = period; } -F32 LLToastLifeTimer::getRemainingTimeF32() -{ - F32 et = mEventTimer.getElapsedTimeF32(); - if (!getStarted() || et > mPeriod) return 0.0f; - return mPeriod - et; -} - //-------------------------------------------------------------------------- LLToast::Params::Params() : can_fade("can_fade", true), @@ -337,7 +317,7 @@ void LLToast::setFading(bool transparent) F32 LLToast::getTimeLeftToLive() { - F32 time_to_live = mTimer->getRemainingTimeF32(); + F32 time_to_live = mTimer->getRemaining(); if (!mIsFading) { diff --git a/indra/newview/lltoast.h b/indra/newview/lltoast.h index cf116bfadf..dd2ced7352 100644 --- a/indra/newview/lltoast.h +++ b/indra/newview/lltoast.h @@ -53,15 +53,11 @@ public: LLToastLifeTimer(LLToast* toast, F32 period); /*virtual*/ - bool tick(); - void stop(); - void start(); + bool tick() override; void restart(); bool getStarted(); void setPeriod(F32 period); - F32 getRemainingTimeF32(); - LLTimer& getEventTimer() { return mEventTimer;} private : LLToast* mToast; }; diff --git a/indra/newview/lltoastimpanel.cpp b/indra/newview/lltoastimpanel.cpp index 42ee30409d..df79af4926 100644 --- a/indra/newview/lltoastimpanel.cpp +++ b/indra/newview/lltoastimpanel.cpp @@ -87,7 +87,10 @@ LLToastIMPanel::LLToastIMPanel(LLToastIMPanel::Params &p) : LLToastPanel(p.notif { LLAvatarName avatar_name; LLAvatarNameCache::get(p.avatar_id, &avatar_name); - p.message = "[From " + avatar_name.getDisplayName() + "]\n" + p.message; + // move Lua prefix from the message field to the [From] field + auto [message, is_lua] = LLStringUtil::withoutPrefix(p.message, LUA_PREFIX); + std::string prefix = is_lua ? "LUA - " : ""; + p.message = "[From " + prefix + avatar_name.getDisplayName() + "]\n" + message; } style_params.font.style = "NORMAL"; mMessage->setText(p.message, style_params); diff --git a/indra/newview/lltoolmgr.cpp b/indra/newview/lltoolmgr.cpp index 07963a7bed..2ffb571201 100644 --- a/indra/newview/lltoolmgr.cpp +++ b/indra/newview/lltoolmgr.cpp @@ -84,9 +84,9 @@ LLToolMgr::LLToolMgr() LLUICtrl::EnableCallbackRegistry::currentRegistrar().add("Build.Active", boost::bind(&LLToolMgr::inEdit, this)); LLUICtrl::EnableCallbackRegistry::currentRegistrar().add("Build.Enabled", boost::bind(&LLToolMgr::canEdit, this)); LLUICtrl::EnableCallbackRegistry::currentRegistrar().add("Build.EnabledOrActive", boost::bind(&LLToolMgr::buildEnabledOrActive, this)); - LLUICtrl::CommitCallbackRegistry::currentRegistrar().add("Build.Toggle", boost::bind(&LLToolMgr::toggleBuildMode, this, _2)); + LLUICtrl::CommitCallbackRegistry::currentRegistrar().add("Build.Toggle", { boost::bind(&LLToolMgr::toggleBuildMode, this, _2) }); LLUICtrl::EnableCallbackRegistry::currentRegistrar().add("Marketplace.Enabled", boost::bind(&LLToolMgr::canAccessMarketplace, this)); - LLUICtrl::CommitCallbackRegistry::currentRegistrar().add("Marketplace.Toggle", boost::bind(&LLToolMgr::toggleMarketplace, this, _2)); + LLUICtrl::CommitCallbackRegistry::currentRegistrar().add("Marketplace.Toggle", { boost::bind(&LLToolMgr::toggleMarketplace, this, _2) }); gToolNull = new LLTool(LLStringUtil::null); // Does nothing setCurrentTool(gToolNull); diff --git a/indra/newview/lltoolplacer.cpp b/indra/newview/lltoolplacer.cpp index b15bb5efd5..2ac5d6b6b1 100644 --- a/indra/newview/lltoolplacer.cpp +++ b/indra/newview/lltoolplacer.cpp @@ -164,13 +164,17 @@ bool LLToolPlacer::addObject( LLPCode pcode, S32 x, S32 y, U8 use_physics ) bool b_hit_land = false; S32 hit_face = -1; LLViewerObject* hit_obj = NULL; - U8 state = 0; bool success = raycastForNewObjPos( x, y, &hit_obj, &hit_face, &b_hit_land, &ray_start_region, &ray_end_region, ®ionp ); if( !success ) { return false; } + return rezNewObject(pcode,hit_obj, hit_face, b_hit_land, ray_start_region, ray_end_region, regionp, use_physics); +} +bool LLToolPlacer::rezNewObject(LLPCode pcode, LLViewerObject * hit_obj, S32 hit_face, bool b_hit_land, LLVector3 ray_start_region, + LLVector3 ray_end_region, LLViewerRegion* regionp, U8 use_physics) +{ if( hit_obj && (hit_obj->isAvatar() || hit_obj->isAttachment()) ) { // Can't create objects on avatars or attachments @@ -195,6 +199,8 @@ bool LLToolPlacer::addObject( LLPCode pcode, S32 x, S32 y, U8 use_physics ) bool create_selected = false; LLVolumeParams volume_params; + U8 state = 0; + switch (pcode) { case LL_PCODE_LEGACY_GRASS: diff --git a/indra/newview/lltoolplacer.h b/indra/newview/lltoolplacer.h index f9501f83b2..92446c319b 100644 --- a/indra/newview/lltoolplacer.h +++ b/indra/newview/lltoolplacer.h @@ -50,6 +50,10 @@ public: static void setObjectType( LLPCode type ) { sObjectType = type; } static LLPCode getObjectType() { return sObjectType; } +// static bool addObject(LLPCode pcode, S32 x, S32 y, U8 use_physics); + static bool rezNewObject(LLPCode pcode, LLViewerObject* hit_obj, S32 hit_face, bool b_hit_land, LLVector3 ray_start_region, + LLVector3 ray_end_region, LLViewerRegion *regionp, U8 use_physics); + protected: static LLPCode sObjectType; diff --git a/indra/newview/lluilistener.cpp b/indra/newview/lluilistener.cpp index beae71e7bf..44eb8461f1 100644 --- a/indra/newview/lluilistener.cpp +++ b/indra/newview/lluilistener.cpp @@ -34,55 +34,195 @@ // std headers // external library headers // other Linden headers +#include "llmenugl.h" +#include "lltoolbarview.h" #include "llui.h" // getRootView(), resolvePath() #include "lluictrl.h" #include "llerror.h" +#include "llviewermenufile.h" // close_all_windows() +extern LLMenuBarGL* gMenuBarView; + +#define THROTTLE_PERIOD 1.5 // required seconds between throttled functions +#define MIN_THROTTLE 0.5 LLUIListener::LLUIListener(): LLEventAPI("UI", - "LLUICtrl::CommitCallbackRegistry listener.\n" - "Capable of invoking any function (with parameter) you can specify in XUI.") + "Operations to manipulate the viewer's user interface.") { add("call", "Invoke the operation named by [\"function\"], passing [\"parameter\"],\n" "as if from a user gesture on a menu -- or a button click.", &LLUIListener::call, - LLSD().with("function", LLSD())); + llsd::map("function", LLSD(), "reply", LLSD())); + + add("callables", + "Return a list [\"callables\"] of dicts {name, access} of functions registered to\n" + "invoke with \"call\".\n" + "access has values \"allow\", \"block\" or \"throttle\".", + &LLUIListener::callables, + llsd::map("reply", LLSD::String())); add("getValue", "For the UI control identified by the path in [\"path\"], return the control's\n" "current value as [\"value\"] reply.", &LLUIListener::getValue, - LLSDMap("path", LLSD())("reply", LLSD())); + llsd::map("path", LLSD(), "reply", LLSD())); + + add("getTopMenus", + "List names of Top menus suitable for passing as \"parent_menu\"", + &LLUIListener::getTopMenus, + llsd::map("reply", LLSD::String())); + + LLSD required_args = llsd::map("name", LLSD(), "label", LLSD(), "reply", LLSD()); + add("addMenu", + "Add new drop-down menu [\"name\"] with displayed [\"label\"] to the Top menu.", + &LLUIListener::addMenu, + required_args); + + required_args.insert("parent_menu", LLSD()); + add("addMenuBranch", + "Add new menu branch [\"name\"] with displayed [\"label\"]\n" + "to the [\"parent_menu\"] within the Top menu.", + &LLUIListener::addMenuBranch, + required_args); + + add("addMenuItem", + "Add new menu item [\"name\"] with displayed [\"label\"]\n" + "and call-on-click UI function [\"func\"] with optional [\"param\"]\n" + "to the [\"parent_menu\"] within the Top menu.\n" + "If [\"pos\"] is present, insert at specified 0-relative position.", + &LLUIListener::addMenuItem, + required_args.with("func", LLSD())); + + add("addMenuSeparator", + "Add menu separator to the [\"parent_menu\"] within the Top menu.\n" + "If [\"pos\"] is present, insert at specified 0-relative position.", + &LLUIListener::addMenuSeparator, + llsd::map("parent_menu", LLSD(), "reply", LLSD())); + + add("setMenuVisible", + "Set menu [\"name\"] visibility to [\"visible\"]", + &LLUIListener::setMenuVisible, + llsd::map("name", LLSD(), "visible", LLSD(), "reply", LLSD())); + + add("defaultToolbars", + "Restore default toolbar buttons", + &LLUIListener::restoreDefaultToolbars); + + add("clearAllToolbars", + "Clear all buttons off the toolbars", + &LLUIListener::clearAllToolbars); + + add("addToolbarBtn", + "Add [\"btn_name\"] toolbar button to the [\"toolbar\"]:\n" + "\"left\", \"right\", \"bottom\" (default is \"bottom\")\n" + "Position of the command in the original list can be specified as [\"rank\"],\n" + "where 0 means the first item", + &LLUIListener::addToolbarBtn, + llsd::map("btn_name", LLSD(), "reply", LLSD())); + + add("removeToolbarBtn", + "Remove [\"btn_name\"] toolbar button off the toolbar,\n" + "return [\"rank\"] (old position) of the command in the original list,\n" + "rank 0 is the first position,\n" + "rank -1 means that [\"btn_name\"] was not found", + &LLUIListener::removeToolbarBtn, + llsd::map("btn_name", LLSD(), "reply", LLSD())); + + add("getToolbarBtnNames", + "Return the table of Toolbar buttons names", + &LLUIListener::getToolbarBtnNames, + llsd::map("reply", LLSD())); + + add("closeAllFloaters", + "Close all the floaters", + &LLUIListener::closeAllFloaters); } -void LLUIListener::call(const LLSD& event) const +typedef LLUICtrl::CommitCallbackInfo cb_info; +void LLUIListener::call(const LLSD& event) { - LLUICtrl::commit_callback_t* func = - LLUICtrl::CommitCallbackRegistry::getValue(event["function"]); - if (! func) + Response response(LLSD(), event); + cb_info *info = LLUICtrl::CommitCallbackRegistry::getValue(event["function"]); + if (!info || !info->callback_func) + { + return response.error(stringize("Function ", std::quoted(event["function"].asString()), " was not found")); + } + if (info->handle_untrusted == cb_info::UNTRUSTED_BLOCK) + { + return response.error(stringize("Function ", std::quoted(event["function"].asString()), " may not be called from the script")); + } + + //Separate UNTRUSTED_THROTTLE and UNTRUSTED_ALLOW functions to have different timeout + F64 *throttlep, period; + if (info->handle_untrusted == cb_info::UNTRUSTED_THROTTLE) { - // This API is intended for use by a script. It's a fire-and-forget - // API: we provide no reply. Therefore, a typo in the script will - // provide no feedback whatsoever to that script. To rub the coder's - // nose in such an error, crump rather than quietly ignoring it. - LL_ERRS("LLUIListener") << "function '" << event["function"] << "' not found" << LL_ENDL; + throttlep = &mLastUntrustedThrottle; + period = THROTTLE_PERIOD; } else { - // Interestingly, view_listener_t::addMenu() (addCommit(), - // addEnable()) constructs a commit_callback_t callable that accepts - // two parameters but discards the first. Only the second is passed to - // handleEvent(). Therefore we feel completely safe passing NULL for - // the first parameter. - (*func)(NULL, event["parameter"]); + throttlep = &mLastMinThrottle; + period = MIN_THROTTLE; + } + + F64 cur_time = LLTimer::getElapsedSeconds(); + F64 time_delta = *throttlep + period; + if (cur_time < time_delta) + { + LL_WARNS("LLUIListener") << "Throttled function " << std::quoted(event["function"].asString()) << LL_ENDL; + return; } + *throttlep = cur_time; + + // Interestingly, view_listener_t::addMenu() (addCommit(), + // addEnable()) constructs a commit_callback_t callable that accepts + // two parameters but discards the first. Only the second is passed to + // handleEvent(). Therefore we feel completely safe passing NULL for + // the first parameter. + (info->callback_func)(NULL, event["parameter"]); } -void LLUIListener::getValue(const LLSD&event) const +void LLUIListener::callables(const LLSD& event) const { - LLSD reply = LLSD::emptyMap(); + Response response(LLSD(), event); + + using Registry = LLUICtrl::CommitCallbackRegistry; + using Method = Registry::Registrar& (*)(); + static Method registrars[] = + { + &Registry::defaultRegistrar, + &Registry::currentRegistrar, + }; + LLSD list; + for (auto method : registrars) + { + auto& registrar{ (*method)() }; + for (auto it = registrar.beginItems(), end = registrar.endItems(); it != end; ++it) + { + LLSD entry{ llsd::map("name", it->first) }; + switch (it->second.handle_untrusted) + { + case cb_info::UNTRUSTED_ALLOW: + entry["access"] = "allow"; + break; + case cb_info::UNTRUSTED_BLOCK: + entry["access"] = "block"; + break; + case cb_info::UNTRUSTED_THROTTLE: + entry["access"] = "throttle"; + break; + } + list.append(entry); + } + } + response["callables"] = list; +} + +void LLUIListener::getValue(const LLSD& event) const +{ + Response response(LLSD(), event); const LLView* root = LLUI::getInstance()->getRootView(); const LLView* view = LLUI::getInstance()->resolvePath(root, event["path"].asString()); @@ -90,12 +230,192 @@ void LLUIListener::getValue(const LLSD&event) const if (ctrl) { - reply["value"] = ctrl->getValue(); + response["value"] = ctrl->getValue(); } else { - // *TODO: ??? return something indicating failure to resolve + response.error(stringize("UI control ", std::quoted(event["path"].asString()), " was not found")); + } +} + +void LLUIListener::getTopMenus(const LLSD& event) const +{ + Response response(LLSD(), event); + response["menus"] = llsd::toArray( + *gMenuBarView->getChildList(), + [](auto childp) {return childp->getName(); }); +} + +LLMenuGL::Params get_params(const LLSD&event) +{ + LLMenuGL::Params item_params; + item_params.name = event["name"]; + item_params.label = event["label"]; + item_params.can_tear_off = true; + return item_params; +} + +LLMenuGL* get_parent_menu(LLEventAPI::Response& response, const LLSD&event) +{ + auto parent_menu_name{ event["parent_menu"].asString() }; + LLMenuGL* parent_menu = gMenuBarView->findChildMenuByName(parent_menu_name, true); + if(!parent_menu) + { + response.error(stringize("Parent menu ", std::quoted(parent_menu_name), " was not found")); + } + return parent_menu; +} + +// Return event["pos"].asInteger() if passed, but clamp (0 <= pos <= size). +// Reserve -1 return to mean event has no "pos" key. +S32 get_pos(const LLSD& event, U32 size) +{ + return event["pos"].isInteger()? llclamp(event["pos"].asInteger(), 0, size) : -1; +} + +void LLUIListener::addMenu(const LLSD&event) const +{ + Response response(LLSD(), event); + LLMenuGL::Params item_params = get_params(event); + if(!gMenuBarView->appendMenu(LLUICtrlFactory::create<LLMenuGL>(item_params))) + { + response.error(stringize("Menu ", std::quoted(event["name"].asString()), " was not added")); + } +} + +void LLUIListener::addMenuBranch(const LLSD&event) const +{ + Response response(LLSD(), event); + if(LLMenuGL* parent_menu = get_parent_menu(response, event)) + { + LLMenuGL::Params item_params = get_params(event); + if(!parent_menu->appendMenu(LLUICtrlFactory::create<LLMenuGL>(item_params))) + { + response.error(stringize("Menu branch ", std::quoted(event["name"].asString()), " was not added")); + } + } +} + +void LLUIListener::addMenuItem(const LLSD&event) const +{ + Response response(LLSD(), event); + LLMenuItemCallGL::Params item_params; + item_params.name = event["name"]; + item_params.label = event["label"]; + LLUICtrl::CommitCallbackParam item_func; + item_func.function_name = event["func"]; + if (event.has("param")) + { + item_func.parameter = event["param"]; } + item_params.on_click = item_func; + if(LLMenuGL* parent_menu = get_parent_menu(response, event)) + { + auto item = LLUICtrlFactory::create<LLMenuItemCallGL>(item_params); + // Clamp pos to getItemCount(), meaning append. If pos exceeds that, + // insert() will silently ignore the request. + auto pos = get_pos(event, parent_menu->getItemCount()); + if (pos >= 0) + { + // insert() returns void: we just have to assume it worked. + parent_menu->insert(pos, item); + } + else if (! parent_menu->append(item)) + { + response.error(stringize("Menu item ", std::quoted(event["name"].asString()), + " was not added")); + } + } +} + +void LLUIListener::addMenuSeparator(const LLSD&event) const +{ + Response response(LLSD(), event); + if(LLMenuGL* parent_menu = get_parent_menu(response, event)) + { + // Clamp pos to getItemCount(), meaning append. If pos exceeds that, + // insert() will silently ignore the request. + auto pos = get_pos(event, parent_menu->getItemCount()); + if (pos >= 0) + { + // Even though addSeparator() does not accept a position, + // LLMenuItemSeparatorGL isa LLMenuItemGL, so we can use insert(). + LLMenuItemSeparatorGL::Params p; + LLMenuItemGL* separator = LLUICtrlFactory::create<LLMenuItemSeparatorGL>(p); + // insert() returns void: we just have to assume it worked. + parent_menu->insert(pos, separator); + } + else if (! parent_menu->addSeparator()) + { + response.error("Separator was not added"); + } + } +} - sendReply(reply, event); +void LLUIListener::setMenuVisible(const LLSD &event) const +{ + Response response(LLSD(), event); + std::string menu_name(event["name"]); + if (!gMenuBarView->getItem(menu_name)) + { + return response.error(stringize("Menu ", std::quoted(menu_name), " was not found")); + } + gMenuBarView->setItemVisible(menu_name, event["visible"].asBoolean()); +} + +void LLUIListener::restoreDefaultToolbars(const LLSD &event) const +{ + LLToolBarView::loadDefaultToolbars(); +} + +void LLUIListener::clearAllToolbars(const LLSD &event) const +{ + LLToolBarView::clearAllToolbars(); +} + +void LLUIListener::addToolbarBtn(const LLSD &event) const +{ + Response response(LLSD(), event); + + typedef LLToolBarEnums::EToolBarLocation ToolBarLocation; + ToolBarLocation toolbar = ToolBarLocation::TOOLBAR_BOTTOM; + if (event.has("toolbar")) + { + if (event["toolbar"] == "left") + { + toolbar = ToolBarLocation::TOOLBAR_LEFT; + } + else if (event["toolbar"] == "right") + { + toolbar = ToolBarLocation::TOOLBAR_RIGHT; + } + else if (event["toolbar"] != "bottom") + { + return response.error(stringize("Toolbar name ", std::quoted(event["toolbar"].asString()), " is not correct. Toolbar names are: left, right, bottom")); + } + } + S32 rank = event.has("rank") ? event["rank"].asInteger() : LLToolBar::RANK_NONE; + if(!gToolBarView->addCommand(event["btn_name"].asString(), toolbar, rank)) + { + response.error(stringize("Toolbar button ", std::quoted(event["btn_name"].asString()), " was not found")); + } +} + +void LLUIListener::removeToolbarBtn(const LLSD &event) const +{ + Response response(LLSD(), event); + + S32 old_rank = LLToolBar::RANK_NONE; + gToolBarView->removeCommand(event["btn_name"].asString(), old_rank); + response["rank"] = old_rank; +} + +void LLUIListener::getToolbarBtnNames(const LLSD &event) const +{ + Response response(llsd::map("cmd_names", LLCommandManager::instance().getCommandNames()), event); +} + +void LLUIListener::closeAllFloaters(const LLSD &event) const +{ + close_all_windows(); } diff --git a/indra/newview/lluilistener.h b/indra/newview/lluilistener.h index 70455c2c68..8fc8e6ebb4 100644 --- a/indra/newview/lluilistener.h +++ b/indra/newview/lluilistener.h @@ -40,8 +40,27 @@ public: LLUIListener(); private: - void call(const LLSD& event) const; - void getValue(const LLSD&event) const; + void call(const LLSD& event); + void callables(const LLSD& event) const; + void getValue(const LLSD& event) const; + void getTopMenus(const LLSD& event) const; + + void addMenu(const LLSD&event) const; + void addMenuBranch(const LLSD&event) const; + void addMenuItem(const LLSD&event) const; + void addMenuSeparator(const LLSD&event) const; + void setMenuVisible(const LLSD &event) const; + + void restoreDefaultToolbars(const LLSD &event) const; + void clearAllToolbars(const LLSD &event) const; + void addToolbarBtn(const LLSD &event) const; + void removeToolbarBtn(const LLSD &event) const; + void getToolbarBtnNames(const LLSD &event) const; + + void closeAllFloaters(const LLSD &event) const; + + F64 mLastUntrustedThrottle {0}; + F64 mLastMinThrottle {0}; }; #endif /* ! defined(LL_LLUILISTENER_H) */ diff --git a/indra/newview/llviewerchat.cpp b/indra/newview/llviewerchat.cpp index 8b01c4ef88..5daccb6784 100644 --- a/indra/newview/llviewerchat.cpp +++ b/indra/newview/llviewerchat.cpp @@ -30,6 +30,7 @@ // newview includes #include "llagent.h" // gAgent #include "llslurl.h" +#include "lltrans.h" #include "lluicolor.h" #include "lluicolortable.h" #include "llviewercontrol.h" // gSavedSettings @@ -220,8 +221,7 @@ S32 LLViewerChat::getChatFontSize() //static void LLViewerChat::formatChatMsg(const LLChat& chat, std::string& formated_msg) { - std::string tmpmsg = chat.mText; - + std::string tmpmsg = without_LUA_PREFIX(chat.mText, chat.mIsScript); if(chat.mChatStyle == CHAT_STYLE_IRC) { formated_msg = chat.mFromName + tmpmsg.substr(3); @@ -231,6 +231,11 @@ void LLViewerChat::formatChatMsg(const LLChat& chat, std::string& formated_msg) formated_msg = tmpmsg; } + if (chat.mIsScript) + { + formated_msg = LLTrans::getString("ScriptStr") + formated_msg; + } + } //static diff --git a/indra/newview/llviewercontrollistener.cpp b/indra/newview/llviewercontrollistener.cpp index 6f77e21fcc..4e39b03903 100644 --- a/indra/newview/llviewercontrollistener.cpp +++ b/indra/newview/llviewercontrollistener.cpp @@ -141,7 +141,8 @@ void LLViewerControlListener::set(LLSD const & request) if (request.has("value")) { - info.control->setValue(request["value"]); + LL_WARNS("LLViewerControlListener") << "Changing debug setting " << std::quoted(info.key) << " to " << request["value"] << LL_ENDL; + info.control->setValue(request["value"], false); } else { @@ -158,7 +159,9 @@ void LLViewerControlListener::toggle(LLSD const & request) if (info.control->isType(TYPE_BOOLEAN)) { - info.control->set(! info.control->get().asBoolean()); + bool value = !info.control->get().asBoolean(); + LL_WARNS("LLViewerControlListener") << "Toggling debug setting " << std::quoted(info.key) << " to " << value << LL_ENDL; + info.control->set(value, false); } else { diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index ef68609182..8919e1f375 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -96,6 +96,8 @@ #include "llfloaterlandholdings.h" #include "llfloaterlinkreplace.h" #include "llfloaterloadprefpreset.h" +#include "llfloaterluadebug.h" +#include "llfloaterluascripts.h" #include "llfloatermap.h" #include "llfloatermarketplacelistings.h" #include "llfloatermediasettings.h" @@ -412,6 +414,9 @@ void LLViewerFloaterReg::registerFloaters() LLFloaterReg::add("linkreplace", "floater_linkreplace.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterLinkReplace>); LLFloaterReg::add("load_pref_preset", "floater_load_pref_preset.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterLoadPrefPreset>); + LLFloaterReg::add("lua_debug", "floater_lua_debug.xml", (LLFloaterBuildFunc) &LLFloaterReg::build<LLFloaterLUADebug>); + LLFloaterReg::add("lua_scripts", "floater_lua_scripts.xml", (LLFloaterBuildFunc) &LLFloaterReg::build<LLFloaterLUAScripts>); + LLFloaterReg::add("mem_leaking", "floater_mem_leaking.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterMemLeak>); LLFloaterReg::add("media_settings", "floater_media_settings.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterMediaSettings>); diff --git a/indra/newview/llviewerinventory.cpp b/indra/newview/llviewerinventory.cpp index e2022cae37..4c9910fd0a 100644 --- a/indra/newview/llviewerinventory.cpp +++ b/indra/newview/llviewerinventory.cpp @@ -71,6 +71,9 @@ #include "llclipboard.h" #include "llhttpretrypolicy.h" #include "llsettingsvo.h" +#include "llinventorylistener.h" + +LLInventoryListener sInventoryListener; // do-nothing ops for use in callbacks. void no_op_inventory_func(const LLUUID&) {} diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index d8c4e03d93..a5d199d9d7 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -179,6 +179,8 @@ extern bool gShaderProfileFrame; // Globals // +LLUIListener sUIListener; + LLMenuBarGL *gMenuBarView = NULL; LLViewerMenuHolderGL *gMenuHolder = NULL; LLMenuGL *gPopupMenuView = NULL; @@ -361,8 +363,6 @@ private: static LLMenuParcelObserver* gMenuParcelObserver = NULL; -static LLUIListener sUIListener; - LLMenuParcelObserver::LLMenuParcelObserver() { mLandBuyHandle = gMenuLand->getChild<LLMenuItemCallGL>("Land Buy")->getHandle(); @@ -9342,80 +9342,89 @@ class LLToolsSelectTool : public view_listener_t }; /// WINDLIGHT callbacks -class LLWorldEnvSettings : public view_listener_t +void defocusEnvFloaters() { - void defocusEnvFloaters() + // currently there is only one instance of each floater + std::vector<std::string> env_floaters_names = {"env_edit_extdaycycle", "env_fixed_environmentent_water", + "env_fixed_environmentent_sky"}; + for (std::vector<std::string>::const_iterator it = env_floaters_names.begin(); it != env_floaters_names.end(); ++it) { - //currently there is only one instance of each floater - std::vector<std::string> env_floaters_names = { "env_edit_extdaycycle", "env_fixed_environmentent_water", "env_fixed_environmentent_sky" }; - for (std::vector<std::string>::const_iterator it = env_floaters_names.begin(); it != env_floaters_names.end(); ++it) + LLFloater *env_floater = LLFloaterReg::findTypedInstance<LLFloater>(*it); + if (env_floater) { - LLFloater* env_floater = LLFloaterReg::findTypedInstance<LLFloater>(*it); - if (env_floater) - { - env_floater->setFocus(false); - } + env_floater->setFocus(false); } } +} - bool handleEvent(const LLSD& userdata) +bool handle_env_setting_event(std::string event_name) +{ + if (event_name == "sunrise") { - std::string event_name = userdata.asString(); - - if (event_name == "sunrise") - { - LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_SUNRISE, LLEnvironment::TRANSITION_INSTANT); - LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT); - defocusEnvFloaters(); - } - else if (event_name == "noon") - { - LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_MIDDAY, LLEnvironment::TRANSITION_INSTANT); - LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT); - defocusEnvFloaters(); - } - else if (event_name == "legacy noon") - { - LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_LEGACY_MIDDAY, LLEnvironment::TRANSITION_INSTANT); - LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT); - defocusEnvFloaters(); - } - else if (event_name == "sunset") - { - LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_SUNSET, LLEnvironment::TRANSITION_INSTANT); - LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT); - defocusEnvFloaters(); - } - else if (event_name == "midnight") - { - LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_MIDNIGHT, LLEnvironment::TRANSITION_INSTANT); - LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT); - defocusEnvFloaters(); - } - else if (event_name == "region") - { - // reset probe data when reverting back to region sky setting - gPipeline.mReflectionMapManager.reset(); + LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_SUNRISE, + LLEnvironment::TRANSITION_INSTANT); + LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT); + defocusEnvFloaters(); + } + else if (event_name == "noon") + { + LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_MIDDAY, + LLEnvironment::TRANSITION_INSTANT); + LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT); + defocusEnvFloaters(); + } + else if (event_name == "legacy noon") + { + LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_LEGACY_MIDDAY, LLEnvironment::TRANSITION_INSTANT); + LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT); + defocusEnvFloaters(); + } + else if (event_name == "sunset") + { + LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_SUNSET, + LLEnvironment::TRANSITION_INSTANT); + LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT); + defocusEnvFloaters(); + } + else if (event_name == "midnight") + { + LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_MIDNIGHT, + LLEnvironment::TRANSITION_INSTANT); + LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT); + defocusEnvFloaters(); + } + else if (event_name == "region") + { + // reset probe data when reverting back to region sky setting + gPipeline.mReflectionMapManager.reset(); - LLEnvironment::instance().clearEnvironment(LLEnvironment::ENV_LOCAL); - LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT); - defocusEnvFloaters(); - } - else if (event_name == "pause_clouds") - { - if (LLEnvironment::instance().isCloudScrollPaused()) - LLEnvironment::instance().resumeCloudScroll(); + LLEnvironment::instance().clearEnvironment(LLEnvironment::ENV_LOCAL); + LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT); + defocusEnvFloaters(); + } + else if (event_name == "pause_clouds") + { + if (LLEnvironment::instance().isCloudScrollPaused()) + LLEnvironment::instance().resumeCloudScroll(); else - LLEnvironment::instance().pauseCloudScroll(); - } - else if (event_name == "adjust_tool") - { - LLFloaterReg::showInstance("env_adjust_snapshot"); - } - else if (event_name == "my_environs") - { - LLFloaterReg::showInstance("my_environments"); - } + LLEnvironment::instance().pauseCloudScroll(); + } + else if (event_name == "adjust_tool") + { + LLFloaterReg::showInstance("env_adjust_snapshot"); + } + else if (event_name == "my_environs") + { + LLFloaterReg::showInstance("my_environments"); + } + return true; +} + +class LLWorldEnvSettings : public view_listener_t +{ + bool handleEvent(const LLSD& userdata) + { + handle_env_setting_event(userdata.asString()); return true; } @@ -9701,16 +9710,18 @@ void initialize_edit_menu() } +typedef LLUICtrl::CommitCallbackInfo cb_info; + void initialize_spellcheck_menu() { - LLUICtrl::CommitCallbackRegistry::Registrar& commit = LLUICtrl::CommitCallbackRegistry::currentRegistrar(); + LLUICtrl::CommitRegistrarHelper registrar(LLUICtrl::CommitCallbackRegistry::defaultRegistrar()); LLUICtrl::EnableCallbackRegistry::Registrar& enable = LLUICtrl::EnableCallbackRegistry::currentRegistrar(); - commit.add("SpellCheck.ReplaceWithSuggestion", boost::bind(&handle_spellcheck_replace_with_suggestion, _1, _2)); + registrar.add("SpellCheck.ReplaceWithSuggestion", boost::bind(&handle_spellcheck_replace_with_suggestion, _1, _2), cb_info::UNTRUSTED_BLOCK); enable.add("SpellCheck.VisibleSuggestion", boost::bind(&visible_spellcheck_suggestion, _1, _2)); - commit.add("SpellCheck.AddToDictionary", boost::bind(&handle_spellcheck_add_to_dictionary, _1)); + registrar.add("SpellCheck.AddToDictionary", boost::bind(&handle_spellcheck_add_to_dictionary, _1), cb_info::UNTRUSTED_BLOCK); enable.add("SpellCheck.EnableAddToDictionary", boost::bind(&enable_spellcheck_add_to_dictionary, _1)); - commit.add("SpellCheck.AddToIgnore", boost::bind(&handle_spellcheck_add_to_ignore, _1)); + registrar.add("SpellCheck.AddToIgnore", boost::bind(&handle_spellcheck_add_to_ignore, _1), cb_info::UNTRUSTED_BLOCK); enable.add("SpellCheck.EnableAddToIgnore", boost::bind(&enable_spellcheck_add_to_ignore, _1)); } @@ -9734,8 +9745,8 @@ void initialize_menus() bool mMult; }; + LLUICtrl::CommitRegistrarHelper registrar(LLUICtrl::CommitCallbackRegistry::currentRegistrar()); LLUICtrl::EnableCallbackRegistry::Registrar& enable = LLUICtrl::EnableCallbackRegistry::currentRegistrar(); - LLUICtrl::CommitCallbackRegistry::Registrar& commit = LLUICtrl::CommitCallbackRegistry::currentRegistrar(); // Generic enable and visible // Don't prepend MenuName.Foo because these can be used in any menu. @@ -9750,11 +9761,11 @@ void initialize_menus() enable.add("Conversation.IsConversationLoggingAllowed", boost::bind(&LLFloaterIMContainer::isConversationLoggingAllowed)); // Agent - commit.add("Agent.toggleFlying", boost::bind(&LLAgent::toggleFlying)); + registrar.add("Agent.toggleFlying", boost::bind(&LLAgent::toggleFlying)); enable.add("Agent.enableFlyLand", boost::bind(&enable_fly_land)); - commit.add("Agent.PressMicrophone", boost::bind(&LLAgent::pressMicrophone, _2)); - commit.add("Agent.ReleaseMicrophone", boost::bind(&LLAgent::releaseMicrophone, _2)); - commit.add("Agent.ToggleMicrophone", boost::bind(&LLAgent::toggleMicrophone, _2)); + registrar.add("Agent.PressMicrophone", boost::bind(&LLAgent::pressMicrophone, _2), cb_info::UNTRUSTED_BLOCK); + registrar.add("Agent.ReleaseMicrophone", boost::bind(&LLAgent::releaseMicrophone, _2), cb_info::UNTRUSTED_BLOCK); + registrar.add("Agent.ToggleMicrophone", boost::bind(&LLAgent::toggleMicrophone, _2), cb_info::UNTRUSTED_BLOCK); enable.add("Agent.IsMicrophoneOn", boost::bind(&LLAgent::isMicrophoneOn, _2)); enable.add("Agent.IsActionAllowed", boost::bind(&LLAgent::isActionAllowed, _2)); commit.add("Agent.ToggleHearMediaSoundFromAvatar", boost::bind(&LLAgent::toggleHearMediaSoundFromAvatar)); @@ -9768,12 +9779,12 @@ void initialize_menus() view_listener_t::addMenu(new LLEnableEditShape(), "Edit.EnableEditShape"); view_listener_t::addMenu(new LLEnableHoverHeight(), "Edit.EnableHoverHeight"); view_listener_t::addMenu(new LLEnableEditPhysics(), "Edit.EnableEditPhysics"); - commit.add("CustomizeAvatar", boost::bind(&handle_customize_avatar)); - commit.add("NowWearing", boost::bind(&handle_now_wearing)); - commit.add("EditOutfit", boost::bind(&handle_edit_outfit)); - commit.add("EditShape", boost::bind(&handle_edit_shape)); - commit.add("HoverHeight", boost::bind(&handle_hover_height)); - commit.add("EditPhysics", boost::bind(&handle_edit_physics)); + registrar.add("CustomizeAvatar", boost::bind(&handle_customize_avatar)); + registrar.add("NowWearing", boost::bind(&handle_now_wearing)); + registrar.add("EditOutfit", boost::bind(&handle_edit_outfit)); + registrar.add("EditShape", boost::bind(&handle_edit_shape)); + registrar.add("HoverHeight", boost::bind(&handle_hover_height)); + registrar.add("EditPhysics", boost::bind(&handle_edit_physics)); // View menu view_listener_t::addMenu(new LLViewMouselook(), "View.Mouselook"); @@ -9843,14 +9854,14 @@ void initialize_menus() view_listener_t::addMenu(new LLToolsSnapObjectXY(), "Tools.SnapObjectXY"); view_listener_t::addMenu(new LLToolsUseSelectionForGrid(), "Tools.UseSelectionForGrid"); view_listener_t::addMenu(new LLToolsSelectNextPartFace(), "Tools.SelectNextPart"); - commit.add("Tools.Link", boost::bind(&handle_link_objects)); - commit.add("Tools.Unlink", boost::bind(&LLSelectMgr::unlinkObjects, LLSelectMgr::getInstance())); + registrar.add("Tools.Link", boost::bind(&handle_link_objects)); + registrar.add("Tools.Unlink", boost::bind(&LLSelectMgr::unlinkObjects, LLSelectMgr::getInstance())); view_listener_t::addMenu(new LLToolsStopAllAnimations(), "Tools.StopAllAnimations"); view_listener_t::addMenu(new LLToolsReleaseKeys(), "Tools.ReleaseKeys"); view_listener_t::addMenu(new LLToolsEnableReleaseKeys(), "Tools.EnableReleaseKeys"); - commit.add("Tools.LookAtSelection", boost::bind(&handle_look_at_selection, _2)); - commit.add("Tools.BuyOrTake", boost::bind(&handle_buy_or_take)); - commit.add("Tools.TakeCopy", boost::bind(&handle_take_copy)); + registrar.add("Tools.LookAtSelection", boost::bind(&handle_look_at_selection, _2)); + registrar.add("Tools.BuyOrTake", boost::bind(&handle_buy_or_take), cb_info::UNTRUSTED_THROTTLE); + registrar.add("Tools.TakeCopy", boost::bind(&handle_take_copy), cb_info::UNTRUSTED_THROTTLE); view_listener_t::addMenu(new LLToolsSaveToObjectInventory(), "Tools.SaveToObjectInventory"); view_listener_t::addMenu(new LLToolsSelectedScriptAction(), "Tools.SelectedScriptAction"); @@ -9901,7 +9912,7 @@ void initialize_menus() view_listener_t::addMenu(new LLAdvancedToggleInfoDisplay(), "Advanced.ToggleInfoDisplay"); view_listener_t::addMenu(new LLAdvancedCheckInfoDisplay(), "Advanced.CheckInfoDisplay"); view_listener_t::addMenu(new LLAdvancedSelectedTextureInfo(), "Advanced.SelectedTextureInfo"); - commit.add("Advanced.SelectedMaterialInfo", boost::bind(&handle_selected_material_info)); + registrar.add("Advanced.SelectedMaterialInfo", boost::bind(&handle_selected_material_info)); view_listener_t::addMenu(new LLAdvancedToggleWireframe(), "Advanced.ToggleWireframe"); view_listener_t::addMenu(new LLAdvancedCheckWireframe(), "Advanced.CheckWireframe"); // Develop > Render @@ -9914,13 +9925,14 @@ void initialize_menus() view_listener_t::addMenu(new LLAdvancedClickRenderShadowOption(), "Advanced.ClickRenderShadowOption"); view_listener_t::addMenu(new LLAdvancedClickRenderProfile(), "Advanced.ClickRenderProfile"); view_listener_t::addMenu(new LLAdvancedClickRenderBenchmark(), "Advanced.ClickRenderBenchmark"); - view_listener_t::addMenu(new LLAdvancedClickHDRIPreview(), "Advanced.ClickHDRIPreview"); - view_listener_t::addMenu(new LLAdvancedClickGLTFOpen(), "Advanced.ClickGLTFOpen"); - view_listener_t::addMenu(new LLAdvancedClickGLTFSaveAs(), "Advanced.ClickGLTFSaveAs"); - view_listener_t::addMenu(new LLAdvancedClickGLTFUpload(), "Advanced.ClickGLTFUpload"); - view_listener_t::addMenu(new LLAdvancedClickGLTFEdit(), "Advanced.ClickGLTFEdit"); - view_listener_t::addMenu(new LLAdvancedClickResizeWindow(), "Advanced.ClickResizeWindow"); - view_listener_t::addMenu(new LLAdvancedPurgeShaderCache(), "Advanced.ClearShaderCache"); + view_listener_t::addMenu(new LLAdvancedClickHDRIPreview(), "Advanced.ClickHDRIPreview", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedClickGLTFOpen(), "Advanced.ClickGLTFOpen", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedClickGLTFSaveAs(), "Advanced.ClickGLTFSaveAs", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedClickGLTFUpload(), "Advanced.ClickGLTFUpload", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedClickGLTFEdit(), "Advanced.ClickGLTFEdit", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedClickResizeWindow(), "Advanced.ClickResizeWindow", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedPurgeShaderCache(), "Advanced.ClearShaderCache", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedRebuildTerrain(), "Advanced.RebuildTerrain", cb_info::UNTRUSTED_BLOCK); #ifdef TOGGLE_HACKED_GODLIKE_VIEWER view_listener_t::addMenu(new LLAdvancedHandleToggleHackedGodmode(), "Advanced.HandleToggleHackedGodmode"); @@ -9943,15 +9955,15 @@ void initialize_menus() view_listener_t::addMenu(new LLAdvancedTerrainDeleteLocalPaintMap(), "Advanced.TerrainDeleteLocalPaintMap"); // Advanced > UI - commit.add("Advanced.WebBrowserTest", boost::bind(&handle_web_browser_test, _2)); // sigh! this one opens the MEDIA browser - commit.add("Advanced.WebContentTest", boost::bind(&handle_web_content_test, _2)); // this one opens the Web Content floater - commit.add("Advanced.ShowURL", boost::bind(&handle_show_url, _2)); - commit.add("Advanced.ReportBug", boost::bind(&handle_report_bug, _2)); + registrar.add("Advanced.WebBrowserTest", boost::bind(&handle_web_browser_test, _2)); // sigh! this one opens the MEDIA browser + registrar.add("Advanced.WebContentTest", boost::bind(&handle_web_content_test, _2)); // this one opens the Web Content floater + registrar.add("Advanced.ShowURL", boost::bind(&handle_show_url, _2)); + registrar.add("Advanced.ReportBug", boost::bind(&handle_report_bug, _2)); view_listener_t::addMenu(new LLAdvancedBuyCurrencyTest(), "Advanced.BuyCurrencyTest"); view_listener_t::addMenu(new LLAdvancedDumpSelectMgr(), "Advanced.DumpSelectMgr"); view_listener_t::addMenu(new LLAdvancedDumpInventory(), "Advanced.DumpInventory"); - commit.add("Advanced.DumpTimers", boost::bind(&handle_dump_timers) ); - commit.add("Advanced.DumpFocusHolder", boost::bind(&handle_dump_focus) ); + registrar.add("Advanced.DumpTimers", boost::bind(&handle_dump_timers), cb_info::UNTRUSTED_THROTTLE); + registrar.add("Advanced.DumpFocusHolder", boost::bind(&handle_dump_focus)); view_listener_t::addMenu(new LLAdvancedPrintSelectedObjectInfo(), "Advanced.PrintSelectedObjectInfo"); view_listener_t::addMenu(new LLAdvancedPrintAgentInfo(), "Advanced.PrintAgentInfo"); view_listener_t::addMenu(new LLAdvancedToggleDebugClicks(), "Advanced.ToggleDebugClicks"); @@ -9972,11 +9984,11 @@ void initialize_menus() view_listener_t::addMenu(new LLAdvancedCheckDebugWindowProc(), "Advanced.CheckDebugWindowProc"); // Advanced > XUI - commit.add("Advanced.ReloadColorSettings", boost::bind(&LLUIColorTable::loadFromSettings, LLUIColorTable::getInstance())); + registrar.add("Advanced.ReloadColorSettings", boost::bind(&LLUIColorTable::loadFromSettings, LLUIColorTable::getInstance())); view_listener_t::addMenu(new LLAdvancedToggleXUINames(), "Advanced.ToggleXUINames"); view_listener_t::addMenu(new LLAdvancedCheckXUINames(), "Advanced.CheckXUINames"); view_listener_t::addMenu(new LLAdvancedSendTestIms(), "Advanced.SendTestIMs"); - commit.add("Advanced.FlushNameCaches", boost::bind(&handle_flush_name_caches)); + registrar.add("Advanced.FlushNameCaches", boost::bind(&handle_flush_name_caches), cb_info::UNTRUSTED_BLOCK); // Advanced > Character > Grab Baked Texture view_listener_t::addMenu(new LLAdvancedGrabBakedTexture(), "Advanced.GrabBakedTexture"); @@ -9987,8 +9999,8 @@ void initialize_menus() view_listener_t::addMenu(new LLAdvancedEnableAppearanceToXML(), "Advanced.EnableAppearanceToXML"); view_listener_t::addMenu(new LLAdvancedToggleCharacterGeometry(), "Advanced.ToggleCharacterGeometry"); - view_listener_t::addMenu(new LLAdvancedTestMale(), "Advanced.TestMale"); - view_listener_t::addMenu(new LLAdvancedTestFemale(), "Advanced.TestFemale"); + view_listener_t::addMenu(new LLAdvancedTestMale(), "Advanced.TestMale", cb_info::UNTRUSTED_THROTTLE); + view_listener_t::addMenu(new LLAdvancedTestFemale(), "Advanced.TestFemale", cb_info::UNTRUSTED_THROTTLE); // Advanced > Character > Animation Speed view_listener_t::addMenu(new LLAdvancedAnimTenFaster(), "Advanced.AnimTenFaster"); @@ -10020,7 +10032,7 @@ void initialize_menus() view_listener_t::addMenu(new LLAdvancedDropPacket(), "Advanced.DropPacket"); // Advanced > Cache - view_listener_t::addMenu(new LLAdvancedPurgeDiskCache(), "Advanced.PurgeDiskCache"); + view_listener_t::addMenu(new LLAdvancedPurgeDiskCache(), "Advanced.PurgeDiskCache", cb_info::UNTRUSTED_BLOCK); // Advanced > Recorder view_listener_t::addMenu(new LLAdvancedAgentPilot(), "Advanced.AgentPilot"); @@ -10029,25 +10041,25 @@ void initialize_menus() view_listener_t::addMenu(new LLAdvancedViewerEventRecorder(), "Advanced.EventRecorder"); // Advanced > Debugging - view_listener_t::addMenu(new LLAdvancedForceErrorBreakpoint(), "Advanced.ForceErrorBreakpoint"); - view_listener_t::addMenu(new LLAdvancedForceErrorLlerror(), "Advanced.ForceErrorLlerror"); - view_listener_t::addMenu(new LLAdvancedForceErrorLlerrorMsg(), "Advanced.ForceErrorLlerrorMsg"); - view_listener_t::addMenu(new LLAdvancedForceErrorBadMemoryAccess(), "Advanced.ForceErrorBadMemoryAccess"); - view_listener_t::addMenu(new LLAdvancedForceErrorBadMemoryAccessCoro(), "Advanced.ForceErrorBadMemoryAccessCoro"); - view_listener_t::addMenu(new LLAdvancedForceErrorInfiniteLoop(), "Advanced.ForceErrorInfiniteLoop"); - view_listener_t::addMenu(new LLAdvancedForceErrorSoftwareException(), "Advanced.ForceErrorSoftwareException"); - view_listener_t::addMenu(new LLAdvancedForceOSException(), "Advanced.ForceErrorOSException"); - view_listener_t::addMenu(new LLAdvancedForceErrorSoftwareExceptionCoro(), "Advanced.ForceErrorSoftwareExceptionCoro"); - view_listener_t::addMenu(new LLAdvancedForceErrorDriverCrash(), "Advanced.ForceErrorDriverCrash"); - view_listener_t::addMenu(new LLAdvancedForceErrorCoroutineCrash(), "Advanced.ForceErrorCoroutineCrash"); - view_listener_t::addMenu(new LLAdvancedForceErrorThreadCrash(), "Advanced.ForceErrorThreadCrash"); - view_listener_t::addMenu(new LLAdvancedForceErrorDisconnectViewer(), "Advanced.ForceErrorDisconnectViewer"); + view_listener_t::addMenu(new LLAdvancedForceErrorBreakpoint(), "Advanced.ForceErrorBreakpoint", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedForceErrorLlerror(), "Advanced.ForceErrorLlerror", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedForceErrorLlerrorMsg(), "Advanced.ForceErrorLlerrorMsg", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedForceErrorBadMemoryAccess(), "Advanced.ForceErrorBadMemoryAccess", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedForceErrorBadMemoryAccessCoro(), "Advanced.ForceErrorBadMemoryAccessCoro", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedForceErrorInfiniteLoop(), "Advanced.ForceErrorInfiniteLoop", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedForceErrorSoftwareException(), "Advanced.ForceErrorSoftwareException", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedForceOSException(), "Advanced.ForceErrorOSException", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedForceErrorSoftwareExceptionCoro(), "Advanced.ForceErrorSoftwareExceptionCoro", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedForceErrorDriverCrash(), "Advanced.ForceErrorDriverCrash", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedForceErrorCoroutineCrash(), "Advanced.ForceErrorCoroutineCrash", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedForceErrorThreadCrash(), "Advanced.ForceErrorThreadCrash", cb_info::UNTRUSTED_BLOCK); + view_listener_t::addMenu(new LLAdvancedForceErrorDisconnectViewer(), "Advanced.ForceErrorDisconnectViewer", cb_info::UNTRUSTED_BLOCK); // Advanced (toplevel) view_listener_t::addMenu(new LLAdvancedToggleShowObjectUpdates(), "Advanced.ToggleShowObjectUpdates"); view_listener_t::addMenu(new LLAdvancedCheckShowObjectUpdates(), "Advanced.CheckShowObjectUpdates"); view_listener_t::addMenu(new LLAdvancedCompressImage(), "Advanced.CompressImage"); - view_listener_t::addMenu(new LLAdvancedCompressFileTest(), "Advanced.CompressFileTest"); + view_listener_t::addMenu(new LLAdvancedCompressFileTest(), "Advanced.CompressFileTest", cb_info::UNTRUSTED_BLOCK); view_listener_t::addMenu(new LLAdvancedShowDebugSettings(), "Advanced.ShowDebugSettings"); view_listener_t::addMenu(new LLAdvancedEnableViewAdminOptions(), "Advanced.EnableViewAdminOptions"); view_listener_t::addMenu(new LLAdvancedToggleViewAdminOptions(), "Advanced.ToggleViewAdminOptions"); @@ -10062,11 +10074,11 @@ void initialize_menus() view_listener_t::addMenu(new LLDevelopSetLoggingLevel(), "Develop.SetLoggingLevel"); //Develop (clear cache immediately) - commit.add("Develop.ClearCache", boost::bind(&handle_cache_clear_immediately) ); + registrar.add("Develop.ClearCache", boost::bind(&handle_cache_clear_immediately), cb_info::UNTRUSTED_BLOCK); // Develop (Fonts debugging) - commit.add("Develop.Fonts.Dump", boost::bind(&LLFontGL::dumpFonts)); - commit.add("Develop.Fonts.DumpTextures", boost::bind(&LLFontGL::dumpFontTextures)); + registrar.add("Develop.Fonts.Dump", boost::bind(&LLFontGL::dumpFonts), cb_info::UNTRUSTED_THROTTLE); + registrar.add("Develop.Fonts.DumpTextures", boost::bind(&LLFontGL::dumpFontTextures), cb_info::UNTRUSTED_THROTTLE); // Admin >Object view_listener_t::addMenu(new LLAdminForceTakeCopy(), "Admin.ForceTakeCopy"); @@ -10103,20 +10115,20 @@ void initialize_menus() view_listener_t::addMenu(new LLObjectMute(), "Avatar.Mute"); view_listener_t::addMenu(new LLAvatarAddFriend(), "Avatar.AddFriend"); view_listener_t::addMenu(new LLAvatarAddContact(), "Avatar.AddContact"); - commit.add("Avatar.Freeze", boost::bind(&handle_avatar_freeze, LLSD())); + registrar.add("Avatar.Freeze", boost::bind(&handle_avatar_freeze, LLSD())); view_listener_t::addMenu(new LLAvatarDebug(), "Avatar.Debug"); view_listener_t::addMenu(new LLAvatarVisibleDebug(), "Avatar.VisibleDebug"); view_listener_t::addMenu(new LLAvatarInviteToGroup(), "Avatar.InviteToGroup"); - commit.add("Avatar.Eject", boost::bind(&handle_avatar_eject, LLSD())); - commit.add("Avatar.ShowInspector", boost::bind(&handle_avatar_show_inspector)); + registrar.add("Avatar.Eject", boost::bind(&handle_avatar_eject, LLSD())); + registrar.add("Avatar.ShowInspector", boost::bind(&handle_avatar_show_inspector)); view_listener_t::addMenu(new LLAvatarSendIM(), "Avatar.SendIM"); - view_listener_t::addMenu(new LLAvatarCall(), "Avatar.Call"); + view_listener_t::addMenu(new LLAvatarCall(), "Avatar.Call", cb_info::UNTRUSTED_BLOCK); enable.add("Avatar.EnableCall", boost::bind(&LLAvatarActions::canCall)); - view_listener_t::addMenu(new LLAvatarReportAbuse(), "Avatar.ReportAbuse"); + view_listener_t::addMenu(new LLAvatarReportAbuse(), "Avatar.ReportAbuse", cb_info::UNTRUSTED_THROTTLE); view_listener_t::addMenu(new LLAvatarToggleMyProfile(), "Avatar.ToggleMyProfile"); view_listener_t::addMenu(new LLAvatarTogglePicks(), "Avatar.TogglePicks"); view_listener_t::addMenu(new LLAvatarToggleSearch(), "Avatar.ToggleSearch"); - view_listener_t::addMenu(new LLAvatarResetSkeleton(), "Avatar.ResetSkeleton"); + view_listener_t::addMenu(new LLAvatarResetSkeleton(), "Avatar.ResetSkeleton", cb_info::UNTRUSTED_THROTTLE); view_listener_t::addMenu(new LLAvatarEnableResetSkeleton(), "Avatar.EnableResetSkeleton"); view_listener_t::addMenu(new LLAvatarResetSkeletonAndAnimations(), "Avatar.ResetSkeletonAndAnimations"); view_listener_t::addMenu(new LLAvatarResetSelfSkeleton(), "Avatar.ResetSelfSkeleton"); @@ -10124,22 +10136,22 @@ void initialize_menus() enable.add("Avatar.IsMyProfileOpen", boost::bind(&my_profile_visible)); enable.add("Avatar.IsPicksTabOpen", boost::bind(&picks_tab_visible)); - commit.add("Avatar.OpenMarketplace", boost::bind(&LLWeb::loadURLExternal, gSavedSettings.getString("MarketplaceURL"))); + registrar.add("Avatar.OpenMarketplace", boost::bind(&LLWeb::loadURLExternal, gSavedSettings.getString("MarketplaceURL"))); view_listener_t::addMenu(new LLAvatarEnableAddFriend(), "Avatar.EnableAddFriend"); enable.add("Avatar.EnableFreezeEject", boost::bind(&enable_freeze_eject, _2)); // Object pie menu view_listener_t::addMenu(new LLObjectBuild(), "Object.Build"); - commit.add("Object.Touch", boost::bind(&handle_object_touch)); - commit.add("Object.ShowOriginal", boost::bind(&handle_object_show_original)); - commit.add("Object.SitOrStand", boost::bind(&handle_object_sit_or_stand)); - commit.add("Object.Delete", boost::bind(&handle_object_delete)); + registrar.add("Object.Touch", boost::bind(&handle_object_touch)); + registrar.add("Object.ShowOriginal", boost::bind(&handle_object_show_original)); + registrar.add("Object.SitOrStand", boost::bind(&handle_object_sit_or_stand)); + registrar.add("Object.Delete", boost::bind(&handle_object_delete)); view_listener_t::addMenu(new LLObjectAttachToAvatar(true), "Object.AttachToAvatar"); view_listener_t::addMenu(new LLObjectAttachToAvatar(false), "Object.AttachAddToAvatar"); view_listener_t::addMenu(new LLObjectReturn(), "Object.Return"); - commit.add("Object.Duplicate", boost::bind(&LLSelectMgr::duplicate, LLSelectMgr::getInstance())); - view_listener_t::addMenu(new LLObjectReportAbuse(), "Object.ReportAbuse"); + registrar.add("Object.Duplicate", boost::bind(&LLSelectMgr::duplicate, LLSelectMgr::getInstance())); + view_listener_t::addMenu(new LLObjectReportAbuse(), "Object.ReportAbuse", cb_info::UNTRUSTED_THROTTLE); view_listener_t::addMenu(new LLObjectMute(), "Object.Mute"); enable.add("Object.VisibleTake", boost::bind(&visible_take_object)); @@ -10148,15 +10160,15 @@ void initialize_menus() enable.add("Object.EnableTakeMultiple", boost::bind(&enable_take_objects)); enable.add("Object.VisibleBuy", boost::bind(&visible_buy_object)); - commit.add("Object.Buy", boost::bind(&handle_buy)); - commit.add("Object.Edit", boost::bind(&handle_object_edit)); - commit.add("Object.EditGLTFMaterial", boost::bind(&handle_object_edit_gltf_material)); - commit.add("Object.Inspect", boost::bind(&handle_object_inspect)); - commit.add("Object.Open", boost::bind(&handle_object_open)); - commit.add("Object.Take", boost::bind(&handle_take, false)); - commit.add("Object.TakeSeparate", boost::bind(&handle_take, true)); - commit.add("Object.TakeSeparateCopy", boost::bind(&handle_take_separate_copy)); - commit.add("Object.ShowInspector", boost::bind(&handle_object_show_inspector)); + registrar.add("Object.Buy", boost::bind(&handle_buy), cb_info::UNTRUSTED_THROTTLE); + registrar.add("Object.Edit", boost::bind(&handle_object_edit)); + registrar.add("Object.EditGLTFMaterial", boost::bind(&handle_object_edit_gltf_material)); + registrar.add("Object.Inspect", boost::bind(&handle_object_inspect)); + registrar.add("Object.Open", boost::bind(&handle_object_open)); + registrar.add("Object.Take", boost::bind(&handle_take, false)); + registrar.add("Object.TakeSeparate", boost::bind(&handle_take, true)); + registrar.add("Object.TakeSeparateCopy", boost::bind(&handle_take_separate_copy)); + registrar.add("Object.ShowInspector", boost::bind(&handle_object_show_inspector)); enable.add("Object.EnableInspect", boost::bind(&enable_object_inspect)); enable.add("Object.EnableEditGLTFMaterial", boost::bind(&enable_object_edit_gltf_material)); enable.add("Object.EnableOpen", boost::bind(&enable_object_open)); @@ -10175,7 +10187,7 @@ void initialize_menus() enable.add("Object.EnableMute", boost::bind(&enable_object_mute)); enable.add("Object.EnableUnmute", boost::bind(&enable_object_unmute)); enable.add("Object.EnableBuy", boost::bind(&enable_buy_object)); - commit.add("Object.ZoomIn", boost::bind(&handle_look_at_selection, "zoom")); + registrar.add("Object.ZoomIn", boost::bind(&handle_look_at_selection, "zoom")); // Attachment pie menu enable.add("Attachment.Label", boost::bind(&onEnableAttachmentLabel, _1, _2)); @@ -10197,14 +10209,14 @@ void initialize_menus() view_listener_t::addMenu(new LLMuteParticle(), "Particle.Mute"); view_listener_t::addMenu(new LLLandEnableBuyPass(), "Land.EnableBuyPass"); - commit.add("Land.Buy", boost::bind(&handle_buy_land)); + registrar.add("Land.Buy", boost::bind(&handle_buy_land), cb_info::UNTRUSTED_THROTTLE); // Generic actions - commit.add("ReportAbuse", boost::bind(&handle_report_abuse)); - commit.add("BuyCurrency", boost::bind(&handle_buy_currency)); + registrar.add("ReportAbuse", boost::bind(&handle_report_abuse), cb_info::UNTRUSTED_THROTTLE); + registrar.add("BuyCurrency", boost::bind(&handle_buy_currency), cb_info::UNTRUSTED_THROTTLE); view_listener_t::addMenu(new LLShowHelp(), "ShowHelp"); view_listener_t::addMenu(new LLToggleHelp(), "ToggleHelp"); - view_listener_t::addMenu(new LLToggleSpeak(), "ToggleSpeak"); + view_listener_t::addMenu(new LLToggleSpeak(), "ToggleSpeak", cb_info::UNTRUSTED_BLOCK); view_listener_t::addMenu(new LLPromptShowURL(), "PromptShowURL"); view_listener_t::addMenu(new LLShowAgentProfile(), "ShowAgentProfile"); view_listener_t::addMenu(new LLShowAgentProfilePicks(), "ShowAgentProfilePicks"); @@ -10213,19 +10225,19 @@ void initialize_menus() view_listener_t::addMenu(new LLToggleShaderControl(), "ToggleShaderControl"); view_listener_t::addMenu(new LLCheckControl(), "CheckControl"); view_listener_t::addMenu(new LLGoToObject(), "GoToObject"); - commit.add("PayObject", boost::bind(&handle_give_money_dialog)); + registrar.add("PayObject", boost::bind(&handle_give_money_dialog)); - commit.add("Inventory.NewWindow", boost::bind(&LLPanelMainInventory::newWindow)); + registrar.add("Inventory.NewWindow", boost::bind(&LLPanelMainInventory::newWindow), cb_info::UNTRUSTED_THROTTLE); enable.add("EnablePayObject", boost::bind(&enable_pay_object)); enable.add("EnablePayAvatar", boost::bind(&enable_pay_avatar)); enable.add("EnableEdit", boost::bind(&enable_object_edit)); enable.add("EnableMuteParticle", boost::bind(&enable_mute_particle)); enable.add("VisibleBuild", boost::bind(&enable_object_build)); - commit.add("Pathfinding.Linksets.Select", boost::bind(&LLFloaterPathfindingLinksets::openLinksetsWithSelectedObjects)); + registrar.add("Pathfinding.Linksets.Select", boost::bind(&LLFloaterPathfindingLinksets::openLinksetsWithSelectedObjects)); enable.add("EnableSelectInPathfindingLinksets", boost::bind(&enable_object_select_in_pathfinding_linksets)); enable.add("VisibleSelectInPathfindingLinksets", boost::bind(&visible_object_select_in_pathfinding_linksets)); - commit.add("Pathfinding.Characters.Select", boost::bind(&LLFloaterPathfindingCharacters::openCharactersWithSelectedObjects)); + registrar.add("Pathfinding.Characters.Select", boost::bind(&LLFloaterPathfindingCharacters::openCharactersWithSelectedObjects)); enable.add("EnableSelectInPathfindingCharacters", boost::bind(&enable_object_select_in_pathfinding_characters)); enable.add("Advanced.EnableErrorOSException", boost::bind(&enable_os_exception)); enable.add("EnableGLTF", boost::bind(&enable_gltf)); diff --git a/indra/newview/llviewermenu.h b/indra/newview/llviewermenu.h index 8c5e0705d0..b5e387207a 100644 --- a/indra/newview/llviewermenu.h +++ b/indra/newview/llviewermenu.h @@ -143,6 +143,7 @@ void handle_give_money_dialog(); bool enable_pay_object(); bool enable_buy_object(); bool handle_go_to(); +bool handle_env_setting_event(std::string event_name); // Export to XML or Collada void handle_export_selected( void * ); diff --git a/indra/newview/llviewermenufile.cpp b/indra/newview/llviewermenufile.cpp index 83f8e96f9a..935f112955 100644 --- a/indra/newview/llviewermenufile.cpp +++ b/indra/newview/llviewermenufile.cpp @@ -906,16 +906,22 @@ class LLFileEnableCloseAllWindows : public view_listener_t } }; +void close_all_windows() +{ + bool app_quitting = false; + gFloaterView->closeAllChildren(app_quitting); + LLFloaterSnapshot *floater_snapshot = LLFloaterSnapshot::findInstance(); + if (floater_snapshot) + floater_snapshot->closeFloater(app_quitting); + if (gMenuHolder) + gMenuHolder->hideMenus(); +} + class LLFileCloseAllWindows : public view_listener_t { bool handleEvent(const LLSD& userdata) { - bool app_quitting = false; - gFloaterView->closeAllChildren(app_quitting); - LLFloaterSnapshot* floater_snapshot = LLFloaterSnapshot::findInstance(); - if (floater_snapshot) - floater_snapshot->closeFloater(app_quitting); - if (gMenuHolder) gMenuHolder->hideMenus(); + close_all_windows(); return true; } }; diff --git a/indra/newview/llviewermenufile.h b/indra/newview/llviewermenufile.h index d99f9dc4c6..6b628f07de 100644 --- a/indra/newview/llviewermenufile.h +++ b/indra/newview/llviewermenufile.h @@ -74,6 +74,8 @@ bool get_bulk_upload_expected_cost( void do_bulk_upload(std::vector<std::string> filenames, bool allow_2k); +void close_all_windows(); + //consider moving all file pickers below to more suitable place class LLFilePickerThread : public LLThread { //multi-threaded file picker (runs system specific file picker in background and calls "notify" from main thread) diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 872a9a1581..a7593b953a 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -2422,8 +2422,11 @@ void process_chat_from_simulator(LLMessageSystem *msg, void **user_data) bool ircstyle = false; + auto [message, is_script] = LLStringUtil::withoutPrefix(mesg, LUA_PREFIX); + chat.mIsScript = is_script; + // Look for IRC-style emotes here so chatbubbles work - std::string prefix = mesg.substr(0, 4); + std::string prefix = message.substr(0, 4); if (prefix == "/me " || prefix == "/me'") { ircstyle = true; @@ -2465,19 +2468,25 @@ void process_chat_from_simulator(LLMessageSystem *msg, void **user_data) else { chat.mText = ""; + auto [msg_without_prefix, is_lua] = LLStringUtil::withoutPrefix(mesg, LUA_PREFIX); + std::string prefix; + if (is_lua) + { + prefix = LUA_PREFIX; + } switch(chat.mChatType) { case CHAT_TYPE_WHISPER: - chat.mText = LLTrans::getString("whisper") + " "; + prefix += LLTrans::getString("whisper") + " "; + break; + case CHAT_TYPE_SHOUT: + prefix += LLTrans::getString("shout") + " "; break; case CHAT_TYPE_DEBUG_MSG: case CHAT_TYPE_OWNER: case CHAT_TYPE_NORMAL: case CHAT_TYPE_DIRECT: break; - case CHAT_TYPE_SHOUT: - chat.mText = LLTrans::getString("shout") + " "; - break; case CHAT_TYPE_START: case CHAT_TYPE_STOP: LL_WARNS("Messaging") << "Got chat type start/stop in main chat processing." << LL_ENDL; @@ -2487,7 +2496,7 @@ void process_chat_from_simulator(LLMessageSystem *msg, void **user_data) break; } - chat.mText += mesg; + chat.mText = prefix + msg_without_prefix; } // We have a real utterance now, so can stop showing "..." and proceed. @@ -2568,6 +2577,11 @@ void process_chat_from_simulator(LLMessageSystem *msg, void **user_data) msg_notify["from_id"] = chat.mFromID; msg_notify["source_type"] = chat.mSourceType; on_new_message(msg_notify); + + + msg_notify["chat_type"] = chat.mChatType; + msg_notify["message"] = mesg; + LLEventPumps::instance().obtain("LLNearbyChat").post(msg_notify); } } @@ -2713,7 +2727,7 @@ public: virtual ~LLPostTeleportNotifiers(); //function to be called at the supplied frequency - virtual bool tick(); + bool tick() override; }; LLPostTeleportNotifiers::LLPostTeleportNotifiers() : LLEventTimer( 2.0 ) diff --git a/indra/newview/llviewerparcelmediaautoplay.h b/indra/newview/llviewerparcelmediaautoplay.h index 96b569f126..9759cf6a17 100644 --- a/indra/newview/llviewerparcelmediaautoplay.h +++ b/indra/newview/llviewerparcelmediaautoplay.h @@ -35,7 +35,7 @@ class LLViewerParcelMediaAutoPlay : LLEventTimer, public LLSingleton<LLViewerPar { LLSINGLETON(LLViewerParcelMediaAutoPlay); public: - virtual bool tick() override; + bool tick() override; static void playStarted(); private: diff --git a/indra/newview/llviewerstats.cpp b/indra/newview/llviewerstats.cpp index 73aabf49d1..af6705bd39 100644 --- a/indra/newview/llviewerstats.cpp +++ b/indra/newview/llviewerstats.cpp @@ -66,6 +66,7 @@ #include "llinventorymodel.h" #include "lluiusage.h" #include "lltranslate.h" +#include "llluamanager.h" // "Minimal Vulkan" to get max API Version @@ -620,6 +621,10 @@ void send_viewer_stats(bool include_preferences) system["shader_level"] = shader_level; + LLSD &scripts = body["scripts"]; + scripts["lua_scripts"] = LLLUAmanager::sScriptCount; + scripts["lua_auto_scripts"] = LLLUAmanager::sAutorunScriptCount; + LLSD &download = body["downloads"]; download["world_kbytes"] = F64Kilobytes(gTotalWorldData).value(); diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index 8ea8fbf905..19cf613939 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -4841,7 +4841,7 @@ bool LLViewerWindow::saveSnapshot(const std::string& filepath, S32 image_width, LL_INFOS() << "Saving snapshot to: " << filepath << LL_ENDL; LLPointer<LLImageRaw> raw = new LLImageRaw; - bool success = rawSnapshot(raw, image_width, image_height, true, false, show_ui, show_hud, do_rebuild); + bool success = rawSnapshot(raw, image_width, image_height, true, false, show_ui, show_hud, do_rebuild, 0, type); if (success) { diff --git a/indra/newview/llviewerwindowlistener.cpp b/indra/newview/llviewerwindowlistener.cpp index da7e18af5c..52f413792a 100644 --- a/indra/newview/llviewerwindowlistener.cpp +++ b/indra/newview/llviewerwindowlistener.cpp @@ -43,22 +43,13 @@ LLViewerWindowListener::LLViewerWindowListener(LLViewerWindow* llviewerwindow): mViewerWindow(llviewerwindow) { // add() every method we want to be able to invoke via this event API. - LLSD saveSnapshotArgs; - saveSnapshotArgs["filename"] = LLSD::String(); - saveSnapshotArgs["reply"] = LLSD::String(); - // The following are optional, so don't build them into required prototype. -// saveSnapshotArgs["width"] = LLSD::Integer(); -// saveSnapshotArgs["height"] = LLSD::Integer(); -// saveSnapshotArgs["showui"] = LLSD::Boolean(); -// saveSnapshotArgs["showhud"] = LLSD::Boolean(); -// saveSnapshotArgs["rebuild"] = LLSD::Boolean(); -// saveSnapshotArgs["type"] = LLSD::String(); add("saveSnapshot", - "Save screenshot: [\"filename\"], [\"width\"], [\"height\"], [\"showui\"], [\"showhud\"], [\"rebuild\"], [\"type\"]\n" + "Save screenshot: [\"filename\"] (extension may be specified: bmp, jpeg, png)\n" + "[\"width\"], [\"height\"], [\"showui\"], [\"showhud\"], [\"rebuild\"], [\"type\"]\n" "type: \"COLOR\", \"DEPTH\"\n" - "Post on [\"reply\"] an event containing [\"ok\"]", + "Post on [\"reply\"] an event containing [\"result\"]", &LLViewerWindowListener::saveSnapshot, - saveSnapshotArgs); + llsd::map("filename", LLSD::String(), "reply", LLSD())); add("requestReshape", "Resize the window: [\"w\"], [\"h\"]", &LLViewerWindowListener::requestReshape); @@ -66,12 +57,15 @@ LLViewerWindowListener::LLViewerWindowListener(LLViewerWindow* llviewerwindow): void LLViewerWindowListener::saveSnapshot(const LLSD& event) const { + Response response(LLSD(), event); + typedef std::map<LLSD::String, LLSnapshotModel::ESnapshotLayerType> TypeMap; TypeMap types; #define tp(name) types[#name] = LLSnapshotModel::SNAPSHOT_TYPE_##name tp(COLOR); tp(DEPTH); -#undef tp +#undef tp + // Our add() call should ensure that the incoming LLSD does in fact // contain our required arguments. Deal with the optional ones. S32 width (mViewerWindow->getWindowWidthRaw()); @@ -94,14 +88,36 @@ void LLViewerWindowListener::saveSnapshot(const LLSD& event) const TypeMap::const_iterator found = types.find(event["type"]); if (found == types.end()) { - LL_ERRS("LLViewerWindowListener") << "LLViewerWindowListener::saveSnapshot(): " - << "unrecognized type " << event["type"] << LL_ENDL; - return; + return response.error(stringize("Unrecognized type ", std::quoted(event["type"].asString()), " [\"COLOR\"] or [\"DEPTH\"] is expected.")); } type = found->second; } - bool ok = mViewerWindow->saveSnapshot(event["filename"], width, height, showui, showhud, rebuild, type); - sendReply(LLSDMap("ok", ok), event); + + std::string filename(event["filename"]); + if (filename.empty()) + { + return response.error(stringize("File path is empty.")); + } + + LLSnapshotModel::ESnapshotFormat format(LLSnapshotModel::SNAPSHOT_FORMAT_BMP); + std::string ext = gDirUtilp->getExtension(filename); + if (ext.empty()) + { + filename.append(".bmp"); + } + else if (ext == "png") + { + format = LLSnapshotModel::SNAPSHOT_FORMAT_PNG; + } + else if (ext == "jpeg" || ext == "jpg") + { + format = LLSnapshotModel::SNAPSHOT_FORMAT_JPEG; + } + else if (ext != "bmp") + { + return response.error(stringize("Unrecognized format. [\"png\"], [\"jpeg\"] or [\"bmp\"] is expected.")); + } + response["result"] = mViewerWindow->saveSnapshot(filename, width, height, showui, showhud, rebuild, type, format); } void LLViewerWindowListener::requestReshape(LLSD const & event_data) const diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index b04bb99b97..3a94f87a98 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -113,6 +113,7 @@ #include "llskinningutil.h" #include "llperfstats.h" +#include "lleventapi.h" #include <boost/lexical_cast.hpp> @@ -11744,3 +11745,33 @@ bool LLVOAvatar::isBuddy() const return is_friend; } +class LLVOAvatarListener : public LLEventAPI +{ + public: + LLVOAvatarListener() : LLEventAPI("LLVOAvatar", "LLVOAvatar listener to retrieve avatar info") + { + add("getSpeed", + "Return the avatar movement speed in the XY plane", + &LLVOAvatarListener::getSpeed, + LLSD().with("reply", LLSD())); + add("isInAir", + "Return the info whether avatar is in the air, and if so the time in the air", + &LLVOAvatarListener::isInAir, + LLSD().with("reply", LLSD())); + } + + private: + void getSpeed(const LLSD &request) const + { + LLVector3 avatar_velocity = gAgentAvatarp->getCharacterVelocity() * gAgentAvatarp->getTimeDilation(); + avatar_velocity.mV[VZ] = 0.f; + Response response(llsd::map("value", avatar_velocity.magVec()), request); + } + void isInAir(const LLSD &request) const + { + Response response(llsd::map("value", gAgentAvatarp->mInAir, + "duration", gAgentAvatarp->mInAir ? gAgentAvatarp->mTimeInAir.getElapsedTimeF32() : 0), request); + } +}; + +static LLVOAvatarListener VOAvatarListener; diff --git a/indra/newview/llwearableitemslist.cpp b/indra/newview/llwearableitemslist.cpp index 8ce1a745c3..217d46d1d2 100644 --- a/indra/newview/llwearableitemslist.cpp +++ b/indra/newview/llwearableitemslist.cpp @@ -33,7 +33,6 @@ #include "llagentwearables.h" #include "llappearancemgr.h" -#include "llinventoryfunctions.h" #include "llinventoryicon.h" #include "llgesturemgr.h" #include "lltransutil.h" @@ -41,14 +40,6 @@ #include "llviewermenu.h" #include "llvoavatarself.h" -class LLFindOutfitItems : public LLInventoryCollectFunctor -{ -public: - LLFindOutfitItems() {} - virtual ~LLFindOutfitItems() {} - virtual bool operator()(LLInventoryCategory* cat, - LLInventoryItem* item); -}; bool LLFindOutfitItems::operator()(LLInventoryCategory* cat, LLInventoryItem* item) @@ -894,7 +885,7 @@ void LLWearableItemsList::ContextMenu::show(LLView* spawning_view, LLWearableTyp } LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; - registrar.add("Wearable.CreateNew", boost::bind(createNewWearableByType, w_type)); + registrar.add("Wearable.CreateNew", { boost::bind(createNewWearableByType, w_type), cb_info::UNTRUSTED_THROTTLE }); menup = createFromFile("menu_wearable_list_item.xml"); if (!menup) { @@ -920,7 +911,7 @@ void LLWearableItemsList::ContextMenu::show(LLView* spawning_view, LLWearableTyp // virtual LLContextMenu* LLWearableItemsList::ContextMenu::createMenu() { - LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; + LLUICtrl::ScopedRegistrarHelper registrar; const uuid_vec_t& ids = mUUIDs; // selected items IDs LLUUID selected_id = ids.front(); // ID of the first selected item diff --git a/indra/newview/llwearableitemslist.h b/indra/newview/llwearableitemslist.h index 3fe1059176..1a22d6b366 100644 --- a/indra/newview/llwearableitemslist.h +++ b/indra/newview/llwearableitemslist.h @@ -32,6 +32,7 @@ #include "llsingleton.h" // newview +#include "llinventoryfunctions.h" #include "llinventoryitemslist.h" #include "llinventorylistitem.h" #include "lllistcontextmenu.h" @@ -507,4 +508,11 @@ protected: LLWearableType::EType mMenuWearableType; }; +struct LLFindOutfitItems : public LLInventoryCollectFunctor +{ + LLFindOutfitItems() {} + virtual ~LLFindOutfitItems() {} + virtual bool operator()(LLInventoryCategory *cat, LLInventoryItem *item); +}; + #endif //LL_LLWEARABLEITEMSLIST_H diff --git a/indra/newview/llwindowlistener.cpp b/indra/newview/llwindowlistener.cpp index ebcdd537a5..601abdf5cf 100644 --- a/indra/newview/llwindowlistener.cpp +++ b/indra/newview/llwindowlistener.cpp @@ -54,7 +54,7 @@ LLWindowListener::LLWindowListener(LLViewerWindow *window, const KeyboardGetter& "Given [\"keysym\"], [\"keycode\"] or [\"char\"], inject the specified "; std::string keyExplain = "(integer keycode values, or keysym string from any addKeyName() call in\n" - "http://bitbucket.org/lindenlab/viewer-release/src/tip/indra/llwindow/llkeyboard.cpp )\n"; + "https://github.com/secondlife/viewer/blob/main/indra/llwindow/llkeyboard.cpp#L68-L124)\n"; std::string mask = "Specify optional [\"mask\"] as an array containing any of \"CTL\", \"ALT\",\n" "\"SHIFT\" or \"MAC_CONTROL\"; the corresponding modifier bits will be combined\n" diff --git a/indra/newview/scripts/lua/auto/menus.lua b/indra/newview/scripts/lua/auto/menus.lua new file mode 100644 index 0000000000..b2f54d83df --- /dev/null +++ b/indra/newview/scripts/lua/auto/menus.lua @@ -0,0 +1,51 @@ +-- Inject Lua-related menus into the top menu structure. Run this as a Lua +-- script so that turning off the Lua feature also disables these menus. + +-- Under Develop -> Consoles, want to present the equivalent of: +-- <menu_item_separator/> +-- <menu_item_check +-- label="LUA Debug Console" +-- name="LUA Debug Console"> +-- <menu_item_check.on_check +-- function="Floater.Visible" +-- parameter="lua_debug" /> +-- <menu_item_check.on_click +-- function="Floater.Toggle" +-- parameter="lua_debug" /> +-- </menu_item_check> +-- <menu_item_check +-- label="LUA Scripts Info" +-- name="LUA Scripts"> +-- <menu_item_check.on_check +-- function="Floater.Visible" +-- parameter="lua_scripts" /> +-- <menu_item_check.on_click +-- function="Floater.Toggle" +-- parameter="lua_scripts" /> +-- </menu_item_check> + +local startup = require 'startup' +local UI = require 'UI' + +-- Don't mess with the viewer's menu structure until we've logged in. +startup.wait('STATE_STARTED') + +-- Add LUA Debug Console to Develop->Consoles +local pos = 9 +UI.addMenuSeparator{ + parent_menu='Consoles', pos=pos, +} +pos += 1 +UI.addMenuItem{ + parent_menu='Consoles', pos=pos, + name='lua_debug', label='LUA Debug Console', + func='Floater.ToggleOrBringToFront', param='lua_debug', +} +pos += 1 + +-- Add LUA Scripts Info to Develop->Consoles +UI.addMenuItem{ + parent_menu='Consoles', pos=pos, + name='lua_scripts', label='LUA Scripts Info', + func='Floater.ToggleOrBringToFront', param='lua_scripts', +} diff --git a/indra/newview/scripts/lua/luafloater_camera_control.xml b/indra/newview/scripts/lua/luafloater_camera_control.xml new file mode 100644 index 0000000000..0601a363e5 --- /dev/null +++ b/indra/newview/scripts/lua/luafloater_camera_control.xml @@ -0,0 +1,244 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<floater + legacy_header_height="18" + height="350" + layout="topleft" + name="camera_demo" + title="Follow camera control" + width="360"> + <text + type="string" + follows="left|top" + height="10" + width="100" + layout="topleft" + top="30" + left="10" + name="camera_lbl"> + Camera position: + </text> + <text + type="string" + follows="left|top" + height="10" + width="10" + layout="topleft" + top_pad="12" + left="10" + value="X" + name="cam_x_lbl"/> + <line_editor + border_style="line" + border_thickness="1" + follows="left|bottom" + font="SansSerif" + height="20" + layout="topleft" + max_length_bytes="50" + name="cam_x" + top_delta="-2" + left_pad="5" + width="65" /> + <button + follows="left|bottom" + height="20" + image_overlay="Icon_Copy" + image_selected="Toolbar_Middle_Selected" + image_unselected="Toolbar_Middle_Off" + layout="topleft" + tool_tip="Use Agent position" + name="agent_cam_btn" + left_pad="10" + width="20" > + </button> + <text + type="string" + follows="left|top" + height="10" + width="10" + layout="topleft" + top_pad="7" + left="10" + value="Y" + name="cam_y_lbl"/> + <line_editor + border_style="line" + border_thickness="1" + follows="left|bottom" + font="SansSerif" + height="20" + layout="topleft" + max_length_bytes="50" + name="cam_y" + top_delta="-2" + left_pad="5" + width="65" /> + <text + type="string" + follows="left|top" + height="10" + width="10" + layout="topleft" + top_pad="7" + left="10" + value="Z" + name="cam_z_lbl"/> + <line_editor + border_style="line" + border_thickness="1" + follows="left|bottom" + font="SansSerif" + height="20" + layout="topleft" + max_length_bytes="50" + name="cam_z" + top_delta="-2" + left_pad="5" + width="65" /> + <text + type="string" + follows="left|top" + height="10" + width="100" + layout="topleft" + top="30" + left="145" + name="focus_lbl"> + Focus position: + </text> + <text + type="string" + follows="left|top" + height="10" + width="10" + layout="topleft" + top_pad="12" + left="145" + value="X" + name="cam_x_lbl"/> + <line_editor + border_style="line" + border_thickness="1" + follows="left|bottom" + font="SansSerif" + height="20" + layout="topleft" + max_length_bytes="50" + name="focus_x" + top_delta="-2" + left_pad="5" + width="60" /> + <button + follows="left|bottom" + height="20" + image_overlay="Icon_Copy" + image_selected="Toolbar_Middle_Selected" + image_unselected="Toolbar_Middle_Off" + layout="topleft" + tool_tip="Use Agent position" + name="agent_focus_btn" + left_pad="10" + width="20" > + </button> + <text + type="string" + follows="left|top" + height="10" + width="10" + layout="topleft" + top_pad="7" + left="145" + value="Y" + name="cam_y_lbl"/> + <line_editor + border_style="line" + border_thickness="1" + follows="left|bottom" + font="SansSerif" + height="20" + layout="topleft" + max_length_bytes="50" + name="focus_y" + top_delta="-2" + left_pad="5" + width="60" /> + <text + type="string" + follows="left|top" + height="10" + width="10" + layout="topleft" + top_pad="7" + left="145" + value="Z" + name="cam_z_lbl"/> + <line_editor + border_style="line" + border_thickness="1" + follows="left|bottom" + font="SansSerif" + height="20" + layout="topleft" + max_length_bytes="50" + name="focus_z" + top_delta="-2" + left_pad="5" + width="60" /> + <text + type="string" + follows="left|top" + height="10" + width="100" + layout="topleft" + top="30" + left="270" + name="focus_lbl"> + Lock: + </text> + <check_box + width="80" + height="21" + layout="topleft" + follows="top|left" + top_pad="12" + label="Camera" + name="lock_camera_ctrl"/> + <check_box + width="80" + height="21" + layout="topleft" + follows="top|left" + top_pad="2" + label="Focus" + name="lock_focus_ctrl"/> + <button + follows="left|bottom" + height="25" + label="Update params" + layout="topleft" + name="update_btn" + left="10" + top="140" + width="120" > + </button> + <button + follows="left|bottom" + height="25" + label="Reset" + layout="topleft" + name="reset_btn" + left_pad="15" + width="90" > + </button> + <text_editor + follows="top|left" + font="SansSerif" + height="160" + left="5" + enabled="false" + name="events_editor" + top_pad="15" + word_wrap="true" + max_length="65536" + width="350"/> +</floater> diff --git a/indra/newview/scripts/lua/luafloater_demo.xml b/indra/newview/scripts/lua/luafloater_demo.xml new file mode 100644 index 0000000000..b2273d7718 --- /dev/null +++ b/indra/newview/scripts/lua/luafloater_demo.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<floater + legacy_header_height="18" + height="300" + layout="topleft" + name="lua_demo" + title="LUA" + width="320"> + <check_box + height="16" + label="Disable button" + layout="topleft" + top_pad="35" + left="5" + name="disable_ctrl" + width="146" /> + <line_editor + border_style="line" + border_thickness="1" + follows="left|bottom" + font="SansSerif" + height="20" + layout="topleft" + max_length_bytes="50" + name="openfloater_cmd" + top_delta="25" + width="100" /> + <button + follows="left|bottom" + height="23" + label="Open floater" + layout="topleft" + name="open_btn" + top_delta="25" + width="100" > + </button> + <text + type="string" + follows="left|top" + height="10" + layout="topleft" + top="30" + left_delta="170" + name="title_lbl"> + Select title + </text> + <combo_box + follows="top|left" + height="23" + name="title_cmb" + top_pad="5" + width="100"> + <combo_box.item + label="LUA" + value="LUA" /> + <combo_box.item + label="LUA floater" + value="LUA floater" /> + <combo_box.item + label="LUA-U title" + value="LUA-U title" /> + </combo_box> + <text + type="string" + follows="left|bottom" + height="10" + layout="topleft" + top_delta="50" + name="show_time_lbl"> + Double click me + </text> + <text + type="string" + follows="left|top" + height="10" + layout="topleft" + top_pad="15" + font="SansSerif" + text_color="white" + left_delta="15" + name="time_lbl"/> + <text_editor + follows="top|left" + font="SansSerif" + height="140" + left="5" + enabled="false" + name="events_editor" + top_pad="15" + word_wrap="true" + max_length="65536" + width="310"/> +</floater> diff --git a/indra/newview/scripts/lua/luafloater_gesture_list.xml b/indra/newview/scripts/lua/luafloater_gesture_list.xml new file mode 100644 index 0000000000..a38a04eed0 --- /dev/null +++ b/indra/newview/scripts/lua/luafloater_gesture_list.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<floater + legacy_header_height="18" + height="150" + layout="topleft" + name="lua_gestures" + title="Gestures" + width="320"> + <scroll_list + draw_heading="false" + left="5" + width="310" + height="115" + top_pad ="25" + follows="all" + name="gesture_list"> + <scroll_list.columns + name="gesture_name" + label="Name"/> + </scroll_list> +</floater> diff --git a/indra/newview/scripts/lua/luafloater_outfits_list.xml b/indra/newview/scripts/lua/luafloater_outfits_list.xml new file mode 100644 index 0000000000..8cab864308 --- /dev/null +++ b/indra/newview/scripts/lua/luafloater_outfits_list.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<floater + legacy_header_height="18" + height="205" + layout="topleft" + name="lua_outfits" + title="Outfits" + width="325"> + <scroll_list + draw_heading="false" + left="5" + width="315" + height="150" + top_pad ="25" + follows="all" + name="outfits_list"> + <scroll_list.columns + name="outfit_name" + label="Name"/> + </scroll_list> + <button + follows="left|bottom" + height="23" + label="Replace COF" + layout="topleft" + name="replace_btn" + enabled="false" + top_pad="5" + width="95" > + </button> + <button + follows="left|bottom" + height="23" + label="Add to COF" + left_pad="7" + layout="topleft" + name="add_btn" + enabled="false" + width="95" > + </button> + <button + follows="left|bottom" + height="23" + label="Show wearables" + left_pad="7" + layout="topleft" + name="select_btn" + enabled="false" + width="111" > + </button> +</floater> diff --git a/indra/newview/scripts/lua/luafloater_speedometer.xml b/indra/newview/scripts/lua/luafloater_speedometer.xml new file mode 100644 index 0000000000..54b99c7d48 --- /dev/null +++ b/indra/newview/scripts/lua/luafloater_speedometer.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<floater + legacy_header_height="18" + height="60" + layout="topleft" + name="lua_speedometer" + title="Speedometer" + can_minimize="false" + width="170"> + <text + type="string" + follows="left|top" + font="SansSerifBold" + font.size="Huge" + text_color="White" + layout="topleft" + top="25" + left_delta="30" + width="70" + height="30" + name="speed_lbl"/> + <text + type="string" + follows="left|top" + width="55" + height="30" + font="SansSerifBold" + font.size="Huge" + text_color="White" + layout="topleft" + left_pad="2" + name="mps_lbl"> + m/s + </text> +</floater> diff --git a/indra/newview/scripts/lua/qtest.lua b/indra/newview/scripts/lua/qtest.lua new file mode 100644 index 0000000000..9526f58b04 --- /dev/null +++ b/indra/newview/scripts/lua/qtest.lua @@ -0,0 +1,146 @@ +-- Exercise the Queue, WaitQueue, ErrorQueue family + +Queue = require('Queue') +WaitQueue = require('WaitQueue') +ErrorQueue = require('ErrorQueue') +util = require('util') + +inspect = require('inspect') + +-- resume() wrapper to propagate errors +function resume(co, ...) + -- if there's an idiom other than table.pack() to assign an arbitrary + -- number of return values, I don't yet know it + local ok_result = table.pack(coroutine.resume(co, ...)) + if not ok_result[1] then + -- if [1] is false, then [2] is the error message + error(ok_result[2]) + end + -- ok is true, whew, just return the rest of the values + return table.unpack(ok_result, 2) +end + +-- ------------------ Queue variables are instance-specific ------------------ +q1 = Queue() +q2 = Queue() + +q1:Enqueue(17) + +assert(not q1:IsEmpty()) +assert(q2:IsEmpty()) +assert(q1:Dequeue() == 17) +assert(q1:Dequeue() == nil) +assert(q2:Dequeue() == nil) + +-- ----------------------------- test WaitQueue ------------------------------ +q1 = WaitQueue() +q2 = WaitQueue() +result = {} +values = { 1, 1, 2, 3, 5, 8, 13, 21 } + +for i, value in pairs(values) do + q1:Enqueue(value) +end +-- close() while not empty tests that queue drains before reporting done +q1:close() + +-- ensure that WaitQueue instance variables are in fact independent +assert(q2:IsEmpty()) + +-- consumer() coroutine to pull from the passed q until closed +function consumer(desc, q) + print(string.format('consumer(%s) start', desc)) + local value = q:Dequeue() + while value ~= nil do + print(string.format('consumer(%s) got %q', desc, value)) + table.insert(result, value) + value = q:Dequeue() + end + print(string.format('consumer(%s) done', desc)) +end + +-- run two consumers +coa = coroutine.create(consumer) +cob = coroutine.create(consumer) +-- Since consumer() doesn't yield while it can still retrieve values, +-- consumer(a) will dequeue all values from q1 and return when done. +resume(coa, 'a', q1) +-- consumer(b) will wake up to find the queue empty and closed. +resume(cob, 'b', q1) +coroutine.close(coa) +coroutine.close(cob) + +print('values:', inspect(values)) +print('result:', inspect(result)) + +assert(util.equal(values, result)) + +-- try incrementally enqueueing values +q3 = WaitQueue() +result = {} +values = { 'This', 'is', 'a', 'test', 'script' } + +coros = {} +for _, name in {'a', 'b'} do + local coro = coroutine.create(consumer) + table.insert(coros, coro) + -- Resuming both coroutines should leave them both waiting for a queue item. + resume(coro, name, q3) +end + +for _, s in pairs(values) do + print(string.format('Enqueue(%q)', s)) + q3:Enqueue(s) +end +q3:close() + +function joinall(coros) + local running + local errors = 0 + repeat + running = false + for i, coro in pairs(coros) do + if coroutine.status(coro) == 'suspended' then + running = true + -- directly call coroutine.resume() instead of our resume() + -- wrapper because we explicitly check for errors here + local ok, message = coroutine.resume(coro) + if not ok then + print('*** ' .. message) + errors += 1 + end + if coroutine.status(coro) == 'dead' then + coros[i] = nil + end + end + end + until not running + return errors +end + +joinall(coros) + +print(string.format('%q', table.concat(result, ' '))) +assert(util.equal(values, result)) + +-- ----------------------------- test ErrorQueue ----------------------------- +q4 = ErrorQueue() +result = {} +values = { 'This', 'is', 'a', 'test', 'script' } + +coros = {} +for _, name in {'a', 'b'} do + local coro = coroutine.create(consumer) + table.insert(coros, coro) + -- Resuming both coroutines should leave them both waiting for a queue item. + resume(coro, name, q4) +end + +for i = 1, 4 do + print(string.format('Enqueue(%q)', values[i])) + q4:Enqueue(values[i]) +end +q4:Error('something went wrong') + +assert(joinall(coros) == 2) + diff --git a/indra/newview/scripts/lua/require/ErrorQueue.lua b/indra/newview/scripts/lua/require/ErrorQueue.lua new file mode 100644 index 0000000000..e6e9a5ef48 --- /dev/null +++ b/indra/newview/scripts/lua/require/ErrorQueue.lua @@ -0,0 +1,37 @@ +-- ErrorQueue isa WaitQueue with the added feature that a producer can push an +-- error through the queue. Once that error is dequeued, every consumer will +-- raise that error. + +local WaitQueue = require('WaitQueue') +local function dbg(...) end +-- local dbg = require('printf') +local util = require('util') + +local ErrorQueue = WaitQueue() + +util.classctor(ErrorQueue) + +function ErrorQueue:Error(message) + -- Setting Error() is a marker, like closing the queue. Once we reach the + -- error, every subsequent Dequeue() call will raise the same error. + dbg('Setting self._closed to %q', message) + self._closed = message + self:_wake_waiters() +end + +function ErrorQueue:Dequeue() + local value = WaitQueue.Dequeue(self) + dbg('ErrorQueue:Dequeue: base Dequeue() got %s', value) + if value ~= nil then + -- queue not yet closed, show caller + return value + end + if self._closed == true then + -- WaitQueue:close() sets true: queue has only been closed, tell caller + return nil + end + -- self._closed is a message set by Error() + error(self._closed) +end + +return ErrorQueue diff --git a/indra/newview/scripts/lua/require/LLAgent.lua b/indra/newview/scripts/lua/require/LLAgent.lua new file mode 100644 index 0000000000..07ef1e0b0b --- /dev/null +++ b/indra/newview/scripts/lua/require/LLAgent.lua @@ -0,0 +1,74 @@ +local leap = require 'leap' +local mapargs = require 'mapargs' + +local LLAgent = {} + +function LLAgent.getRegionPosition() + return leap.request('LLAgent', {op = 'getPosition'}).region +end + +function LLAgent.getGlobalPosition() + return leap.request('LLAgent', {op = 'getPosition'}).global +end + +-- Return array information about the agent's groups +-- id: group id\n" +-- name: group name\n" +-- insignia: group insignia texture id +-- notices: bool indicating if this user accepts notices from this group +-- display: bool indicating if this group is listed in the user's profile +-- contrib: user's land contribution to this group +function LLAgent.getGroups() + return leap.request('LLAgent', {op = 'getGroups'}).groups +end + +-- Use LL.leaphelp('LLAgent') and see 'setCameraParams' to get more info about params +-- -- TYPE -- DEFAULT -- RANGE +-- LLAgent.setCamera{ [, camera_pos] -- vector3 +-- [, focus_pos] -- vector3 +-- [, focus_offset] -- vector3 -- {1,0,0} -- {-10,-10,-10} to {10,10,10} +-- [, distance] -- float (meters) -- 3 -- 0.5 to 50 +-- [, focus_threshold] -- float (meters) -- 1 -- 0 to 4 +-- [, camera_threshold] -- float (meters) -- 1 -- 0 to 4 +-- [, focus_lag] -- float (seconds) -- 0.1 -- 0 to 3 +-- [, camera_lag] -- float (seconds) -- 0.1 -- 0 to 3 +-- [, camera_pitch] -- float (degrees) -- 0 -- -45 to 80 +-- [, behindness_angle] -- float (degrees) -- 10 -- 0 to 180 +-- [, behindness_lag] -- float (seconds) -- 0 -- 0 to 3 +-- [, camera_locked] -- bool -- false +-- [, focus_locked]} -- bool -- false +function LLAgent.setCamera(...) + local args = mapargs('camera_pos,focus_pos,focus_offset,focus_lag,camera_lag,' .. + 'distance,focus_threshold,camera_threshold,camera_pitch,' .. + 'camera_locked,focus_locked,behindness_angle,behindness_lag', ...) + args.op = 'setCameraParams' + leap.send('LLAgent', args) +end + +function LLAgent.setFollowCamActive(active) + leap.send('LLAgent', {op = 'setFollowCamActive', active = active}) +end + +function LLAgent.removeCamParams() + leap.send('LLAgent', {op = 'removeCameraParams'}) +end + +-- Play specified animation by "item_id" locally +-- if "inworld" is specified as true, animation will be played inworld instead +function LLAgent.playAnimation(...) + local args = mapargs('item_id,inworld', ...) + args.op = 'playAnimation' + return leap.request('LLAgent', args) +end + +function LLAgent.stopAnimation(item_id) + return leap.request('LLAgent', {op = 'stopAnimation', item_id=item_id}) +end + +-- Get animation info by "item_id" +-- reply contains "duration", "is_loop", "num_joints", "asset_id", "priority" +function LLAgent.getAnimationInfo(item_id) + return leap.request('LLAgent', {op = 'getAnimationInfo', item_id=item_id}).anim_info +end + +return LLAgent diff --git a/indra/newview/scripts/lua/require/LLAppearance.lua b/indra/newview/scripts/lua/require/LLAppearance.lua new file mode 100644 index 0000000000..f533d22daf --- /dev/null +++ b/indra/newview/scripts/lua/require/LLAppearance.lua @@ -0,0 +1,31 @@ +local leap = require 'leap' + +local LLAppearance = {} + +function LLAppearance.wearOutfit(folder, action) + action = action or 'add' + leap.request('LLAppearance', {op='wearOutfit', append = (action == 'add'), folder_id=folder}) +end + +function LLAppearance.wearOutfitByName(folder, action) + action = action or 'add' + leap.request('LLAppearance', {op='wearOutfit', append = (action == 'add'), folder_name=folder}) +end + +function LLAppearance.wearItems(items_id, replace) + leap.send('LLAppearance', {op='wearItems', replace = replace, items_id=items_id}) +end + +function LLAppearance.detachItems(items_id) + leap.send('LLAppearance', {op='detachItems', items_id=items_id}) +end + +function LLAppearance.getOutfitsList() + return leap.request('LLAppearance', {op='getOutfitsList'})['outfits'] +end + +function LLAppearance.getOutfitItems(id) + return leap.request('LLAppearance', {op='getOutfitItems', outfit_id = id})['items'] +end + +return LLAppearance diff --git a/indra/newview/scripts/lua/require/LLChat.lua b/indra/newview/scripts/lua/require/LLChat.lua new file mode 100644 index 0000000000..bc0fc86d22 --- /dev/null +++ b/indra/newview/scripts/lua/require/LLChat.lua @@ -0,0 +1,38 @@ +local leap = require 'leap' + +local LLChat = {} + +-- *************************************************************************** +-- Nearby chat +-- *************************************************************************** + +-- 0 is public nearby channel, other channels are used to communicate with LSL scripts +function LLChat.sendNearby(msg, channel) + leap.send('LLChatBar', {op='sendChat', message=msg, channel=channel}) +end + +function LLChat.sendWhisper(msg) + leap.send('LLChatBar', {op='sendChat', type='whisper', message=msg}) +end + +function LLChat.sendShout(msg) + leap.send('LLChatBar', {op='sendChat', type='shout', message=msg}) +end + +-- *************************************************************************** +-- Group chat +-- *************************************************************************** + +function LLChat.startGroupChat(group_id) + return leap.request('GroupChat', {op='startGroupChat', group_id=group_id}) +end + +function LLChat.leaveGroupChat(group_id) + leap.send('GroupChat', {op='leaveGroupChat', group_id=group_id}) +end + +function LLChat.sendGroupIM(msg, group_id) + leap.send('GroupChat', {op='sendGroupIM', message=msg, group_id=group_id}) +end + +return LLChat diff --git a/indra/newview/scripts/lua/require/LLChatListener.lua b/indra/newview/scripts/lua/require/LLChatListener.lua new file mode 100644 index 0000000000..82b28966ce --- /dev/null +++ b/indra/newview/scripts/lua/require/LLChatListener.lua @@ -0,0 +1,48 @@ +local fiber = require 'fiber' +local inspect = require 'inspect' +local leap = require 'leap' +local util = require 'util' + +local LLChatListener = {} +local waitfor = {} +local listener_name = {} + +function LLChatListener:new() + local obj = setmetatable({}, self) + self.__index = self + obj.name = 'Chat_listener' + + return obj +end + +util.classctor(LLChatListener) + +function LLChatListener:handleMessages(event_data) + print(inspect(event_data)) + return true +end + +function LLChatListener:start() + waitfor = leap.WaitFor(-1, self.name) + function waitfor:filter(pump, data) + if pump == "LLNearbyChat" then + return data + end + end + + fiber.launch(self.name, function() + event = waitfor:wait() + while event and self:handleMessages(event) do + event = waitfor:wait() + end + end) + + listener_name = leap.request(leap.cmdpump(), {op='listen', source='LLNearbyChat', listener="ChatListener", tweak=true}).listener +end + +function LLChatListener:stop() + leap.send(leap.cmdpump(), {op='stoplistening', source='LLNearbyChat', listener=listener_name}) + waitfor:close() +end + +return LLChatListener diff --git a/indra/newview/scripts/lua/require/LLDebugSettings.lua b/indra/newview/scripts/lua/require/LLDebugSettings.lua new file mode 100644 index 0000000000..cff1a63c21 --- /dev/null +++ b/indra/newview/scripts/lua/require/LLDebugSettings.lua @@ -0,0 +1,17 @@ +local leap = require 'leap' + +local LLDebugSettings = {} + +function LLDebugSettings.set(name, value) + leap.request('LLViewerControl', {op='set', group='Global', key=name, value=value}) +end + +function LLDebugSettings.toggle(name) + leap.request('LLViewerControl', {op='toggle', group='Global', key=name}) +end + +function LLDebugSettings.get(name) + return leap.request('LLViewerControl', {op='get', group='Global', key=name})['value'] +end + +return LLDebugSettings diff --git a/indra/newview/scripts/lua/require/LLFloaterAbout.lua b/indra/newview/scripts/lua/require/LLFloaterAbout.lua new file mode 100644 index 0000000000..a6e42d364f --- /dev/null +++ b/indra/newview/scripts/lua/require/LLFloaterAbout.lua @@ -0,0 +1,11 @@ +-- Engage the LLFloaterAbout LLEventAPI + +local leap = require 'leap' + +local LLFloaterAbout = {} + +function LLFloaterAbout.getInfo() + return leap.request('LLFloaterAbout', {op='getInfo'}) +end + +return LLFloaterAbout diff --git a/indra/newview/scripts/lua/require/LLGesture.lua b/indra/newview/scripts/lua/require/LLGesture.lua new file mode 100644 index 0000000000..343b611e2c --- /dev/null +++ b/indra/newview/scripts/lua/require/LLGesture.lua @@ -0,0 +1,23 @@ +-- Engage the LLGesture LLEventAPI + +local leap = require 'leap' + +local LLGesture = {} + +function LLGesture.getActiveGestures() + return leap.request('LLGesture', {op='getActiveGestures'})['gestures'] +end + +function LLGesture.isGesturePlaying(id) + return leap.request('LLGesture', {op='isGesturePlaying', id=id})['playing'] +end + +function LLGesture.startGesture(id) + leap.send('LLGesture', {op='startGesture', id=id}) +end + +function LLGesture.stopGesture(id) + leap.send('LLGesture', {op='stopGesture', id=id}) +end + +return LLGesture diff --git a/indra/newview/scripts/lua/require/LLInventory.lua b/indra/newview/scripts/lua/require/LLInventory.lua new file mode 100644 index 0000000000..2c80a8602b --- /dev/null +++ b/indra/newview/scripts/lua/require/LLInventory.lua @@ -0,0 +1,67 @@ +local leap = require 'leap' +local mapargs = require 'mapargs' +local result_view = require 'result_view' + +local function result(keys) + -- capture result_view() instances for both categories and items + local result_table = { + categories=result_view(keys.categories), + items=result_view(keys.items), + -- call result_table:close() to release result sets before garbage + -- collection or script completion + close = function(self) + result_view.close(keys.categories[1], keys.items[1]) + end + } + -- When the result_table is destroyed, close its result_views. + return LL.setdtor('LLInventory result', result_table, result_table.close) +end + +local LLInventory = {} + +-- Get the items/folders info by provided IDs, +-- reply will contain "items" and "categories" tables accordingly +function LLInventory.getItemsInfo(item_ids) + return result(leap.request('LLInventory', {op = 'getItemsInfo', item_ids=item_ids})) +end + +-- Get the table of folder type names, which can be later used to get the ID of the basic folders +function LLInventory.getFolderTypeNames() + return leap.request('LLInventory', {op = 'getFolderTypeNames'}).names +end + +-- Get the UUID of the basic folder("Textures", "My outfits", "Sounds" etc.) by specified folder type name +function LLInventory.getBasicFolderID(ft_name) + return leap.request('LLInventory', {op = 'getBasicFolderID', ft_name=ft_name}).id +end + +-- Get the table of asset type names, which can be later used to get the specific items via LLInventory.collectDescendantsIf(...) +function LLInventory.getAssetTypeNames() + return leap.request('LLInventory', {op = 'getAssetTypeNames'}).names +end + +-- Get the direct descendants of the 'folder_id' provided, +-- reply will contain "items" and "categories" tables accordingly +function LLInventory.getDirectDescendants(folder_id) + return result(leap.request('LLInventory', {op = 'getDirectDescendants', folder_id=folder_id})) +end +-- backwards compatibility +LLInventory.getDirectDescendents = LLInventory.getDirectDescendants + +-- Get the descendants of the 'folder_id' provided, which pass specified filters +-- reply will contain "items" and "categories" tables accordingly +-- LLInventory.collectDescendantsIf{ folder_id -- parent folder ID +-- [, name] -- name (substring) +-- [, desc] -- description (substring) +-- [, type] -- asset type +-- [, limit] -- item count limit in reply, maximum and default is 100 +-- [, filter_links]} -- EXCLUDE_LINKS - don't show links, ONLY_LINKS - only show links, INCLUDE_LINKS - show links too (default) +function LLInventory.collectDescendantsIf(...) + local args = mapargs('folder_id,name,desc,type,filter_links,limit', ...) + args.op = 'collectDescendantsIf' + return result(leap.request('LLInventory', args)) +end +-- backwards compatibility +LLInventory.collectDescendentsIf = LLInventory.collectDescendantsIf + +return LLInventory diff --git a/indra/newview/scripts/lua/require/Queue.lua b/indra/newview/scripts/lua/require/Queue.lua new file mode 100644 index 0000000000..5bc72e4057 --- /dev/null +++ b/indra/newview/scripts/lua/require/Queue.lua @@ -0,0 +1,51 @@ +-- from https://create.roblox.com/docs/luau/queues#implementing-queues, +-- amended per https://www.lua.org/pil/16.1.html + +-- While coding some scripting in Lua +-- I found that I needed a queua +-- I thought of linked list +-- But had to resist +-- For fear it might be too obscua. + +local util = require 'util' + +local Queue = {} + +function Queue:new() + local obj = setmetatable({}, self) + self.__index = self + + obj._first = 0 + obj._last = -1 + obj._queue = {} + + return obj +end + +util.classctor(Queue) + +-- Check if the queue is empty +function Queue:IsEmpty() + return self._first > self._last +end + +-- Add a value to the queue +function Queue:Enqueue(value) + local last = self._last + 1 + self._last = last + self._queue[last] = value +end + +-- Remove a value from the queue +function Queue:Dequeue() + if self:IsEmpty() then + return nil + end + local first = self._first + local value = self._queue[first] + self._queue[first] = nil + self._first = first + 1 + return value +end + +return Queue diff --git a/indra/newview/scripts/lua/require/Region.lua b/indra/newview/scripts/lua/require/Region.lua new file mode 100644 index 0000000000..e4eefece33 --- /dev/null +++ b/indra/newview/scripts/lua/require/Region.lua @@ -0,0 +1,17 @@ +LLFloaterAbout = require 'LLFloaterAbout' + +local Region = {} + +function Region.getInfo() + info = LLFloaterAbout.getInfo() + return { + HOSTNAME=info.HOSTNAME, + POSITION=info.POSITION, + POSITION_LOCAL=info.POSITION_LOCAL, + REGION=info.REGION, + SERVER_VERSION=info.SERVER_VERSION, + SLURL=info.SLURL, + } +end + +return Region diff --git a/indra/newview/scripts/lua/require/UI.lua b/indra/newview/scripts/lua/require/UI.lua new file mode 100644 index 0000000000..cf2695917e --- /dev/null +++ b/indra/newview/scripts/lua/require/UI.lua @@ -0,0 +1,243 @@ +-- Engage the viewer's UI + +local leap = require 'leap' +local mapargs = require 'mapargs' +local result_view = require 'result_view' +local Timer = (require 'timers').Timer +local util = require 'util' + +-- Allow lazily accessing UI submodules on demand, e.g. a reference to +-- UI.Floater lazily loads the UI/Floater module. +local UI = util.submoduledir({}, 'UI') + +-- *************************************************************************** +-- registered menu actions +-- *************************************************************************** +function UI.call(func, parameter) + -- 'call' is fire-and-forget + leap.request('UI', {op='call', ['function']=func, parameter=parameter}) +end + +function UI.callables() + return leap.request('UI', {op='callables'}).callables +end + +function UI.getValue(path) + return leap.request('UI', {op='getValue', path=path})['value'] +end + +-- *************************************************************************** +-- UI views +-- *************************************************************************** +-- Either: +-- wreq{op='Something', a=1, b=2, ...} +-- or: +-- (args should be local, as this wreq() call modifies it) +-- local args = {a=1, b=2, ...} +-- wreq('Something', args) +local function wreq(op_or_data, data_if_op) + if data_if_op ~= nil then + -- this is the wreq(op, data) form + data_if_op.op = op_or_data + op_or_data = data_if_op + end + return leap.request('LLWindow', op_or_data) +end + +-- omit 'parent' to list all view paths +function UI.listviews(parent) + return wreq{op='getPaths', under=parent} +end + +function UI.viewinfo(path) + return wreq{op='getInfo', path=path} +end + +-- *************************************************************************** +-- mouse actions +-- *************************************************************************** +-- pass a table: +-- UI.click{path=path +-- [, button='LEFT' | 'CENTER' | 'RIGHT'] +-- [, x=x, y=y] +-- [, hold=duration]} +function UI.click(...) + local args = mapargs('path,button,x,y,hold', ...) + args.button = args.button or 'LEFT' + local hold = args.hold or 1.0 + wreq('mouseMove', args) + wreq('mouseDown', args) + Timer(hold, 'wait') + wreq('mouseUp', args) +end + +-- pass a table as for UI.click() +function UI.doubleclick(...) + local args = mapargs('path,button,x,y', ...) + args.button = args.button or 'LEFT' + wreq('mouseDown', args) + wreq('mouseUp', args) + wreq('mouseDown', args) + wreq('mouseUp', args) +end + +-- UI.drag{path=, xoff=, yoff=} +function UI.drag(...) + local args = mapargs('path,xoff,yoff', ...) + -- query the specified path + local rect = UI.viewinfo(args.path).rect + local centerx = math.floor(rect.left + (rect.right - rect.left)/2) + local centery = math.floor(rect.bottom + (rect.top - rect.bottom)/2) + wreq{op='mouseMove', path=args.path, x=centerx, y=centery} + wreq{op='mouseDown', path=args.path, button='LEFT'} + wreq{op='mouseMove', path=args.path, x=centerx + args.xoff, y=centery + args.yoff} + wreq{op='mouseUp', path=args.path, button='LEFT'} +end + +-- *************************************************************************** +-- keyboard actions +-- *************************************************************************** +-- pass a table: +-- UI.keypress{ +-- [path=path] -- if omitted, default input field +-- [, char='x'] -- requires one of char, keycode, keysym +-- [, keycode=120] +-- keysym per https://github.com/secondlife/viewer/blob/main/indra/llwindow/llkeyboard.cpp#L68-L124 +-- [, keysym='Enter'] +-- [, mask={'SHIFT', 'CTL', 'ALT', 'MAC_CONTROL'}] -- some subset of these +-- } +function UI.keypress(...) + local args = mapargs('path,char,keycode,keysym,mask', ...) + if args.char == '\n' then + args.char = nil + args.keysym = 'Enter' + end + return wreq('keyDown', args) +end + +-- UI.type{text=, path=} +function UI.type(...) + local args = mapargs('text,path', ...) + if #args.text > 0 then + -- The caller's path may be specified in a way that requires recursively + -- searching parts of the LLView tree. No point in doing that more than + -- once. Capture the actual path found by that first call and use that for + -- subsequent calls. + local path = UI.keypress{path=args.path, char=string.sub(args.text, 1, 1)}.path + for i = 2, #args.text do + UI.keypress{path=path, char=string.sub(args.text, i, i)} + end + end +end + +-- *************************************************************************** +-- Snapshot +-- *************************************************************************** +-- UI.snapshot{filename=filename -- extension may be specified: bmp, jpeg, png +-- [, type='COLOR' | 'DEPTH'] +-- [, width=width][, height=height] -- uses current window size if not specified +-- [, showui=true][, showhud=true] +-- [, rebuild=false]} +function UI.snapshot(...) + local args = mapargs('filename,width,height,showui,showhud,rebuild,type', ...) + args.op = 'saveSnapshot' + return leap.request('LLViewerWindow', args).result +end + +-- *************************************************************************** +-- Top menu +-- *************************************************************************** + +function UI.getTopMenus() + return leap.request('UI', {op='getTopMenus'}).menus +end + +function UI.addMenu(...) + local args = mapargs('name,label', ...) + args.op = 'addMenu' + return leap.request('UI', args) +end + +function UI.setMenuVisible(name, visible) + return leap.request('UI', {op='setMenuVisible', name=name, visible=visible}) +end + +function UI.addMenuBranch(...) + local args = mapargs('name,label,parent_menu', ...) + args.op = 'addMenuBranch' + return leap.request('UI', args) +end + +-- see UI.callables() for valid values of 'func' +function UI.addMenuItem(...) + local args = mapargs('name,label,parent_menu,func,param,pos', ...) + args.op = 'addMenuItem' + return leap.request('UI', args) +end + +function UI.addMenuSeparator(...) + local args = mapargs('parent_menu,pos', ...) + args.op = 'addMenuSeparator' + return leap.request('UI', args) +end + +-- *************************************************************************** +-- Toolbar buttons +-- *************************************************************************** +-- Clears all buttons off the toolbars +function UI.clearAllToolbars() + leap.send('UI', {op='clearAllToolbars'}) +end + +function UI.defaultToolbars() + leap.send('UI', {op='defaultToolbars'}) +end + +-- UI.addToolbarBtn{btn_name=btn_name +-- [, toolbar= bottom] -- left, right, bottom -- default is bottom +-- [, rank=1]} -- position on the toolbar, starts at 0 (0 - first position, 1 - second position etc.) +function UI.addToolbarBtn(...) + local args = mapargs('btn_name,toolbar,rank', ...) + args.op = 'addToolbarBtn' + return leap.request('UI', args) +end + +-- Returns the rank(position) of the command in the original list +function UI.removeToolbarBtn(btn_name) + return leap.request('UI', {op = 'removeToolbarBtn', btn_name=btn_name}).rank +end + +function UI.getToolbarBtnNames() + return leap.request('UI', {op = 'getToolbarBtnNames'}).cmd_names +end + +-- *************************************************************************** +-- Floaters +-- *************************************************************************** +function UI.showFloater(floater_name) + leap.send("LLFloaterReg", {op = "showInstance", name = floater_name}) +end + +function UI.hideFloater(floater_name) + leap.send("LLFloaterReg", {op = "hideInstance", name = floater_name}) +end + +function UI.toggleFloater(floater_name) + leap.send("LLFloaterReg", {op = "toggleInstance", name = floater_name}) +end + +function UI.isFloaterVisible(floater_name) + return leap.request("LLFloaterReg", {op = "instanceVisible", name = floater_name}).visible +end + +function UI.closeAllFloaters() + return leap.send("UI", {op = "closeAllFloaters"}) +end + +function UI.getFloaterNames() + local key_length = leap.request("LLFloaterReg", {op = "getFloaterNames"}).floaters + local view = result_view(key_length) + return LL.setdtor('registered floater names', view, view.close) +end + +return UI diff --git a/indra/newview/scripts/lua/require/UI/Floater.lua b/indra/newview/scripts/lua/require/UI/Floater.lua new file mode 100644 index 0000000000..d057a74386 --- /dev/null +++ b/indra/newview/scripts/lua/require/UI/Floater.lua @@ -0,0 +1,146 @@ +-- Floater base class + +local leap = require 'leap' +local fiber = require 'fiber' +local util = require 'util' + +-- list of all the events that a LLLuaFloater might send +local event_list = leap.request("LLFloaterReg", {op="getFloaterEvents"}).events +local event_set = {} +for _, event in pairs(event_list) do + event_set[event] = true +end + +local function _event(event_name) + if not event_set[event_name] then + error("Incorrect event name: " .. event_name, 3) + end + return event_name +end + +-- --------------------------------------------------------------------------- +local Floater = {} + +-- Pass: +-- relative file path to floater's XUI definition file +-- optional: sign up for additional events for defined control +-- {<control_name>={action1, action2, ...}} +function Floater:new(path, extra) + local obj = setmetatable({}, self) + self.__index = self + + local path_parts = string.split(path, '/') + obj.name = 'Floater ' .. path_parts[#path_parts] + + obj._command = {op="showLuaFloater", xml_path=LL.abspath(path)} + if extra then + -- validate each of the actions for each specified control + for control, actions in pairs(extra) do + for _, action in pairs(actions) do + _event(action) + end + end + obj._command.extra_events = extra + end + + return obj +end + +util.classctor(Floater) + +function Floater:show() + -- leap.eventstream() returns the first response, and launches a + -- background fiber to call the passed callback with all subsequent + -- responses. + local event = leap.eventstream( + 'LLFloaterReg', + self._command, + -- handleEvents() returns false when done. + -- eventstream() expects a true return when done. + function(event) return not self:handleEvents(event) end) + self._pump = event.command_name + -- we might need the returned reqid to cancel the eventstream() fiber + self.reqid = event.reqid + + -- The response to 'showLuaFloater' *is* the 'post_build' event. Check if + -- subclass has a post_build() method. Honor the convention that if + -- handleEvents() returns false, we're done. + if not self:handleEvents(event) then + return + end +end + +function Floater:post(action) + leap.send(self._pump, action) +end + +function Floater:request(action) + return leap.request(self._pump, action) +end + +-- local inspect = require 'inspect' + +function Floater:handleEvents(event_data) + local event = event_data.event + if event_set[event] == nil then + LL.print_warning(string.format('%s received unknown event %q', self.name, event)) + end + + -- Before checking for a general (e.g.) commit() method, first look for + -- commit_ctrl_name(): in other words, concatenate the event name with the + -- ctrl_name, with an underscore between. If there exists such a specific + -- method, call that. + local handler, ret + if event_data.ctrl_name then + local specific = event .. '_' .. event_data.ctrl_name + handler = self[specific] + if handler then + ret = handler(self, event_data) + -- Avoid 'return ret or true' because we explicitly want to allow + -- the handler to return false. + if ret ~= nil then + return ret + else + return true + end + end + end + + -- No specific "event_on_ctrl()" method found; try just "event()" + handler = self[event] + if handler then + ret = handler(self, event_data) + if ret ~= nil then + return ret + end +-- else +-- print(string.format('%s ignoring event %s', self.name, inspect(event_data))) + end + + -- We check for event() method before recognizing floater_close in case + -- the consumer needs to react specially to closing the floater. Now that + -- we've checked, recognize it ourselves. Returning false terminates the + -- anonymous fiber function launched by leap.eventstream(). + if event == _event('floater_close') then + LL.print_warning(self.name .. ' closed') + return false + end + return true +end + +-- onCtrl() permits a different dispatch style in which the general event() +-- method explicitly calls (e.g.) +-- self:onCtrl(event_data, { +-- ctrl_name=function() +-- self:post(...) +-- end, +-- ... +-- }) +function Floater:onCtrl(event_data, ctrl_map) + local handler = ctrl_map[event_data.ctrl_name] + if handler then + handler() + end +end + +return Floater diff --git a/indra/newview/scripts/lua/require/UI/popup.lua b/indra/newview/scripts/lua/require/UI/popup.lua new file mode 100644 index 0000000000..8ccf3b87f3 --- /dev/null +++ b/indra/newview/scripts/lua/require/UI/popup.lua @@ -0,0 +1,82 @@ +local leap = require 'leap' +local mapargs = require 'mapargs' +local util = require 'util' + +-- notification is any name defined in notifications.xml as +-- <notification name=> +-- vars is a table providing values for [VAR] substitution keys in the +-- notification body +-- payload prepopulates the response table +-- wait=false means fire and forget, returning nil +-- wait=true waits for user response: +-- * If the viewer returns a table containing exactly one key=true pair, +-- popup() returns just that key. If the key is a string containing an +-- underscore, e.g. 'OK_okcancelbuttons', it's truncated at the first +-- underscore, e.g. 'OK'. +-- * Otherwise the viewer's response is returned unchanged. To suppress the +-- above transformations, pass a non-empty payload table; this will cause +-- the viewer to return a table with at least two keys. +local popup = util.setmetamethods{ + -- this gets called when a consumer calls popup(notification, vars, payload) + __call = function(self, ...) + local args = mapargs('notification,vars,payload,wait', ...) + -- we use convenience argument names different from 'LLNotifications' + -- listener + newargs = {op='requestAdd', + name=args.notification, + substitutions=args.vars, + payload=args.payload} + -- Specifically test (wait == false), NOT (not wait), because we treat + -- nil (omitted, default true) differently than false (explicitly + -- DON'T wait). + if args.wait == false then + leap.send('LLNotifications', newargs) + else + local response = leap.request('LLNotifications', newargs).response + -- response is typically a table. It might have multiple keys, + -- e.g. if caller passed non-empty payload. In that case, just + -- return the whole thing. + if type(response) ~= 'table' then + return response + end + -- get first key=value pair, if any + local key, value = next(response) + if (not key) or next(response, key) then + -- key == nil means response is empty + -- next(response, non-nil first key) ~= nil means at least two keys + return response + end + -- Here response is a table containing exactly one key. The + -- notifications system typically returns a table of the form + -- {OK_okcancelbuttons=true}, which is tricky to test for because it + -- varies with each set of buttons. + if value == true then + -- change {key=true} to plain key + response = key + if type(response) == 'string' then + -- change 'OK_okcancelbuttons' to plain 'OK' + response = string.split(response, '_')[1] + end + end + return response + end + end +} + +function popup:alert(message, payload) + return self('GenericAlert', {MESSAGE=message, payload=payload}) +end + +function popup:alertOK(message, payload) + return self('GenericAlertOK', {MESSAGE=message, payload=payload}) +end + +function popup:alertYesCancel(message, payload) + return self('GenericAlertYesCancel', {MESSAGE=message, payload=payload}) +end + +function popup:tip(message, payload) + self{'SystemMessageTip', {MESSAGE=message, payload=payload}, wait=false} +end + +return popup diff --git a/indra/newview/scripts/lua/require/WaitQueue.lua b/indra/newview/scripts/lua/require/WaitQueue.lua new file mode 100644 index 0000000000..7e10d03295 --- /dev/null +++ b/indra/newview/scripts/lua/require/WaitQueue.lua @@ -0,0 +1,88 @@ +-- WaitQueue isa Queue with the added feature that when the queue is empty, +-- the Dequeue() operation blocks the calling coroutine until some other +-- coroutine Enqueue()s a new value. + +local fiber = require('fiber') +local Queue = require('Queue') +local util = require('util') + +local function dbg(...) end +-- local dbg = require('printf') + +local WaitQueue = Queue() + +function WaitQueue:new() + local obj = Queue() + setmetatable(obj, self) + self.__index = self + + obj._waiters = {} + obj._closed = false + return obj +end + +util.classctor(WaitQueue) + +function WaitQueue:Enqueue(value) + if self._closed then + error("can't Enqueue() on closed Queue") + end + -- can't simply call Queue:Enqueue(value)! That calls the method on the + -- Queue class definition, instead of calling Queue:Enqueue() on self. + -- Hand-expand the Queue:Enqueue() syntactic sugar. + Queue.Enqueue(self, value) + self:_wake_waiters() +end + +function WaitQueue:_wake_waiters() + -- WaitQueue is designed to support multi-producer, multi-consumer use + -- cases. With multiple consumers, if more than one is trying to + -- Dequeue() from an empty WaitQueue, we'll have multiple waiters. + -- Unlike OS threads, with cooperative concurrency it doesn't make sense + -- to "notify all": we need wake only one of the waiting Dequeue() + -- callers. + if ((not self:IsEmpty()) or self._closed) and next(self._waiters) then + -- Pop the oldest waiting coroutine instead of the most recent, for + -- more-or-less round robin fairness. But skip any coroutines that + -- have gone dead in the meantime. + local waiter = table.remove(self._waiters, 1) + while waiter and fiber.status(waiter) == "dead" do + waiter = table.remove(self._waiters, 1) + end + -- do we still have at least one waiting coroutine? + if waiter then + -- don't pass the head item: let the resumed coroutine retrieve it + fiber.wake(waiter) + end + end +end + +function WaitQueue:Dequeue() + while self:IsEmpty() do + -- Don't check for closed until the queue is empty: producer can close + -- the queue while there are still items left, and we want the + -- consumer(s) to retrieve those last few items. + if self._closed then + dbg('WaitQueue:Dequeue(): closed') + return nil + end + dbg('WaitQueue:Dequeue(): waiting') + -- add the running coroutine to the list of waiters + dbg('WaitQueue:Dequeue() running %s', tostring(coroutine.running() or 'main')) + table.insert(self._waiters, fiber.running()) + -- then let somebody else run + fiber.wait() + end + -- here we're sure this queue isn't empty + dbg('WaitQueue:Dequeue() calling Queue.Dequeue()') + return Queue.Dequeue(self) +end + +function WaitQueue:close() + self._closed = true + -- close() is like Enqueueing an end marker. If there are waiting + -- consumers, give them a chance to see we're closed. + self:_wake_waiters() +end + +return WaitQueue diff --git a/indra/newview/scripts/lua/require/coro.lua b/indra/newview/scripts/lua/require/coro.lua new file mode 100644 index 0000000000..616a797e95 --- /dev/null +++ b/indra/newview/scripts/lua/require/coro.lua @@ -0,0 +1,67 @@ +-- Manage Lua coroutines + +local coro = {} + +coro._coros = {} + +-- Launch a Lua coroutine: create and resume. +-- Returns: new coroutine, values yielded or returned from initial resume() +-- If initial resume() encountered an error, propagates the error. +function coro.launch(func, ...) + local co = coroutine.create(func) + table.insert(coro._coros, co) + return co, coro.resume(co, ...) +end + +-- resume() wrapper to propagate errors +function coro.resume(co, ...) + -- if there's an idiom other than table.pack() to assign an arbitrary + -- number of return values, I don't yet know it + local ok_result = table.pack(coroutine.resume(co, ...)) + if not ok_result[1] then + -- if [1] is false, then [2] is the error message + error(ok_result[2]) + end + -- ok is true, whew, just return the rest of the values + return table.unpack(ok_result, 2) +end + +-- yield to other coroutines even if you don't know whether you're in a +-- created coroutine or the main coroutine +function coro.yield(...) + if coroutine.running() then + -- this is a real coroutine, yield normally + return coroutine.yield(...) + else + -- This is the main coroutine: coroutine.yield() doesn't work. + -- But we can take a spin through previously-launched coroutines. + -- Walk a copy of coro._coros in case any of these coroutines launches + -- another: next() forbids creating new entries during traversal. + for co in coro._live_coros_iter, table.clone(coro._coros) do + coro.resume(co) + end + end +end + +-- Walk coro._coros table, returning running or suspended coroutines. +-- Once a coroutine becomes dead, remove it from _coros and don't return it. +function coro._live_coros() + return coro._live_coros_iter, coro._coros +end + +-- iterator function for _live_coros() +function coro._live_coros_iter(t, idx) + local k, co = next(t, idx) + while k and coroutine.status(co) == 'dead' do +-- t[k] = nil + -- See coro.yield(): sometimes we traverse a copy of _coros, but if we + -- discover a dead coroutine in that copy, delete it from _coros + -- anyway. Deleting it from a temporary copy does nothing. + coro._coros[k] = nil + coroutine.close(co) + k, co = next(t, k) + end + return co +end + +return coro diff --git a/indra/newview/scripts/lua/require/fiber.lua b/indra/newview/scripts/lua/require/fiber.lua new file mode 100644 index 0000000000..b3c684dd67 --- /dev/null +++ b/indra/newview/scripts/lua/require/fiber.lua @@ -0,0 +1,346 @@ +-- Organize Lua coroutines into fibers. + +-- In this usage, the difference between coroutines and fibers is that fibers +-- have a scheduler. Yielding a fiber means allowing other fibers, plural, to +-- run: it's more than just returning control to the specific Lua thread that +-- resumed the running coroutine. + +-- fiber.launch() creates a new fiber ready to run. +-- fiber.status() reports (augmented) status of the passed fiber: instead of +-- 'suspended', it returns either 'ready' or 'waiting' +-- fiber.yield() allows other fibers to run, but leaves the calling fiber +-- ready to run. +-- fiber.wait() marks the running fiber not ready, and resumes other fibers. +-- fiber.wake() marks the designated suspended fiber ready to run, but does +-- not yet resume it. +-- fiber.run() runs all current fibers until all have terminated (successfully +-- or with an error). + +local printf = require 'printf' +local function dbg(...) end +-- local dbg = printf +local coro = require 'coro' + +local fiber = {} + +-- The tables in which we track fibers must have weak keys so dead fibers +-- can be garbage-collected. +local weak_values = {__mode='v'} +local weak_keys = {__mode='k'} + +-- Track each current fiber as being either ready to run or not ready +-- (waiting). wait() moves the running fiber from ready to waiting; wake() +-- moves the designated fiber from waiting back to ready. +-- The ready table is used as a list so yield() can go round robin. +local ready = setmetatable({'main'}, weak_keys) +-- The waiting table is used as a set because order doesn't matter. +local waiting = setmetatable({}, weak_keys) + +-- Every fiber has a name, for diagnostic purposes. Names must be unique. +-- A colliding name will be suffixed with an integer. +-- Predefine 'main' with our marker so nobody else claims that name. +local names = setmetatable({main='main'}, weak_keys) +local byname = setmetatable({main='main'}, weak_values) +-- each colliding name has its own distinct suffix counter +local suffix = {} + +-- Specify a nullary idle() callback to be called whenever there are no ready +-- fibers but there are waiting fibers. The idle() callback is responsible for +-- changing zero or more waiting fibers to ready fibers by calling +-- fiber.wake(), although a given call may leave them all still waiting. +-- When there are no ready fibers, it's a good idea for the idle() function to +-- return control to a higher-level execution agent. Simply returning without +-- changing any fiber's status will spin the CPU. +-- The idle() callback can return non-nil to exit fiber.run() with that value. +function fiber._idle() + error('fiber.yield(): you must first call set_idle(nullary idle() function)') +end + +function fiber.set_idle(func) + fiber._idle = func +end + +-- Launch a new Lua fiber, ready to run. +function fiber.launch(name, func, ...) + local args = table.pack(...) + local co = coroutine.create(function() func(table.unpack(args)) end) + -- a new fiber is ready to run + table.insert(ready, co) + local namekey = name + while byname[namekey] do + if not suffix[name] then + suffix[name] = 1 + end + suffix[name] += 1 + namekey = name .. tostring(suffix[name]) + end + -- found a namekey not yet in byname: set it + byname[namekey] = co + -- and remember it as this fiber's name + names[co] = namekey +-- dbg('launch(%s)', namekey) +-- dbg('byname[%s] = %s', namekey, tostring(byname[namekey])) +-- dbg('names[%s] = %s', tostring(co), names[co]) +-- dbg('ready[-1] = %s', tostring(ready[#ready])) +end + +-- for debugging +function format_all() + output = {} + table.insert(output, 'Ready fibers:' .. if next(ready) then '' else ' none') + for _, co in pairs(ready) do + table.insert(output, string.format(' %s: %s', fiber.get_name(co), fiber.status(co))) + end + table.insert(output, 'Waiting fibers:' .. if next(waiting) then '' else ' none') + for co in pairs(waiting) do + table.insert(output, string.format(' %s: %s', fiber.get_name(co), fiber.status(co))) + end + return table.concat(output, '\n') +end + +function fiber.print_all() + print(format_all()) +end + +-- return either the running coroutine or, if called from the main thread, +-- 'main' +function fiber.running() + return coroutine.running() or 'main' +end + +-- Query a fiber's name (nil for the running fiber) +function fiber.get_name(co) + return names[co or fiber.running()] or 'unknown' +end + +-- Query status of the passed fiber +function fiber.status(co) + local running = coroutine.running() + if (not co) or co == running then + -- silly to ask the status of the running fiber: it's 'running' + return 'running' + end + if co ~= 'main' then + -- for any coroutine but main, consult coroutine.status() + local status = coroutine.status(co) + if status ~= 'suspended' then + return status + end + -- here co is suspended, answer needs further refinement + else + -- co == 'main' + if not running then + -- asking about 'main' from the main fiber + return 'running' + end + -- asking about 'main' from some other fiber, so presumably main is suspended + end + -- here we know co is suspended -- but is it ready to run? + if waiting[co] then + return 'waiting' + end + -- not waiting should imply ready: sanity check + if table.find(ready, co) then + return 'ready' + end + -- Calls within yield() between popping the next ready fiber and + -- re-appending it to the list are in this state. Once we're done + -- debugging yield(), we could reinstate either of the below. +-- error(string.format('fiber.status(%s) is stumped', fiber.get_name(co))) +-- print(string.format('*** fiber.status(%s) is stumped', fiber.get_name(co))) + return '(unknown)' +end + +-- change the running fiber's status to waiting +local function set_waiting() + -- if called from the main fiber, inject a 'main' marker into the list + co = fiber.running() + -- delete from ready list + local i = table.find(ready, co) + if i then + table.remove(ready, i) + end + -- add to waiting list + waiting[co] = true +end + +-- Suspend the current fiber until some other fiber calls fiber.wake() on it +function fiber.wait() + dbg('Fiber %q waiting', fiber.get_name()) + set_waiting() + -- now yield to other fibers + fiber.yield() +end + +-- Mark a suspended fiber as being ready to run +function fiber.wake(co) + if not waiting[co] then + error(string.format('fiber.wake(%s) but status=%s, ready=%s, waiting=%s', + names[co], fiber.status(co), ready[co], waiting[co])) + end + -- delete from waiting list + waiting[co] = nil + -- add to end of ready list + table.insert(ready, co) + dbg('Fiber %q ready', fiber.get_name(co)) + -- but don't yet resume it: that happens next time we reach yield() +end + +-- pop and return the next not-dead fiber in the ready list, or nil if none remain +local function live_ready_iter() + -- don't write: + -- for co in table.remove, ready, 1 + -- because it would keep passing a new second parameter! + for co in function() return table.remove(ready, 1) end do + dbg('%s live_ready_iter() sees %s, status %s', + fiber.get_name(), fiber.get_name(co), fiber.status(co)) + -- keep removing the head entry until we find one that's not dead, + -- discarding any dead coroutines along the way + if co == 'main' or coroutine.status(co) ~= 'dead' then + dbg('%s live_ready_iter() returning %s', + fiber.get_name(), fiber.get_name(co)) + return co + end + end + dbg('%s live_ready_iter() returning nil', fiber.get_name()) + return nil +end + +-- prune the set of waiting fibers +local function prune_waiting() + for waiter in pairs(waiting) do + if waiter ~= 'main' and coroutine.status(waiter) == 'dead' then + waiting[waiter] = nil + end + end +end + +-- Run other ready fibers, leaving this one ready, returning after a cycle. +-- Returns: +-- * true, nil if there remain other live fibers, whether ready or waiting, +-- but it's our turn to run +-- * false, nil if this is the only remaining fiber +-- * nil, x if configured idle() callback returns non-nil x +local function scheduler() + dbg('scheduler():\n%s', format_all()) + -- scheduler() is asymmetric because Lua distinguishes the main thread + -- from other coroutines. The main thread can't yield; it can only resume + -- other coroutines. So although an arbitrary coroutine could resume still + -- other arbitrary coroutines, it could NOT resume the main thread because + -- the main thread can't yield. Therefore, scheduler() delegates its real + -- processing to the main thread. If called from a coroutine, pass control + -- back to the main thread. + if coroutine.running() then + -- this is a real coroutine, yield normally to main thread + coroutine.yield() + -- main certainly still exists + return true + end + + -- This is the main fiber: coroutine.yield() doesn't work. + -- Instead, resume each of the ready fibers. + -- Prune the set of waiting fibers after every time fiber business logic + -- runs (i.e. other fibers might have terminated or hit error), such as + -- here on entry. + prune_waiting() + local others, idle_stop + repeat + for co in live_ready_iter do + -- seize the opportunity to make sure the viewer isn't shutting down + LL.check_stop() + -- before we re-append co, is it the only remaining entry? + others = next(ready) + -- co is live, re-append it to the ready list + table.insert(ready, co) + if co == 'main' then + -- Since we know the caller is the main fiber, it's our turn. + -- Tell caller if there are other ready or waiting fibers. + return others or next(waiting) + end + -- not main, but some other ready coroutine: + -- use coro.resume() so we'll propagate any error encountered + coro.resume(co) + prune_waiting() + end + -- Here there are no ready fibers. Are there any waiting fibers? + if not next(waiting) then + return false + end + -- there are waiting fibers: call consumer's configured idle() function + idle_stop = fiber._idle() + if idle_stop ~= nil then + return nil, idle_stop + end + prune_waiting() + -- loop "forever", that is, until: + -- * main is ready, or + -- * there are neither ready fibers nor waiting fibers, or + -- * fiber._idle() returned non-nil + until false +end + +-- Let other fibers run. This is useful in either of two cases: +-- * fiber.wait() calls this to run other fibers while this one is waiting. +-- fiber.yield() (and therefore fiber.wait()) works from the main thread as +-- well as from explicitly-launched fibers, without the caller having to +-- care. +-- * A long-running fiber that doesn't often call fiber.wait() should sprinkle +-- in fiber.yield() calls to interleave processing on other fibers. +function fiber.yield() + -- The difference between this and fiber.run() is that fiber.yield() + -- assumes its caller has work to do. yield() returns to its caller as + -- soon as scheduler() pops this fiber from the ready list. fiber.run() + -- continues looping until all other fibers have terminated, or the + -- set_idle() callback tells it to stop. + local others, idle_done = scheduler() + -- scheduler() returns either if we're ready, or if idle_done ~= nil. + if idle_done ~= nil then + -- Returning normally from yield() means the caller can carry on with + -- its pending work. But in this case scheduler() returned because the + -- configured set_idle() function interrupted it -- not because we're + -- actually ready. Don't return normally. + error('fiber.set_idle() interrupted yield() with: ' .. tostring(idle_done)) + end + -- We're ready! Just return to caller. In this situation we don't care + -- whether there are other ready fibers. + dbg('fiber.yield() returning to %s (%sothers are ready)', + fiber.get_name(), ((not others) and "no " or "")) +end + +-- Run fibers until all but main have terminated: return nil. +-- Or until configured idle() callback returns x ~= nil: return x. +function fiber.run() + -- A fiber calling run() is not also doing other useful work. Remove the + -- calling fiber from the ready list. Otherwise yield() would keep seeing + -- that our caller is ready and return to us, instead of realizing that + -- all coroutines are waiting and call idle(). But don't say we're + -- waiting, either, because then when all other fibers have terminated + -- we'd call idle() forever waiting for something to make us ready again. + local i = table.find(ready, fiber.running()) + if i then + table.remove(ready, i) + end + local others, idle_done + repeat + dbg('%s calling fiber.run() calling scheduler()', fiber.get_name()) + others, idle_done = scheduler() + dbg("%s fiber.run()'s scheduler() returned %s, %s", fiber.get_name(), + tostring(others), tostring(idle_done)) + until (not others) + dbg('%s fiber.run() done', fiber.get_name()) + -- For whatever it's worth, put our own fiber back in the ready list. + table.insert(ready, fiber.running()) + -- Once there are no more waiting fibers, and the only ready fiber is + -- us, return to caller. All previously-launched fibers are done. Possibly + -- the chunk is done, or the chunk may decide to launch a new batch of + -- fibers. + return idle_done +end + +-- Make sure we finish up with a call to run(). That allows a consuming script +-- to kick off some number of fibers, do some work on the main thread and then +-- fall off the end of the script without explicitly calling fiber.run(). +-- run() ensures the rest of the fibers run to completion (or error). +LL.atexit(fiber.run) + +return fiber diff --git a/indra/newview/scripts/lua/require/inspect.lua b/indra/newview/scripts/lua/require/inspect.lua new file mode 100644 index 0000000000..9900a0b81b --- /dev/null +++ b/indra/newview/scripts/lua/require/inspect.lua @@ -0,0 +1,371 @@ +local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local math = _tl_compat and _tl_compat.math or math; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table +local inspect = {Options = {}, } + + + + + + + + + + + + + + + + + +inspect._VERSION = 'inspect.lua 3.1.0' +inspect._URL = 'http://github.com/kikito/inspect.lua' +inspect._DESCRIPTION = 'human-readable representations of tables' +inspect._LICENSE = [[ + MIT LICENSE + + Copyright (c) 2022 Enrique GarcÃa Cota + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +]] +inspect.KEY = setmetatable({}, { __tostring = function() return 'inspect.KEY' end }) +inspect.METATABLE = setmetatable({}, { __tostring = function() return 'inspect.METATABLE' end }) + +local tostring = tostring +local rep = string.rep +local match = string.match +local char = string.char +local gsub = string.gsub +local fmt = string.format + +local _rawget +if rawget then + _rawget = rawget +else + _rawget = function(t, k) return t[k] end +end + +local function rawpairs(t) + return next, t, nil +end + + + +local function smartQuote(str) + if match(str, '"') and not match(str, "'") then + return "'" .. str .. "'" + end + return '"' .. gsub(str, '"', '\\"') .. '"' +end + + +local shortControlCharEscapes = { + ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", + ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\127"] = "\\127", +} +local longControlCharEscapes = { ["\127"] = "\127" } +for i = 0, 31 do + local ch = char(i) + if not shortControlCharEscapes[ch] then + shortControlCharEscapes[ch] = "\\" .. i + longControlCharEscapes[ch] = fmt("\\%03d", i) + end +end + +local function escape(str) + return (gsub(gsub(gsub(str, "\\", "\\\\"), + "(%c)%f[0-9]", longControlCharEscapes), + "%c", shortControlCharEscapes)) +end + +local luaKeywords = { + ['and'] = true, + ['break'] = true, + ['do'] = true, + ['else'] = true, + ['elseif'] = true, + ['end'] = true, + ['false'] = true, + ['for'] = true, + ['function'] = true, + ['goto'] = true, + ['if'] = true, + ['in'] = true, + ['local'] = true, + ['nil'] = true, + ['not'] = true, + ['or'] = true, + ['repeat'] = true, + ['return'] = true, + ['then'] = true, + ['true'] = true, + ['until'] = true, + ['while'] = true, +} + +local function isIdentifier(str) + return type(str) == "string" and + not not str:match("^[_%a][_%a%d]*$") and + not luaKeywords[str] +end + +local flr = math.floor +local function isSequenceKey(k, sequenceLength) + return type(k) == "number" and + flr(k) == k and + 1 <= (k) and + k <= sequenceLength +end + +local defaultTypeOrders = { + ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, + ['function'] = 5, ['userdata'] = 6, ['thread'] = 7, +} + +local function sortKeys(a, b) + local ta, tb = type(a), type(b) + + + if ta == tb and (ta == 'string' or ta == 'number') then + return (a) < (b) + end + + local dta = defaultTypeOrders[ta] or 100 + local dtb = defaultTypeOrders[tb] or 100 + + + return dta == dtb and ta < tb or dta < dtb +end + +local function getKeys(t) + + local seqLen = 1 + while _rawget(t, seqLen) ~= nil do + seqLen = seqLen + 1 + end + seqLen = seqLen - 1 + + local keys, keysLen = {}, 0 + for k in rawpairs(t) do + if not isSequenceKey(k, seqLen) then + keysLen = keysLen + 1 + keys[keysLen] = k + end + end + table.sort(keys, sortKeys) + return keys, keysLen, seqLen +end + +local function countCycles(x, cycles) + if type(x) == "table" then + if cycles[x] then + cycles[x] = cycles[x] + 1 + else + cycles[x] = 1 + for k, v in rawpairs(x) do + countCycles(k, cycles) + countCycles(v, cycles) + end + countCycles(getmetatable(x), cycles) + end + end +end + +local function makePath(path, a, b) + local newPath = {} + local len = #path + for i = 1, len do newPath[i] = path[i] end + + newPath[len + 1] = a + newPath[len + 2] = b + + return newPath +end + + +local function processRecursive(process, + item, + path, + visited) + if item == nil then return nil end + if visited[item] then return visited[item] end + + local processed = process(item, path) + if type(processed) == "table" then + local processedCopy = {} + visited[item] = processedCopy + local processedKey + + for k, v in rawpairs(processed) do + processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) + if processedKey ~= nil then + processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) + end + end + + local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) + if type(mt) ~= 'table' then mt = nil end + setmetatable(processedCopy, mt) + processed = processedCopy + end + return processed +end + +local function puts(buf, str) + buf.n = buf.n + 1 + buf[buf.n] = str +end + + + +local Inspector = {} + + + + + + + + + + +local Inspector_mt = { __index = Inspector } + +local function tabify(inspector) + puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level)) +end + +function Inspector:getId(v) + local id = self.ids[v] + local ids = self.ids + if not id then + local tv = type(v) + id = (ids[tv] or 0) + 1 + ids[v], ids[tv] = id, id + end + return tostring(id) +end + +function Inspector:putValue(v) + local buf = self.buf + local tv = type(v) + if tv == 'string' then + puts(buf, smartQuote(escape(v))) + elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or + tv == 'cdata' or tv == 'ctype' then + puts(buf, tostring(v)) + elseif tv == 'table' and not self.ids[v] then + local t = v + + if t == inspect.KEY or t == inspect.METATABLE then + puts(buf, tostring(t)) + elseif self.level >= self.depth then + puts(buf, '{...}') + else + if self.cycles[t] > 1 then puts(buf, fmt('<%d>', self:getId(t))) end + + local keys, keysLen, seqLen = getKeys(t) + + puts(buf, '{') + self.level = self.level + 1 + + for i = 1, seqLen + keysLen do + if i > 1 then puts(buf, ',') end + if i <= seqLen then + puts(buf, ' ') + self:putValue(t[i]) + else + local k = keys[i - seqLen] + tabify(self) + if isIdentifier(k) then + puts(buf, k) + else + puts(buf, "[") + self:putValue(k) + puts(buf, "]") + end + puts(buf, ' = ') + self:putValue(t[k]) + end + end + + local mt = getmetatable(t) + if type(mt) == 'table' then + if seqLen + keysLen > 0 then puts(buf, ',') end + tabify(self) + puts(buf, '<metatable> = ') + self:putValue(mt) + end + + self.level = self.level - 1 + + if keysLen > 0 or type(mt) == 'table' then + tabify(self) + elseif seqLen > 0 then + puts(buf, ' ') + end + + puts(buf, '}') + end + + else + puts(buf, fmt('<%s %d>', tv, self:getId(v))) + end +end + + + + +function inspect.inspect(root, options) + options = options or {} + + local depth = options.depth or (math.huge) + local newline = options.newline or '\n' + local indent = options.indent or ' ' + local process = options.process + + if process then + root = processRecursive(process, root, {}, {}) + end + + local cycles = {} + countCycles(root, cycles) + + local inspector = setmetatable({ + buf = { n = 0 }, + ids = {}, + cycles = cycles, + depth = depth, + level = 0, + newline = newline, + indent = indent, + }, Inspector_mt) + + inspector:putValue(root) + + return table.concat(inspector.buf) +end + +setmetatable(inspect, { + __call = function(_, root, options) + return inspect.inspect(root, options) + end, +}) + +return inspect diff --git a/indra/newview/scripts/lua/require/leap.lua b/indra/newview/scripts/lua/require/leap.lua new file mode 100644 index 0000000000..82f91ce9e9 --- /dev/null +++ b/indra/newview/scripts/lua/require/leap.lua @@ -0,0 +1,550 @@ +-- Lua implementation of LEAP (LLSD Event API Plugin) protocol +-- +-- This module supports Lua scripts run by the Second Life viewer. +-- +-- LEAP protocol passes LLSD objects, converted to/from Lua tables, in both +-- directions. A typical LLSD object is a map containing keys 'pump' and +-- 'data'. +-- +-- The viewer's Lua post_to(pump, data) function posts 'data' to the +-- LLEventPump 'pump'. This is typically used to engage an LLEventAPI method. +-- +-- Similarly, the viewer gives each Lua script its own LLEventPump with a +-- unique name. That name is returned by get_event_pumps(). Every event +-- received on that LLEventPump is queued for retrieval by get_event_next(), +-- which returns (pump, data): the name of the LLEventPump on which the event +-- was received and the received event data. When the queue is empty, +-- get_event_next() blocks the calling Lua script until the next event is +-- received. +-- +-- Usage: +-- 1. Launch some number of Lua coroutines. The code in each coroutine may +-- call leap.send(), leap.request() or leap.generate(). leap.send() returns +-- immediately ("fire and forget"). leap.request() blocks the calling +-- coroutine until it receives and returns the viewer's response to its +-- request. leap.generate() expects an arbitrary number of responses to the +-- original request. +-- 2. To handle events from the viewer other than direct responses to +-- requests, instantiate a leap.WaitFor object with a filter(pump, data) +-- override method that returns non-nil for desired events. A coroutine may +-- call wait() on any such WaitFor. +-- 3. Once the coroutines have been launched, call leap.process() on the main +-- coroutine. process() retrieves incoming events from the viewer and +-- dispatches them to waiting request() or generate() calls, or to +-- appropriate WaitFor instances. process() returns when either +-- get_event_next() raises an error or the viewer posts nil to the script's +-- reply pump to indicate it's done. +-- 4. Alternatively, a running coroutine may call leap.done() to break out of +-- leap.process(). process() won't notice until the next event from the +-- viewer, though. + +local fiber = require('fiber') +local ErrorQueue = require('ErrorQueue') +local inspect = require('inspect') +local function dbg(...) end +-- local dbg = require('printf') +local util = require('util') + +local leap = {} + +-- reply: string name of reply LLEventPump. Any events the viewer posts to +-- this pump will be queued for get_event_next(). We usually specify it as the +-- reply pump for requests to internal viewer services. +-- command: string name of command LLEventPump. post_to(command, ...) +-- engages LLLeapListener operations such as listening on a specified other +-- LLEventPump, etc. +local reply, command = LL.get_event_pumps() +-- Dict of features added to the LEAP protocol since baseline implementation. +-- Before engaging a new feature that might break an older viewer, we can +-- check for the presence of that feature key. This table is solely about the +-- LEAP protocol itself, the way we communicate with the viewer. To discover +-- whether a given listener exists, or supports a particular operation, use +-- command's "getAPI" operation. +-- For Lua, command's "getFeatures" operation suffices? +-- leap._features = {} + +-- Each outstanding request() or generate() call has a corresponding +-- WaitForReqid object (later in this module) to handle the +-- response(s). If an incoming event contains an echoed ["reqid"] key, +-- we can look up the appropriate WaitForReqid object more efficiently +-- in a dict than by tossing such objects into the usual waitfors list. +-- Note: the ["reqid"] must be unique, otherwise we could end up +-- replacing an earlier WaitForReqid object in pending with a +-- later one. That means that no incoming event will ever be given to +-- the old WaitForReqid object. Any coroutine waiting on the discarded +-- WaitForReqid object would therefore wait forever. +-- pending is NOT a weak table because the caller of request() or generate() +-- never sees the WaitForReqid object. pending holds the only reference, so +-- it should NOT be garbage-collected. +local pending = {} +-- Our consumer will instantiate some number of WaitFor subclass objects. +-- As these are traversed in descending priority order, we must keep +-- them in a list. +-- Anyone who instantiates a WaitFor subclass object should retain a reference +-- to it. Once the consuming script drops the reference, allow Lua to +-- garbage-collect the WaitFor despite its entry in waitfors. +local weak_values = {__mode='v'} +local waitfors = setmetatable({}, weak_values) +-- It has been suggested that we should use UUIDs as ["reqid"] values, +-- since UUIDs are guaranteed unique. However, as the "namespace" for +-- ["reqid"] values is our very own reply pump, we can get away with +-- an integer. +leap._reqid = 0 +-- break leap.process() loop +leap._done = false + +-- get the name of the reply pump +function leap.replypump() + return reply +end + +-- get the name of the command pump +function leap.cmdpump() + return command +end + +-- Fire and forget. Send the specified request LLSD, expecting no reply. +-- In fact, should the request produce an eventual reply, it will be +-- treated as an unsolicited event. +-- +-- See also request(), generate(). +function leap.send(pump, data, reqid) + local data = data + if type(data) == 'table' then + data = table.clone(data) + data['reply'] = reply + if reqid ~= nil then + data['reqid'] = reqid + end + end + dbg('leap.send(%s, %s) calling post_on()', pump, data) + LL.post_on(pump, data) +end + +-- common setup code shared by request() and generate() +local function requestSetup(pump, data) + -- invent a new, unique reqid + leap._reqid += 1 + local reqid = leap._reqid + -- Instantiate a new WaitForReqid object. The priority is irrelevant + -- because, unlike the WaitFor base class, WaitForReqid does not + -- self-register on our waitfors list. Instead, capture the new + -- WaitForReqid object in pending so dispatch() can find it. + local waitfor = leap.WaitForReqid(reqid) + pending[reqid] = waitfor + -- Pass reqid to send() to stamp it into (a copy of) the request data. + dbg('requestSetup(%s, %s) storing %s', pump, data, waitfor.name) + leap.send(pump, data, reqid) + return reqid, waitfor +end + +-- Send the specified request LLSD, expecting exactly one reply. Block +-- the calling coroutine until we receive that reply. +-- +-- Every request() (or generate()) LLSD block we send will get stamped +-- with a distinct ["reqid"] value. The requested event API must echo the +-- same ["reqid"] field in each reply associated with that request. This way +-- we can correctly dispatch interleaved replies from different requests. +-- +-- If the desired event API doesn't support the ["reqid"] echo convention, +-- you should use send() instead -- since request() or generate() would +-- wait forever for a reply stamped with that ["reqid"] -- and intercept +-- any replies using WaitFor. +-- +-- Unless the request data already contains a ["reply"] key, we insert +-- reply=self.replypump to try to ensure that the expected reply will be +-- returned over the socket. +-- +-- See also send(), generate(). +function leap.request(pump, data) + local reqid, waitfor = requestSetup(pump, data) + dbg('leap.request(%s, %s) about to wait on %s', pump, data, tostring(waitfor)) + local ok, response = pcall(waitfor.wait, waitfor) + dbg('leap.request(%s, %s) got %s: %s', pump, data, ok, response) + -- kill off temporary WaitForReqid object, even if error + pending[reqid] = nil + if not ok then + error(response) + elseif response.error then + error(response.error) + else + return response + end +end + +-- Send the specified request LLSD, expecting an arbitrary number of replies. +-- Each one is returned on request. +-- +-- Usage: +-- sequence = leap.generate(pump, data) +-- repeat +-- response = sequence.next() +-- until last(response) +-- (last() means whatever test the caller wants to perform on response) +-- sequence.done() +-- +-- See request() remarks about ["reqid"]. +-- +-- Note: this seems like a prime use case for Lua coroutines. But in a script +-- using fibers.lua, a "wild" coroutine confuses the fiber scheduler. If +-- generate() were itself a coroutine, it would call WaitForReqid:wait(), +-- which would yield -- thereby resuming generate() WITHOUT waiting. +function leap.generate(pump, data, checklast) + -- Invent a new, unique reqid. Arrange to handle incoming events + -- bearing that reqid. Stamp the outbound request with that reqid, and + -- send it. + local reqid, waitfor = requestSetup(pump, data) + return { + next = function() + dbg('leap.generate(%s).next() about to wait on %s', reqid, tostring(waitfor)) + local ok, response = pcall(waitfor.wait, waitfor) + dbg('leap.generate(%s).next() got %s: %s', reqid, ok, response) + if not ok then + error(response) + elseif response.error then + error(response.error) + else + return response + end + end, + done = function() + -- cleanup consists of removing our WaitForReqid from pending + pending[reqid] = nil + end + } +end + +-- Send the specified request LLSD, expecting an immediate reply followed by +-- an arbitrary number of subsequent replies with the same reqid. Block the +-- calling coroutine until the first (immediate) reply, but launch a separate +-- fiber on which to call the passed callback with later replies. +-- +-- Once the callback returns true, the background fiber terminates. +function leap.eventstream(pump, data, callback) + local reqid, waitfor = requestSetup(pump, data) + local response = waitfor:wait() + if response.error then + -- clean up our WaitForReqid + waitfor:close() + error(response.error) + end + -- No error, so far so good: + -- call the callback with the first response just in case + dbg('leap.eventstream(%s): first callback', reqid) + local ok, done = pcall(callback, response) + dbg('leap.eventstream(%s) got %s, %s', reqid, ok, done) + if not ok then + -- clean up our WaitForReqid + waitfor:close() + error(done) + end + if done then + return response + end + -- callback didn't throw an error, and didn't say stop, + -- so set up to handle subsequent events + -- TODO: distinguish "daemon" fibers that can be terminated even if waiting + fiber.launch( + pump, + function () + local ok, done + local nth = 1 + repeat + event = waitfor:wait() + if not event then + -- wait() returns nil once the queue is closed (e.g. cancelreq()) + ok, done = true, true + else + nth += 1 + dbg('leap.eventstream(%s): callback %d', reqid, nth) + ok, done = pcall(callback, event) + dbg('leap.eventstream(%s) got %s, %s', reqid, ok, done) + end + -- not ok means callback threw an error (caught as 'done') + -- done means callback succeeded but wants to stop + until (not ok) or done + -- once we break this loop, clean up our WaitForReqid + waitfor:close() + if not ok then + -- can't reflect the error back to our caller + LL.print_warning(fiber.get_name() .. ': ' .. done) + end + end) + return response +end + +-- we might want to clean up after leap.eventstream() even if the callback has +-- not yet returned true +function leap.cancelreq(reqid) + dbg('cancelreq(%s)', reqid) + local waitfor = pending[reqid] + if waitfor ~= nil then + -- close() removes the pending entry and also closes the queue, + -- breaking the background fiber's wait loop. + dbg('cancelreq(%s) canceling %s', reqid, waitfor.name) + waitfor:close() + end +end + +local function cleanup(message) + -- We're done: clean up all pending coroutines. + -- Iterate over copies of the pending and waitfors tables, since the + -- close() operation modifies the real tables. + for i, waitfor in pairs(table.clone(pending)) do + waitfor:close() + end + for i, waitfor in pairs(table.clone(waitfors)) do + waitfor:close() + end +end + +-- Handle an incoming (pump, data) event with no recognizable ['reqid'] +local function unsolicited(pump, data) + -- we maintain waitfors in descending priority order, so the first waitfor + -- to claim this event is the one with the highest priority + for i, waitfor in pairs(waitfors) do + dbg('unsolicited() checking %s', waitfor.name) + if waitfor:handle(pump, data) then + return + end + end + LL.print_debug(string.format('unsolicited(%s, %s) discarding unclaimed event', + pump, inspect(data))) +end + +-- Route incoming (pump, data) event to the appropriate waiting coroutine. +local function dispatch(pump, data) + local reqid = data['reqid'] + -- if the response has no 'reqid', it's not from request() or generate() + if reqid == nil then +-- dbg('dispatch() found no reqid; calling unsolicited(%s, %s)', pump, data) + return unsolicited(pump, data) + end + -- have reqid; do we have a WaitForReqid? + local waitfor = pending[reqid] + if waitfor == nil then +-- dbg('dispatch() found no WaitForReqid(%s); calling unsolicited(%s, %s)', reqid, pump, data) + return unsolicited(pump, data) + end + -- found the right WaitForReqid object, let it handle the event +-- dbg('dispatch() calling %s.handle(%s, %s)', waitfor.name, pump, data) + waitfor:handle(pump, data) +end + +-- We configure fiber.set_idle() function. fiber.yield() calls the configured +-- idle callback whenever there are waiting fibers but no ready fibers. In +-- our case, that means it's time to fetch another incoming viewer event. +fiber.set_idle(function () + -- If someone has called leap.done(), then tell fiber.yield() to break loop. + if leap._done then + cleanup('done') + return 'done' + end + dbg('leap.idle() calling get_event_next()') + local ok, pump, data = pcall(LL.get_event_next) + dbg('leap.idle() got %s: %s, %s', ok, pump, data) + -- ok false means get_event_next() raised a Lua error, pump is message + if not ok then + cleanup(pump) + error(pump) + end + -- data nil means get_event_next() returned (pump, LLSD()) to indicate done + if not data then + cleanup('end') + return 'end' + end + -- got a real pump, data pair + dispatch(pump, data) + -- return to fiber.yield(): any incoming message might result in one or + -- more fibers becoming ready +end) + +function leap.done() + leap._done = true +end + +-- called by WaitFor.enable() +local function registerWaitFor(waitfor) + table.insert(waitfors, waitfor) + -- keep waitfors sorted in descending order of specified priority + table.sort(waitfors, + function (lhs, rhs) return lhs.priority > rhs.priority end) +end + +-- called by WaitFor.disable() +local function unregisterWaitFor(waitfor) + local i = table.find(waitfors, waitfor) + if i ~= nil then + waitfors[i] = nil + end +end + +-- ****************************************************************************** +-- WaitFor and friends +-- ****************************************************************************** + +-- An unsolicited event is handled by the highest-priority WaitFor subclass +-- object willing to accept it. If no such object is found, the unsolicited +-- event is discarded. +-- +-- * First, instantiate a WaitFor subclass object to register its interest in +-- some incoming event(s). WaitFor instances are self-registering; merely +-- instantiating the object suffices. +-- * Any coroutine may call a given WaitFor object's wait() method. This blocks +-- the calling coroutine until a suitable event arrives. +-- * WaitFor's constructor accepts a float priority. Every incoming event +-- (other than those claimed by request() or generate()) is passed to each +-- extant WaitFor.filter() method in descending priority order. The first +-- such filter() to return nontrivial data claims that event. +-- * At that point, the blocked wait() call on that WaitFor object returns the +-- item returned by filter(). +-- * WaitFor contains a queue. Multiple arriving events claimed by that WaitFor +-- object's filter() method are added to the queue. Naturally, until the +-- queue is empty, calling wait() immediately returns the front entry. +-- +-- It's reasonable to instantiate a WaitFor subclass whose filter() method +-- unconditionally returns the incoming event, and whose priority places it +-- last in the list. This object will enqueue every unsolicited event left +-- unclaimed by other WaitFor subclass objects. +-- +-- It's not strictly necessary to associate a WaitFor object with exactly one +-- coroutine. You might have multiple "worker" coroutines drawing from the same +-- WaitFor object, useful if the work being done per event might itself involve +-- "blocking" operations. Or a given coroutine might sample a number of WaitFor +-- objects in round-robin fashion... etc. etc. Nonetheless, it's +-- straightforward to designate one coroutine for each WaitFor object. + +-- --------------------------------- WaitFor --------------------------------- +leap.WaitFor = { _id=0 } + +function leap.WaitFor.tostring(self) + -- Lua (sub)classes have no name; can't prefix with that + return self.name +end + +function leap.WaitFor:new(priority, name) + local obj = setmetatable({__tostring=leap.WaitFor.tostring}, self) + self.__index = self + + obj.priority = priority + if name then + obj.name = name + else + self._id += 1 + obj.name = 'WaitFor' .. self._id + end + obj._queue = ErrorQueue() + obj._registered = false + -- if no priority, then don't enable() - remember 0 is truthy + if priority then + obj:enable() + end + + return obj +end + +util.classctor(leap.WaitFor) + +-- Re-enable a disable()d WaitFor object. New WaitFor objects are +-- enable()d by default. +function leap.WaitFor:enable() + if not self._registered then + registerWaitFor(self) + self._registered = true + end +end + +-- Disable an enable()d WaitFor object. +function leap.WaitFor:disable() + if self._registered then + unregisterWaitFor(self) + self._registered = false + end +end + +-- Block the calling coroutine until a suitable unsolicited event (one +-- for which filter() returns the event) arrives. +function leap.WaitFor:wait() + dbg('%s about to wait', self.name) + local item = self._queue:Dequeue() + dbg('%s got %s', self.name, item) + return item +end + +-- Override filter() to examine the incoming event in whatever way +-- makes sense. +-- +-- Return nil to ignore this event. +-- +-- To claim the event, return the item you want placed in the queue. +-- Typically you'd write: +-- return data +-- or perhaps +-- return {pump=pump, data=data} +-- or some variation. +function leap.WaitFor:filter(pump, data) + error('You must override the WaitFor.filter() method') +end + +-- called by unsolicited() for each WaitFor in waitfors +function leap.WaitFor:handle(pump, data) + local item = self:filter(pump, data) + dbg('%s.filter() returned %s', self.name, item) + -- if this item doesn't pass the filter, we're not interested + if not item then + return false + end + -- okay, filter() claims this event + self:process(item) + return true +end + +-- called by WaitFor:handle() for an accepted event +function leap.WaitFor:process(item) + self._queue:Enqueue(item) +end + +-- called by cleanup() at end +function leap.WaitFor:close() + self:disable() + self._queue:close() +end + +-- called by leap.process() when get_event_next() raises an error +function leap.WaitFor:exception(message) + LL.print_warning(self.name .. ' error: ' .. message) + self._queue:Error(message) +end + +-- ------------------------------ WaitForReqid ------------------------------- +leap.WaitForReqid = leap.WaitFor() + +function leap.WaitForReqid:new(reqid) + -- priority is meaningless, since this object won't be added to the + -- priority-sorted waitfors list. Use the reqid as the debugging name + -- string. + local obj = leap.WaitFor(nil, 'WaitForReqid(' .. reqid .. ')') + setmetatable(obj, self) + self.__index = self + + obj.reqid = reqid + + return obj +end + +util.classctor(leap.WaitForReqid) + +function leap.WaitForReqid:filter(pump, data) + -- Because we expect to directly look up the WaitForReqid object of + -- interest based on the incoming ["reqid"] value, it's not necessary + -- to test the event again. Accept every such event. + return data +end + +function leap.WaitForReqid:close() + -- remove this entry from pending table + pending[self.reqid] = nil + self._queue:close() +end + +return leap diff --git a/indra/newview/scripts/lua/require/login.lua b/indra/newview/scripts/lua/require/login.lua new file mode 100644 index 0000000000..919434f3a5 --- /dev/null +++ b/indra/newview/scripts/lua/require/login.lua @@ -0,0 +1,33 @@ +local leap = require 'leap' +local startup = require 'startup' +local mapargs = require 'mapargs' + +local login = {} + +local function ensure_login_state(op) + -- no point trying to login until the viewer is ready + startup.wait('STATE_LOGIN_WAIT') + -- Once we've actually started login, LLPanelLogin is destroyed, and so is + -- its "LLPanelLogin" listener. At that point, + -- leap.request("LLPanelLogin", ...) will hang indefinitely because no one + -- is listening on that LLEventPump any more. Intercept that case and + -- produce a sensible error. + local state = startup.state() + if startup.before('STATE_LOGIN_WAIT', state) then + error(`Can't engage login operation {op} once we've reached state {state}`, 2) + end +end + +function login.login(...) + ensure_login_state('login') + local args = mapargs('username,grid,slurl', ...) + args.op = 'login' + return leap.request('LLPanelLogin', args) +end + +function login.savedLogins(grid) + ensure_login_state('savedLogins') + return leap.request('LLPanelLogin', {op='savedLogins', grid=grid})['logins'] +end + +return login diff --git a/indra/newview/scripts/lua/require/logout.lua b/indra/newview/scripts/lua/require/logout.lua new file mode 100644 index 0000000000..63dcd7f01f --- /dev/null +++ b/indra/newview/scripts/lua/require/logout.lua @@ -0,0 +1,7 @@ +local leap = require 'leap' + +local function logout() + leap.send('LLAppViewer', {op='userQuit'}); +end + +return logout diff --git a/indra/newview/scripts/lua/require/mapargs.lua b/indra/newview/scripts/lua/require/mapargs.lua new file mode 100644 index 0000000000..45f5a9c556 --- /dev/null +++ b/indra/newview/scripts/lua/require/mapargs.lua @@ -0,0 +1,73 @@ +-- Allow a calling function to be passed a mix of positional arguments with +-- keyword arguments. Reference them as fields of a table. +-- Don't use this for a function that can accept a single table argument. +-- mapargs() assumes that a single table argument means its caller was called +-- with f{table constructor} syntax, and maps that table to the specified names. +-- Usage: +-- function f(...) +-- local a = mapargs({'a1', 'a2', 'a3'}, ...) +-- ... a.a1 ... etc. +-- end +-- f(10, 20, 30) -- a.a1 == 10, a.a2 == 20, a.a3 == 30 +-- f{10, 20, 30} -- a.a1 == 10, a.a2 == 20, a.a3 == 30 +-- f{a3=300, a1=100} -- a.a1 == 100, a.a2 == nil, a.a3 == 300 +-- f{1, a3=3} -- a.a1 == 1, a.a2 == nil, a.a3 == 3 +-- f{a3=3, 1} -- a.a1 == 1, a.a2 == nil, a.a3 == 3 +local function mapargs(names, ...) + local args = table.pack(...) + local posargs = {} + local keyargs = {} + -- For a mixed table, no Lua operation will reliably tell you how many + -- array items it contains, if there are any holes. Track that by hand. + -- We must be able to handle f(1, nil, 3) calls. + local maxpos = 0 + + -- For convenience, allow passing 'names' as a string 'n0,n1,...' + if type(names) == 'string' then + names = string.split(names, ',') + end + + if not (args.n == 1 and type(args[1]) == 'table') then + -- If caller passes more than one argument, or if the first argument + -- is not a table, then it's classic positional function-call syntax: + -- f(first, second, etc.). In that case we need not bother teasing + -- apart positional from keyword arguments. + posargs = args + maxpos = args.n + else + -- Single table argument implies f{mixed} syntax. + -- Tease apart positional arguments from keyword arguments. + for k, v in pairs(args[1]) do + if type(k) == 'number' then + posargs[k] = v + maxpos = math.max(maxpos, k) + else + if table.find(names, k) == nil then + error('unknown keyword argument ' .. tostring(k)) + end + keyargs[k] = v + end + end + end + + -- keyargs already has keyword arguments in place, just fill in positionals + args = keyargs + -- Don't exceed the number of parameter names. Loop explicitly over every + -- index value instead of using ipairs() so we can support holes (nils) in + -- posargs. + for i = 1, math.min(#names, maxpos) do + if posargs[i] ~= nil then + -- As in Python, make it illegal to pass an argument both positionally + -- and by keyword. This implementation permits func(17, first=nil), a + -- corner case about which I don't particularly care. + if args[names[i]] ~= nil then + error(string.format('parameter %s passed both positionally and by keyword', + tostring(names[i]))) + end + args[names[i]] = posargs[i] + end + end + return args +end + +return mapargs diff --git a/indra/newview/scripts/lua/require/printf.lua b/indra/newview/scripts/lua/require/printf.lua new file mode 100644 index 0000000000..e84b2024df --- /dev/null +++ b/indra/newview/scripts/lua/require/printf.lua @@ -0,0 +1,19 @@ +-- printf(...) is short for print(string.format(...)) + +local inspect = require 'inspect' + +local function printf(format, ...) + -- string.format() only handles numbers and strings. + -- Convert anything else to string using the inspect module. + local args = {} + for _, arg in pairs(table.pack(...)) do + if type(arg) == 'number' or type(arg) == 'string' then + table.insert(args, arg) + else + table.insert(args, inspect(arg)) + end + end + print(string.format(format, table.unpack(args))) +end + +return printf diff --git a/indra/newview/scripts/lua/require/result_view.lua b/indra/newview/scripts/lua/require/result_view.lua new file mode 100644 index 0000000000..5301d7838c --- /dev/null +++ b/indra/newview/scripts/lua/require/result_view.lua @@ -0,0 +1,98 @@ +local leap = require 'leap' + +-- metatable for every result_view() table +local mt = { + __len = function(self) + return self.length + end, + __index = function(self, i) + -- right away, convert to 0-relative indexing + i -= 1 + -- can we find this index within the current slice? + local reli = i - self.start + if 0 <= reli and reli < #self.slice then + -- Lua 1-relative indexing + return self.slice[reli + 1] + end + -- is this index outside the overall result set? + if not (0 <= i and i < self.length) then + return nil + end + -- fetch a new slice starting at i, using provided fetch() + local start + self.slice, start = self.fetch(self.key, i) + -- It's possible that caller-provided fetch() function forgot + -- to return the adjusted start index of the new slice. In + -- Lua, 0 tests as true, so if fetch() returned (slice, 0), + -- we'll duly reset self.start to 0. Otherwise, assume the + -- requested index was not adjusted: that the returned slice + -- really does start at i. + self.start = start or i + -- Hopefully this slice contains the desired i. + -- Back to 1-relative indexing. + return self.slice[i - self.start + 1] + end, + -- We purposely avoid putting any array entries (int keys) into + -- our table so that access to any int key will always call our + -- __index() metamethod. Moreover, we want any table iteration to + -- call __index(table, i) however many times; we do NOT want it to + -- retrieve key, length, start, slice. + -- So turn 'for k, v in result' into 'for k, v in ipairs(result)'. + __iter = ipairs, + -- This result set provides read-only access. + -- We do not support pushing updates to individual items back to + -- C++; for the intended use cases, that makes no sense. + __newindex = function(self, i, value) + error("result_view is a read-only data structure", 2) + end +} + +-- result_view(key_length, fetch) returns a table which stores only a slice +-- of a result set plus some control values, yet presents read-only virtual +-- access to the entire result set. +-- key_length: {result set key, total result set length} +-- fetch: function(key, start) that returns (slice, adjusted start) +local result_view = setmetatable( + { + -- generic fetch() function + fetch = function(key, start) + local fetched = leap.request( + 'LLInventory', + {op='getSlice', result=key, index=start}) + return fetched.slice, fetched.start + end, + -- generic close() function accepting variadic result-set keys + close = function(...) + local keys = table.pack(...) + -- table.pack() produces a table with an array entry for every + -- parameter, PLUS an 'n' key with the count. Unfortunately that + -- 'n' key bollixes our conversion to LLSD, which requires either + -- all int keys (for an array) or all string keys (for a map). + keys.n = nil + leap.send('LLInventory', {op='closeResult', result=keys}) + end + }, + { + -- result_view(key_length, fetch) calls this + __call = function(class, key_length, fetch) + return setmetatable( + { + key=key_length[1], + length=key_length[2], + -- C++ result sets use 0-based indexing, so internally we do too + start=0, + -- start with a dummy array with length 0 + slice={}, + -- if caller didn't pass fetch() function, use generic + fetch=fetch or class.fetch, + -- returned view:close() will close result set with passed key + close=function(self) class.close(key_length[1]) end + }, + -- use our special metatable + mt + ) + end + } +) + +return result_view diff --git a/indra/newview/scripts/lua/require/startup.lua b/indra/newview/scripts/lua/require/startup.lua new file mode 100644 index 0000000000..c3040f94b8 --- /dev/null +++ b/indra/newview/scripts/lua/require/startup.lua @@ -0,0 +1,100 @@ +-- query, wait for or mandate a particular viewer startup state + +-- During startup, the viewer steps through a sequence of numbered (and named) +-- states. This can be used to detect when, for instance, the login screen is +-- displayed, or when the viewer has finished logging in and is fully +-- in-world. + +local fiber = require 'fiber' +local leap = require 'leap' +local inspect = require 'inspect' +local function dbg(...) end +-- local dbg = require 'printf' + +-- --------------------------------------------------------------------------- +local startup = {} + +-- Get the list of startup states from the viewer. +local bynum = leap.request('LLStartUp', {op='getStateTable'})['table'] + +local byname = setmetatable( + {}, + -- set metatable to throw an error if you look up invalid state name + {__index=function(t, k) + local v = rawget(t, k) + if v then + return v + end + error(string.format('startup module passed invalid state %q', k), 2) + end}) + +-- derive byname as a lookup table to find the 0-based index for a given name +for i, name in pairs(bynum) do + -- the viewer's states are 0-based, not 1-based like Lua indexes + byname[name] = i - 1 +end +-- dbg('startup states: %s', inspect(byname)) + +-- specialize a WaitFor to track the viewer's startup state +local startup_pump = 'StartupState' +local waitfor = leap.WaitFor(0, startup_pump) +function waitfor:filter(pump, data) + if pump == self.name then + return data + end +end + +function waitfor:process(data) + -- keep updating startup._state for interested parties + startup._state = data.str + dbg('startup updating state to %q', data.str) + -- now pass data along to base-class method to queue + leap.WaitFor.process(self, data) +end + +-- listen for StartupState events +leap.request(leap.cmdpump(), + {op='listen', source=startup_pump, listener='startup.lua', tweak=true}) +-- poke LLStartUp to make sure we get an event +leap.send('LLStartUp', {op='postStartupState'}) + +-- --------------------------------------------------------------------------- +-- wait for response from postStartupState +while not startup._state do + dbg('startup.state() waiting for first StartupState event') + waitfor:wait() +end + +-- return a list of all known startup states +function startup.list() + return bynum +end + +-- report whether state with string name 'left' is before string name 'right' +function startup.before(left, right) + return byname[left] < byname[right] +end + +-- report the viewer's current startup state +function startup.state() + return startup._state +end + +-- error if script is called before specified state string name +function startup.ensure(state) + if startup.before(startup.state(), state) then + -- tell error() to pretend this error was thrown by our caller + error('must not be called before startup state ' .. state, 2) + end +end + +-- block calling fiber until viewer has reached state with specified string name +function startup.wait(state) + dbg('startup.wait(%q)', state) + while startup.before(startup.state(), state) do + local item = waitfor:wait() + dbg('startup.wait(%q) sees %s', state, item) + end +end + +return startup diff --git a/indra/newview/scripts/lua/require/timers.lua b/indra/newview/scripts/lua/require/timers.lua new file mode 100644 index 0000000000..ab1615ffbf --- /dev/null +++ b/indra/newview/scripts/lua/require/timers.lua @@ -0,0 +1,122 @@ +-- Access to the viewer's time-delay facilities + +local leap = require 'leap' +local util = require 'util' + +local timers = {} + +local function dbg(...) end +-- local dbg = require 'printf' + +timers.Timer = {} + +-- delay: time in seconds until callback +-- callback: 'wait', or function to call when timer fires (self:tick if nil) +-- iterate: if non-nil, call callback repeatedly until it returns non-nil +-- (ignored if 'wait') +function timers.Timer:new(delay, callback, iterate) + local obj = setmetatable({}, self) + self.__index = self + + if callback == 'wait' then + dbg('scheduleAfter(%d):', delay) + sequence = leap.generate('Timers', {op='scheduleAfter', after=delay}) + -- ignore the immediate return + dbg('scheduleAfter(%d) -> %s', delay, + sequence.next()) + -- this call is where we wait for real + dbg('next():') + dbg('next() -> %s', + sequence.next()) + sequence.done() + return + end + + callback = callback or function() obj:tick() end + + local calls = 0 + if iterate then + -- With iterative timers, beware of running a timer callback which + -- performs async actions lasting longer than the timer interval. The + -- lengthy callback suspends, allowing leap to retrieve the next + -- event, which is a timer tick. leap calls a new instance of the + -- callback, even though the previous callback call is still + -- suspended... etc. 'in_callback' defends against that recursive + -- case. Rather than re-enter the suspended callback, drop the + -- too-soon timer event. (We could count the too-soon timer events and + -- iterate calling the callback, but it's a bathtub problem: the + -- callback could end up getting farther and farther behind.) + local in_callback = false + obj.id = leap.eventstream( + 'Timers', + {op='scheduleEvery', every=delay}, + function (event) + local reqid = event.reqid + calls += 1 + if calls == 1 then + dbg('timer(%s) first callback', reqid) + -- discard the first (immediate) response: don't call callback + return nil + else + if in_callback then + dbg('dropping timer(%s) callback %d', reqid, calls) + else + dbg('timer(%s) callback %d', reqid, calls) + in_callback = true + local ret = callback(event) + in_callback = false + return ret + end + end + end + ).reqid + else -- (not iterate) + obj.id = leap.eventstream( + 'Timers', + {op='scheduleAfter', after=delay}, + function (event) + calls += 1 + -- Arrange to return nil the first time, true the second. This + -- callback is called immediately with the response to + -- 'scheduleAfter', and if we immediately returned true, we'd + -- be done, and the subsequent timer event would be discarded. + if calls == 1 then + -- Caller doesn't expect an immediate callback. + return nil + else + callback(event) + -- Since caller doesn't want to iterate, the value + -- returned by the callback is irrelevant: just stop after + -- this one and only call. + return true + end + end + ).reqid + end + + return obj +end + +util.classctor(timers.Timer) + +function timers.Timer:tick() + error('Pass a callback to Timer:new(), or override Timer:tick()') +end + +function timers.Timer:cancel() + local ok = leap.request('Timers', {op='cancel', id=self.id}).ok + leap.cancelreq(self.id) + return ok +end + +function timers.Timer:isRunning() + return leap.request('Timers', {op='isRunning', id=self.id}).running +end + +-- returns (true, seconds left) for a live timer, else (false, 0) +function timers.Timer:timeUntilCall() + local result = leap.request('Timers', {op='timeUntilCall', id=self.id}) + return result.ok, result.remaining +end + +return timers diff --git a/indra/newview/scripts/lua/require/util.lua b/indra/newview/scripts/lua/require/util.lua new file mode 100644 index 0000000000..40737a159a --- /dev/null +++ b/indra/newview/scripts/lua/require/util.lua @@ -0,0 +1,114 @@ +-- utility functions, in alpha order + +local util = {} + +-- Allow MyClass(ctor args...) equivalent to MyClass:new(ctor args...) +-- Usage: +-- local MyClass = {} +-- function MyClass:new(...) +-- ... +-- end +-- ... +-- util.classctor(MyClass) +-- or if your constructor is named something other than MyClass:new(), e.g. +-- MyClass:construct(): +-- util.classctor(MyClass, MyClass.construct) +-- return MyClass +function util.classctor(class, ctor) + -- set class's __call metamethod to the specified constructor function + -- (class.new if not specified) + util.setmetamethods{class, __call=(ctor or class.new)} +end + +-- check if array-like table contains certain value +function util.contains(t, v) + return table.find(t, v) ~= nil +end + +-- reliable count of the number of entries in table t +-- (since #t is unreliable) +function util.count(t) + local count = 0 + for _ in pairs(t) do + count += 1 + end + return count +end + +-- cheap test whether table t is empty +function util.empty(t) + return not next(t) +end + +-- recursive table equality +function util.equal(t1, t2) + if not (type(t1) == 'table' and type(t2) == 'table') then + return t1 == t2 + end + -- both t1 and t2 are tables: get modifiable copy of t2 + local temp = table.clone(t2) + for k, v in pairs(t1) do + -- if any key in t1 doesn't have same value in t2, not equal + if not util.equal(v, temp[k]) then + return false + end + -- temp[k] == t1[k], delete temp[k] + temp[k] = nil + end + -- All keys in t1 have equal values in t2; t2 == t1 if there are no extra keys in t2 + return util.empty(temp) +end + +-- Find or create the metatable for a specified table (a new empty table if +-- omitted), and to that metatable assign the specified keys. +-- Setting multiple keys at once is more efficient than a function to set only +-- one at a time, e.g. setametamethod(). +-- t = util.setmetamethods{__index=readfunc, __len=lenfunc} +-- returns a new table with specified metamethods __index, __len +-- util.setmetamethods{t, __call=action} +-- finds or creates the metatable for existing table t and sets __call +-- util.setmetamethods{table=t, __call=action} +-- same as util.setmetamethods{t, __call=action} +function util.setmetamethods(specs) + -- first determine the target table + assert(not (specs.table and specs[1]), + "Pass setmetamethods table either as positional or table=, not both") + local t = specs.table or specs[1] or {} + -- remove both ways of specifying table, leaving only the metamethods + specs.table = nil + specs[1] = nil + local mt = getmetatable(t) + if not mt then + -- t doesn't already have a metatable: just set specs + setmetatable(t, specs) + else + -- t already has a metatable: copy specs into it + local key, value + for key, value in pairs(specs) do + mt[key] = value + end + end + -- having set or enriched t's metatable, return t + return t +end + +-- On the passed module (i.e. table), set an __index metamethod such that +-- referencing module.submodule lazily requires(path/submodule). +-- The loaded submodule is cached in the module table so it need not be passed +-- to require() again. +-- 'path', like any require() string, can be relative to LuaRequirePath. +-- Returns the enriched module, permitting e.g. +-- mymod = util.submoduledir({}, 'mymod') +function util.submoduledir(module, path) + return util.setmetamethods{ + module, + __index=function(t, key) + local mod = require(`{path}/{key}`) + -- cache the submodule + t[key] = mod + return mod + end + } +end + +return util diff --git a/indra/newview/scripts/lua/test_LLAppearance.lua b/indra/newview/scripts/lua/test_LLAppearance.lua new file mode 100644 index 0000000000..a97ec4e0ca --- /dev/null +++ b/indra/newview/scripts/lua/test_LLAppearance.lua @@ -0,0 +1,114 @@ +local LLAppearance = require 'LLAppearance' +local startup = require 'startup' +local inspect = require 'inspect' +local UI = require 'UI' + +local SHOW_OUTFITS = true +local SELECTED_OUTFIT_ID = {} +local DATA_MAP = {} + +local wearables_lbl = 'Show wearables' +local outfits_lbl = 'Show outfits' +local replace_cof_lbl = 'Replace COF' +local add_cof_lbl = 'Add to COF' +local outfits_title = 'Outfits' +local wear_lbl = 'Wear item' +local detach_lbl = 'Detach item' + +local flt = UI.Floater( + "luafloater_outfits_list.xml", + {outfits_list = {"double_click"}}) + +function get_selected_id() + return flt:request({action="get_selected_id", ctrl_name='outfits_list'}).value +end + +function populate_list() + if SHOW_OUTFITS then + DATA_MAP = LLAppearance.getOutfitsList() + else + DATA_MAP = LLAppearance.getOutfitItems(SELECTED_OUTFIT_ID) + end + + local action_data = {} + action_data.action = "add_list_element" + action_data.ctrl_name = "outfits_list" + local outfits = {} + for uuid, info in pairs(DATA_MAP) do + name = {} + if SHOW_OUTFITS then + name = info + else + name = info.name + end + table.insert(outfits, {value = uuid, columns={column = "outfit_name", value = name}}) + end + action_data.value = outfits + flt:post(action_data) +end + +function set_label(btn_name, value) + flt:post({action="set_label", ctrl_name=btn_name, value=value}) +end + +function set_enabled(btn_name, value) + flt:post({action="set_enabled", ctrl_name=btn_name, value=value}) +end + +function update_labels() + if SHOW_OUTFITS then + set_label('select_btn', wearables_lbl) + set_label('replace_btn', replace_cof_lbl) + set_label('add_btn', add_cof_lbl) + + set_enabled('select_btn', false) + flt:post({action="set_title", value=outfits_title}) + else + set_label('select_btn', outfits_lbl) + set_label('replace_btn', wear_lbl) + set_label('add_btn', detach_lbl) + + set_enabled('select_btn', true) + flt:post({action="set_title", value=DATA_MAP[SELECTED_OUTFIT_ID]}) + end + + set_enabled('replace_btn', false) + set_enabled('add_btn', false) +end + +function flt:post_build(event_data) + populate_list() +end + +function flt:commit_replace_btn(event_data) + if SHOW_OUTFITS then + LLAppearance.wearOutfit(get_selected_id(), 'replace') + else + LLAppearance.wearItems(get_selected_id(), false) + end +end + +function flt:commit_add_btn(event_data) + if SHOW_OUTFITS then + LLAppearance.wearOutfit(get_selected_id(), 'add') + else + LLAppearance.detachItems(get_selected_id()) + end +end + +function flt:commit_select_btn(event_data) + SHOW_OUTFITS = not SHOW_OUTFITS + SELECTED_OUTFIT_ID = get_selected_id() + update_labels() + self:post({action="clear_list", ctrl_name='outfits_list'}) + populate_list() +end + +function flt:commit_outfits_list(event_data) + set_enabled('replace_btn', true) + set_enabled('add_btn', true) + set_enabled('select_btn', true) +end + +startup.wait('STATE_STARTED') +flt:show() diff --git a/indra/newview/scripts/lua/test_LLChat.lua b/indra/newview/scripts/lua/test_LLChat.lua new file mode 100644 index 0000000000..3abaf28e42 --- /dev/null +++ b/indra/newview/scripts/lua/test_LLChat.lua @@ -0,0 +1,18 @@ +LLChat = require 'LLChat' + +function generateRandomWord(length) + local alphabet = "abcdefghijklmnopqrstuvwxyz" + local wordTable = {} + for i = 1, length do + local randomIndex = math.random(1, #alphabet) + table.insert(wordTable, alphabet:sub(randomIndex, randomIndex)) + end + return table.concat(wordTable) +end + +local msg = {'AI says:'} +math.randomseed(os.time()) +for i = 1, math.random(1, 10) do + table.insert(msg, generateRandomWord(math.random(1, 8))) +end +LLChat.sendNearby(table.concat(msg, ' ')) diff --git a/indra/newview/scripts/lua/test_LLChatListener.lua b/indra/newview/scripts/lua/test_LLChatListener.lua new file mode 100644 index 0000000000..4a4d40bee5 --- /dev/null +++ b/indra/newview/scripts/lua/test_LLChatListener.lua @@ -0,0 +1,39 @@ +local LLChatListener = require 'LLChatListener' +local LLChat = require 'LLChat' +local UI = require 'UI' + +-- Chat listener script allows to use the following commands in Nearby chat: +-- open inventory -- open defined floater by name +-- close inventory -- close defined floater by name +-- closeall -- close all floaters +-- stop -- close the script +-- any other messages will be echoed. +function openOrEcho(message) + local open_floater_name = string.match(message, "^open%s+(%w+)") + local close_floater_name = string.match(message, "^close%s+(%w+)") + if open_floater_name then + UI.showFloater(open_floater_name) + elseif close_floater_name then + UI.hideFloater(close_floater_name) + elseif message == 'closeall' then + UI.closeAllFloaters() + else + LLChat.sendNearby('Echo: ' .. message) + end +end + +local listener = LLChatListener() + +function listener:handleMessages(event_data) + if string.find(event_data.message, '[LUA]') then + return true + elseif event_data.message == 'stop' then + LLChat.sendNearby('Closing echo script.') + return false + else + openOrEcho(event_data.message) + end + return true +end + +listener:start() diff --git a/indra/newview/scripts/lua/test_LLFloaterAbout.lua b/indra/newview/scripts/lua/test_LLFloaterAbout.lua new file mode 100644 index 0000000000..6bbf61982d --- /dev/null +++ b/indra/newview/scripts/lua/test_LLFloaterAbout.lua @@ -0,0 +1,6 @@ +-- test LLFloaterAbout + +LLFloaterAbout = require('LLFloaterAbout') +inspect = require('inspect') + +print(inspect(LLFloaterAbout.getInfo())) diff --git a/indra/newview/scripts/lua/test_LLGesture.lua b/indra/newview/scripts/lua/test_LLGesture.lua new file mode 100644 index 0000000000..1cce674565 --- /dev/null +++ b/indra/newview/scripts/lua/test_LLGesture.lua @@ -0,0 +1,26 @@ +-- exercise LLGesture API + +LLGesture = require 'LLGesture' +inspect = require 'inspect' + + +-- getActiveGestures() returns {<UUID>: {name, playing, trigger}} +gestures_uuid = LLGesture.getActiveGestures() +-- convert to {<name>: <uuid>} +gestures = {} +for uuid, info in pairs(gestures_uuid) do + gestures[info.name] = uuid +end +-- now run through the list +for name, uuid in pairs(gestures) do + if name == 'afk' then + -- afk has a long timeout, and isn't interesting to look at + continue + end + print(name) + LLGesture.startGesture(uuid) + repeat + LL.sleep(1) + until not LLGesture.isGesturePlaying(uuid) +end +print('Done.') diff --git a/indra/newview/scripts/lua/test_LLInventory.lua b/indra/newview/scripts/lua/test_LLInventory.lua new file mode 100644 index 0000000000..de57484bcd --- /dev/null +++ b/indra/newview/scripts/lua/test_LLInventory.lua @@ -0,0 +1,24 @@ +inspect = require 'inspect' +LLInventory = require 'LLInventory' + +-- Get 'My Landmarks' folder id (you can see all folder types via LLInventory.getFolderTypeNames()) +my_landmarks_id = LLInventory.getBasicFolderID('landmark') +-- Get 3 landmarks from the 'My Landmarks' folder (you can see all folder types via LLInventory.getAssetTypeNames()) +landmarks = LLInventory.collectDescendentsIf{folder_id=my_landmarks_id, type="landmark", limit=3} +for _, landmark in pairs(landmarks.items) do + print(landmark.name) +end + +-- Get 'Calling Cards' folder id +calling_cards_id = LLInventory.getBasicFolderID('callcard') +-- Get all items located directly in 'Calling Cards' folder +calling_cards = LLInventory.getDirectDescendents(calling_cards_id).items + +-- Print a random calling card name from 'Calling Cards' folder +-- (because getDirectDescendents().items is a Lua result set, selecting +-- a random entry only fetches one slice containing that entry) +math.randomseed(os.time()) +for i = 1, 5 do + pick = math.random(#calling_cards) + print(`Random calling card (#{pick} of {#calling_cards}): {calling_cards[pick].name}`) +end diff --git a/indra/newview/scripts/lua/test_animation.lua b/indra/newview/scripts/lua/test_animation.lua new file mode 100644 index 0000000000..c16fef4918 --- /dev/null +++ b/indra/newview/scripts/lua/test_animation.lua @@ -0,0 +1,28 @@ +LLInventory = require 'LLInventory' +LLAgent = require 'LLAgent' + +-- Get 'Animations' folder id (you can see all folder types via LLInventory.getFolderTypeNames()) +animations_id = LLInventory.getBasicFolderID('animatn') +-- Get animations from the 'Animation' folder (you can see all folder types via LLInventory.getAssetTypeNames()) +anims = LLInventory.collectDescendentsIf{folder_id=animations_id, type="animatn"}.items + +local anim_ids = {} +for key in pairs(anims) do + table.insert(anim_ids, key) +end + +-- Start playing a random animation +math.randomseed(os.time()) +local random_id = anim_ids[math.random(#anim_ids)] +local anim_info = LLAgent.getAnimationInfo(random_id) + +print("Starting animation locally: " .. anims[random_id].name) +print("Loop: " .. anim_info.is_loop .. " Joints: " .. anim_info.num_joints .. " Duration " .. tonumber(string.format("%.2f", anim_info.duration))) +LLAgent.playAnimation{item_id=random_id} + +-- Stop animation after 3 sec if it's looped or longer than 3 sec +if anim_info.is_loop == 1 or anim_info.duration > 3 then + LL.sleep(3) + print("Stop animation.") + LLAgent.stopAnimation(random_id) +end diff --git a/indra/newview/scripts/lua/test_atexit.lua b/indra/newview/scripts/lua/test_atexit.lua new file mode 100644 index 0000000000..6fbc0f3eb1 --- /dev/null +++ b/indra/newview/scripts/lua/test_atexit.lua @@ -0,0 +1,3 @@ +LL.atexit(function() print('Third') end) +LL.atexit(function() print('Second') end) +LL.atexit(function() print('First') end) diff --git a/indra/newview/scripts/lua/test_callables.lua b/indra/newview/scripts/lua/test_callables.lua new file mode 100644 index 0000000000..1bee062db8 --- /dev/null +++ b/indra/newview/scripts/lua/test_callables.lua @@ -0,0 +1,6 @@ +startup=require 'startup' +UI=require 'UI' +startup.wait('STATE_LOGIN_WAIT') +for _, cbl in pairs(UI.callables()) do + print(`{cbl.name} ({cbl.access})`) +end diff --git a/indra/newview/scripts/lua/test_camera_control.lua b/indra/newview/scripts/lua/test_camera_control.lua new file mode 100644 index 0000000000..7ac0986ee6 --- /dev/null +++ b/indra/newview/scripts/lua/test_camera_control.lua @@ -0,0 +1,49 @@ +local LLAgent = require 'LLAgent' +local startup = require 'startup' +local UI = require 'UI' + +local flt = UI.Floater('luafloater_camera_control.xml') + +function getValue(ctrl_name) + return flt:request({action="get_value", ctrl_name=ctrl_name}).value +end + +function setValue(ctrl_name, value) + flt:post({action="set_value", ctrl_name=ctrl_name, value=value}) +end + +function flt:commit_update_btn(event_data) + lock_focus = getValue('lock_focus_ctrl') + lock_camera = getValue('lock_camera_ctrl') + local camera_pos = {getValue('cam_x'), getValue('cam_y'), getValue('cam_z')} + local focus_pos = {getValue('focus_x'), getValue('focus_y'), getValue('focus_z')} + + LLAgent.setCamera{camera_pos=camera_pos, focus_pos=focus_pos, + focus_locked=lock_focus, camera_locked=lock_camera} + + self:post({action="add_text", ctrl_name="events_editor", + value = {'Updating FollowCam params', 'camera_pos:', camera_pos, 'focus_pos:', focus_pos, + 'lock_focus:', lock_focus, 'lock_camera:', lock_camera}}) +end + +function flt:commit_agent_cam_btn(event_data) + agent_pos = LLAgent.getRegionPosition() + setValue('cam_x', math.floor(agent_pos[1])) + setValue('cam_y', math.floor(agent_pos[2])) + setValue('cam_z', math.floor(agent_pos[3])) +end + +function flt:commit_agent_focus_btn(event_data) + agent_pos = LLAgent.getRegionPosition() + setValue('focus_x', math.floor(agent_pos[1])) + setValue('focus_y', math.floor(agent_pos[2])) + setValue('focus_z', math.floor(agent_pos[3])) +end + +function flt:commit_reset_btn(event_data) + LLAgent.removeCamParams() + LLAgent.setFollowCamActive(false) +end + +startup.wait('STATE_STARTED') +flt:show() diff --git a/indra/newview/scripts/lua/test_flycam.lua b/indra/newview/scripts/lua/test_flycam.lua new file mode 100644 index 0000000000..05c3c37b93 --- /dev/null +++ b/indra/newview/scripts/lua/test_flycam.lua @@ -0,0 +1,38 @@ +-- Make camera fly around the subject avatar for a few seconds. + +local LLAgent = require 'LLAgent' +local startup = require 'startup' +local timers = require 'timers' + +local height = 2.0 -- meters +local radius = 4.0 -- meters +local speed = 1.0 -- meters/second along circle +local start = os.clock() +local stop = os.clock() + 30 -- seconds + +local function cameraPos(t) + local agent = LLAgent.getRegionPosition() + local radians = speed * t + return { + agent[1] + radius * math.cos(radians), + agent[2] + radius * math.sin(radians), + agent[3] + height + } +end + +local function moveCamera() + if os.clock() < stop then + -- usual case + LLAgent.setCamera{ camera_pos=cameraPos(os.clock() - start), camera_locked=true } + return nil + else + -- last time + LLAgent.removeCamParams() + LLAgent.setFollowCamActive(false) + return true + end +end + +startup.wait('STATE_STARTED') +-- call moveCamera() repeatedly until it returns true +local timer = timers.Timer(0.1, moveCamera, true) diff --git a/indra/newview/scripts/lua/test_group_chat.lua b/indra/newview/scripts/lua/test_group_chat.lua new file mode 100644 index 0000000000..373411c26c --- /dev/null +++ b/indra/newview/scripts/lua/test_group_chat.lua @@ -0,0 +1,14 @@ +LLChat = require 'LLChat' +LLAgent = require 'LLAgent' +UI = require 'UI' + +local GROUPS = LLAgent.getGroups() + +-- Choose one of the groups randomly and send group message +math.randomseed(os.time()) +group_info = GROUPS[math.random(#GROUPS)] +LLChat.startGroupChat(group_info.id) +response = UI.popup:alertYesCancel('Started group chat with ' .. group_info.name .. ' group. Send greetings?') +if response == 'OK' then + LLChat.sendGroupIM('Greetings', group_info.id) +end diff --git a/indra/newview/scripts/lua/test_inv_resultset.lua b/indra/newview/scripts/lua/test_inv_resultset.lua new file mode 100644 index 0000000000..c31cfe3c67 --- /dev/null +++ b/indra/newview/scripts/lua/test_inv_resultset.lua @@ -0,0 +1,18 @@ +local LLInventory = require 'LLInventory' +local inspect = require 'inspect' + +print('basic folders:') +print(inspect(LLInventory.getFolderTypeNames())) + +local folder = LLInventory.getBasicFolderID('my_otfts') +print(`folder = {folder}`) +local result = LLInventory.getDirectDescendants(folder) +print(`type(result) = {type(result)}`) +print(#result.categories, 'categories:') +for i, cat in pairs(result.categories) do + print(`{i}: {cat.name}`) +end +print(#result.items, 'items') +for i, item in pairs(result.items) do + print(`{i}: {item.name}`) +end diff --git a/indra/newview/scripts/lua/test_login.lua b/indra/newview/scripts/lua/test_login.lua new file mode 100644 index 0000000000..54d3635a41 --- /dev/null +++ b/indra/newview/scripts/lua/test_login.lua @@ -0,0 +1,10 @@ +inspect = require 'inspect' +login = require 'login' + +local grid = 'agni' +print(inspect(login.savedLogins(grid))) + +print(inspect(login.login{ + username='Your Username', + grid=grid + })) diff --git a/indra/newview/scripts/lua/test_logout.lua b/indra/newview/scripts/lua/test_logout.lua new file mode 100644 index 0000000000..b1ac59e38c --- /dev/null +++ b/indra/newview/scripts/lua/test_logout.lua @@ -0,0 +1,3 @@ +logout = require 'logout' + +logout() diff --git a/indra/newview/scripts/lua/test_luafloater_demo.lua b/indra/newview/scripts/lua/test_luafloater_demo.lua new file mode 100644 index 0000000000..2158134511 --- /dev/null +++ b/indra/newview/scripts/lua/test_luafloater_demo.lua @@ -0,0 +1,39 @@ +local leap = require 'leap' +local startup = require 'startup' +local UI = require 'UI' + +local flt = UI.Floater( + 'luafloater_demo.xml', + {show_time_lbl = {"right_mouse_down", "double_click"}}) + +-- override base-class handleEvents() to report the event data in the floater's display field +function flt:handleEvents(event_data) + self:post({action="add_text", ctrl_name="events_editor", value = event_data}) + -- forward the call to base-class handleEvents() + return UI.Floater.handleEvents(self, event_data) +end + +function flt:commit_disable_ctrl(event_data) + self:post({action="set_enabled", ctrl_name="open_btn", value = (1 - event_data.value)}) +end + +function flt:commit_title_cmb(event_data) + self:post({action="set_title", value=event_data.value}) +end + +function flt:commit_open_btn(event_data) + floater_name = self:request({action="get_value", ctrl_name='openfloater_cmd'}).value + leap.send("LLFloaterReg", {name = floater_name, op = "showInstance"}) +end + +local function getCurrentTime() + local currentTime = os.date("*t") + return string.format("%02d:%02d:%02d", currentTime.hour, currentTime.min, currentTime.sec) +end + +function flt:double_click_show_time_lbl(event_data) + self:post({action="set_value", ctrl_name="time_lbl", value=getCurrentTime()}) +end + +startup.wait('STATE_LOGIN_WAIT') +flt:show() diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua new file mode 100644 index 0000000000..5f929c0d0c --- /dev/null +++ b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua @@ -0,0 +1,27 @@ +local LLGesture = require 'LLGesture' +local startup = require 'startup' +local UI = require 'UI' + +local flt = UI.Floater( + "luafloater_gesture_list.xml", + {gesture_list = {"double_click"}}) + +function flt:post_build(event_data) + local gestures_uuid = LLGesture.getActiveGestures() + local action_data = {} + action_data.action = "add_list_element" + action_data.ctrl_name = "gesture_list" + local gestures = {} + for uuid, info in pairs(gestures_uuid) do + table.insert(gestures, {value = uuid, columns={column = "gesture_name", value = info.name}}) + end + action_data.value = gestures + self:post(action_data) +end + +function flt:double_click_gesture_list(event_data) + LLGesture.startGesture(event_data.value) +end + +startup.wait('STATE_STARTED') +flt:show() diff --git a/indra/newview/scripts/lua/test_luafloater_speedometer.lua b/indra/newview/scripts/lua/test_luafloater_speedometer.lua new file mode 100644 index 0000000000..2cdd41fd7e --- /dev/null +++ b/indra/newview/scripts/lua/test_luafloater_speedometer.lua @@ -0,0 +1,31 @@ +local leap = require 'leap' +local startup = require 'startup' +local Timer = (require 'timers').Timer +local UI = require 'UI' +local popup = UI.popup +local max_speed = 0 +local flt = UI.Floater("luafloater_speedometer.xml") +startup.wait('STATE_STARTED') + +local timer + +function flt:floater_close(event_data) + if timer then + timer:cancel() + end + popup:tip(string.format("Registered max speed: %.2f m/s", max_speed)) +end + +local function idle(event_data) + local speed = leap.request('LLVOAvatar', {op='getSpeed'})['value'] + flt:post({action="set_value", ctrl_name="speed_lbl", value = string.format("%.2f", speed)}) + max_speed=math.max(max_speed, speed) +end + +msg = 'Are you sure you want to run this "speedometer" script?' +response = popup:alertYesCancel(msg) + +if response == 'OK' then + flt:show() + timer = Timer(1, idle, true) -- iterate +end diff --git a/indra/newview/scripts/lua/test_mapargs.lua b/indra/newview/scripts/lua/test_mapargs.lua new file mode 100644 index 0000000000..999a57acb4 --- /dev/null +++ b/indra/newview/scripts/lua/test_mapargs.lua @@ -0,0 +1,68 @@ +local mapargs = require 'mapargs' +local inspect = require 'inspect' + +function tabfunc(...) + local a = mapargs({'a1', 'a2', 'a3'}, ...) + print(inspect(a)) +end + +print('----------') +print('f(10, 20, 30)') +tabfunc(10, 20, 30) +print('f(10, nil, 30)') +tabfunc(10, nil, 30) +print('f{10, 20, 30}') +tabfunc{10, 20, 30} +print('f{10, nil, 30}') +tabfunc{10, nil, 30} +print('f{a3=300, a1=100}') +tabfunc{a3=300, a1=100} +print('f{1, a3=3}') +tabfunc{1, a3=3} +print('f{a3=3, 1}') +tabfunc{a3=3, 1} +print('----------') + +if false then + -- the code below was used to explore ideas that became mapargs() + mixed = { '[1]', nil, '[3]', abc='[abc]', '[3]', def='[def]' } + local function showtable(desc, t) + print(string.format('%s (len %s)\n%s', desc, #t, inspect(t))) + end + showtable('mixed', mixed) + + print('ipairs(mixed)') + for k, v in ipairs(mixed) do + print(string.format('[%s] = %s', k, tostring(v))) + end + + print('table.pack(mixed)') + print(inspect(table.pack(mixed))) + + local function nilarg(desc, a, b, c) + print(desc) + print('a = ' .. tostring(a)) + print('b = ' .. tostring(b)) + print('c = ' .. tostring(c)) + end + + nilarg('nilarg(1)', 1) + nilarg('nilarg(1, nil, 3)', 1, nil, 3) + + local function nilargs(desc, ...) + args = table.pack(...) + showtable(desc, args) + end + + nilargs('nilargs{a=1, b=2, c=3}', {a=1, b=2, c=3}) + nilargs('nilargs(1, 2, 3)', 1, 2, 3) + nilargs('nilargs(1, nil, 3)', 1, nil, 3) + nilargs('nilargs{1, 2, 3}', {1, 2, 3}) + nilargs('nilargs{1, nil, 3}', {1, nil, 3}) + + print('table.unpack({1, nil, 3})') + a, b, c = table.unpack({1, nil, 3}) + print('a = ' .. tostring(a)) + print('b = ' .. tostring(b)) + print('c = ' .. tostring(c)) +end diff --git a/indra/newview/scripts/lua/test_popup.lua b/indra/newview/scripts/lua/test_popup.lua new file mode 100644 index 0000000000..7a11895669 --- /dev/null +++ b/indra/newview/scripts/lua/test_popup.lua @@ -0,0 +1,10 @@ +UI = require 'UI' +popup = UI.popup +startup = require 'startup' + +startup.wait('STATE_STARTED') + +response = popup:alert('This just has a Close button') +response = popup:alertOK(string.format('You said "%s", is that OK?', response)) +response = popup:alertYesCancel(string.format('You said "%s"', response)) +popup:tip(string.format('You said "%s"', response)) diff --git a/indra/newview/scripts/lua/test_result_view.lua b/indra/newview/scripts/lua/test_result_view.lua new file mode 100644 index 0000000000..304633a472 --- /dev/null +++ b/indra/newview/scripts/lua/test_result_view.lua @@ -0,0 +1,55 @@ +-- Verify the functionality of result_view. +result_view = require 'result_view' + +print('alphabet') +alphabet = "abcdefghijklmnopqrstuvwxyz" +assert(#alphabet == 26) +alphabits = string.split(alphabet, '') + +print('function slice()') +function slice(t, index, count) + return table.move(t, index, index + count - 1, 1, {}) +end + +print('verify slice()') +-- verify that slice() does what we expect +assert(table.concat(slice(alphabits, 4, 3)) == "def") +assert(table.concat(slice(alphabits, 14, 3)) == "nop") +assert(table.concat(slice(alphabits, 25, 3)) == "yz") + +print('function fetch()') +function fetch(key, index) + -- fetch function is defined to be 0-relative: fix for Lua data + -- constrain view of alphabits to slices of at most 3 elements + return slice(alphabits, index+1, 3), index +end + +print('result_view()') +-- for test purposes, key is irrelevant, so just 'key' +view = result_view({'key', #alphabits}, fetch) + +print('function check_iter()') +function check_iter(...) + result = {} + for k, v in ... do + table.insert(result, v) + end + assert(table.concat(result) == alphabet) +end + +print('check_iter(pairs(view))') +check_iter(pairs(view)) +print('check_iter(ipairs(view))') +check_iter(ipairs(view)) +print('check_iter(view)') +check_iter(view) + +print('raw index access') +assert(view[5] == 'e') +assert(view[10] == 'j') +assert(view[15] == 'o') +assert(view[20] == 't') +assert(view[25] == 'y') + +print('Success!') + diff --git a/indra/newview/scripts/lua/test_setdtor.lua b/indra/newview/scripts/lua/test_setdtor.lua new file mode 100644 index 0000000000..ec5cd47e93 --- /dev/null +++ b/indra/newview/scripts/lua/test_setdtor.lua @@ -0,0 +1,91 @@ +inspect = require 'inspect' + +print('initial setdtor') +bye = LL.setdtor('initial setdtor', 'Goodbye world!', print) + +print('arithmetic') +n = LL.setdtor('arithmetic', 11, print) +print("n =", n) +print("n._target =", n._target) +print(pcall(function() n._target = 12 end)) +print("getmetatable(n) =", inspect(getmetatable(n))) +print("-n =", -n) +for i = 10, 12 do + -- Comparison metamethods are only called if both operands have the same + -- metamethod. + tempi = LL.setdtor('tempi', i, function(n) print('temp', i) end) + print(`n < {i}`, n < tempi) + print(`n <= {i}`, n <= tempi) + print(`n == {i}`, n == tempi) + print(`n ~= {i}`, n ~= tempi) + print(`n >= {i}`, n >= tempi) + print(`n > {i}`, n > tempi) +end +i = 2 +print(`n + {i} =`, n + i) +print(`{i} + n =`, i + n) +print(`n - {i} =`, n - i) +print(`{i} - n =`, i - n) +print(`n * {i} =`, n * i) +print(`{i} * n =`, i * n) +print(`n / {i} =`, n / i) +print(`{i} / n =`, i / n) +print(`n // {i} =`, n // i) +print(`{i} // n =`, i // n) +print(`n % {i} =`, n % i) +print(`{i} % n =`, i % n) +print(`n ^ {i} =`, n ^ i) +print(`{i} ^ n =`, i ^ n) + +print('string') +s = LL.setdtor('string', 'hello', print) +print('s =', s) +print('#s =', #s) +print('s .. " world" =', s .. " world") +print('"world " .. s =', "world " .. s) + +print('table') +t = LL.setdtor('table', {'[1]', '[2]', abc='.abc', def='.def'}, + function(t) print(inspect(t)) end) +print('t =', inspect(t)) +print('t._target =', inspect(t._target)) +print('#t =', #t) +print('next(t) =', next(t)) +print('next(t, 1) =', next(t, 1)) +print('t[2] =', t[2]) +print('t.def =', t.def) +t[1] = 'new [1]' +print('t[1] =', t[1]) +print('for k, v in pairs(t) do') +for k, v in pairs(t) do + print(`{k}: {v}`) +end +print('for k, v in ipairs(t) do') +for k, v in ipairs(t) do + print(`{k}: {v}`) +end +print('for k, v in t do') +for k, v in t do + print(`{k}: {v}`) +end +-- and now for something completely different +setmetatable( + t._target, + { + __iter = function(arg) + return next, {'alternate', '__iter'} + end + } +) +print('for k, v in t with __iter() metamethod do') +for k, v in t do + print(`{k}: {v}`) +end + +print('function') +f = LL.setdtor('function', function(a, b) return (a .. b) end, print) +print('f =', f) +print('f._target =', f._target) +print('f("Hello", " world") =', f("Hello", " world")) + +print('cleanup') diff --git a/indra/newview/scripts/lua/test_snapshot.lua b/indra/newview/scripts/lua/test_snapshot.lua new file mode 100644 index 0000000000..d7c878833b --- /dev/null +++ b/indra/newview/scripts/lua/test_snapshot.lua @@ -0,0 +1,15 @@ +local UI = require 'UI' + +PATH = 'E:\\' +-- 'png', 'jpeg' or 'bmp' +EXT = '.png' + +NAME_SIMPLE = 'Snapshot_simple' .. '_' .. os.date("%Y-%m-%d_%H-%M-%S") +UI.snapshot(PATH .. NAME_SIMPLE .. EXT) + +NAME_ARGS = 'Snapshot_args' .. '_' .. os.date("%Y-%m-%d_%H-%M-%S") + +-- 'COLOR' or 'DEPTH' +TYPE = 'COLOR' +UI.snapshot{PATH .. NAME_ARGS .. EXT, width = 700, height = 400, + type = TYPE, showui = false, showhud = false} diff --git a/indra/newview/scripts/lua/test_timers.lua b/indra/newview/scripts/lua/test_timers.lua new file mode 100644 index 0000000000..be5001aa16 --- /dev/null +++ b/indra/newview/scripts/lua/test_timers.lua @@ -0,0 +1,63 @@ +local timers = require 'timers' + +-- This t0 is constructed for 10 seconds, but its purpose is to exercise the +-- query and cancel methods. It would print "t0 fired at..." if it fired, but +-- it doesn't, so you don't see that message. Instead you see that isRunning() +-- is true, that timeUntilCall() is (true, close to 10), that cancel() returns +-- true. After that, isRunning() is false, timeUntilCall() returns (false, 0), +-- and a second cancel() returns false. +print('t0(10)') +start = os.clock() +t0 = timers.Timer(10, function() print('t0 fired at', os.clock() - start) end) +print('t0:isRunning(): ', t0:isRunning()) +print('t0:timeUntilCall(): ', t0:timeUntilCall()) +print('t0:cancel(): ', t0:cancel()) +print('t0:isRunning(): ', t0:isRunning()) +print('t0:timeUntilCall(): ', t0:timeUntilCall()) +print('t0:cancel(): ', t0:cancel()) + +-- t1 is supposed to fire after 5 seconds, but it doesn't wait, so you see the +-- t2 messages immediately after. +print('t1(5)') +start = os.clock() +t1 = timers.Timer(5, function() print('t1 fired at', os.clock() - start) end) + +-- t2 illustrates that instead of passing a callback to new(), you can +-- override the timer instance's tick() method. But t2 doesn't wait either, so +-- you see the Timer(5) message immediately. +print('t2(2)') +start = os.clock() +t2 = timers.Timer(2) +function t2:tick() + print('t2 fired at', os.clock() - start) +end + +-- This anonymous timer blocks the calling fiber for 5 seconds. Other fibers +-- are free to run during that time, so you see the t2 callback message and +-- then the t1 callback message before the Timer(5) completion message. +print('Timer(5) waiting') +start = os.clock() +timers.Timer(5, 'wait') +print(string.format('Timer(5) waited %f seconds', os.clock() - start)) + +-- This test demonstrates a repeating timer. It also shows that you can (but +-- need not) use a coroutine as the timer's callback function: unlike Python, +-- Lua doesn't disinguish between yield() and return. A coroutine wrapped with +-- coroutine.wrap() looks to Lua just like any other function that you can +-- call repeatedly and get a result each time. We use that to count the +-- callback calls and stop after a certain number. Of course that could also +-- be arranged in a plain function by incrementing a script-scope counter, but +-- it's worth knowing that a coroutine timer callback can be used to manage +-- more complex control flows. +start = os.clock() +timers.Timer( + 2, + coroutine.wrap(function() + for i = 1,5 do + print('repeat(2) timer fired at ', os.clock() - start) + coroutine.yield(nil) -- keep running + end + print('repeat(2) timer fired last at ', os.clock() - start) + return true -- stop + end), + true) -- iterate diff --git a/indra/newview/scripts/lua/test_toolbars.lua b/indra/newview/scripts/lua/test_toolbars.lua new file mode 100644 index 0000000000..7683fca8a3 --- /dev/null +++ b/indra/newview/scripts/lua/test_toolbars.lua @@ -0,0 +1,26 @@ +UI = require 'UI' + +local BUTTONS = UI.getToolbarBtnNames() +local TOOLBARS = {'left','right','bottom'} + +-- Clear the toolbars and then add the toolbar buttons to the random toolbar +response = UI.popup:alertYesCancel('Toolbars will be randomly reshuffled. Proceed?') +if response == 'OK' then + UI.clearAllToolbars() + math.randomseed(os.time()) + + -- add the buttons to the random toolbar + for i = 1, #BUTTONS do + UI.addToolbarBtn(BUTTONS[i], TOOLBARS[math.random(3)]) + end + + -- remove some of the added buttons from the toolbars + for i = 1, #BUTTONS do + if math.random(100) < 30 then + UI.removeToolbarBtn(BUTTONS[i]) + end + end + popup:tip('Toolbars were reshuffled') +else + popup:tip('Canceled') +end diff --git a/indra/newview/scripts/lua/test_top_menu.lua b/indra/newview/scripts/lua/test_top_menu.lua new file mode 100644 index 0000000000..780a384c92 --- /dev/null +++ b/indra/newview/scripts/lua/test_top_menu.lua @@ -0,0 +1,34 @@ +UI = require 'UI' + +--Add new drop-down 'LUA Menu' to the Top menu. +local MENU_NAME = "lua_menu" +UI.addMenu{name=MENU_NAME,label="LUA Menu"} + +--Add two new menu items to the 'LUA Menu': 'Debug console' and 'Scripts' +UI.addMenuItem{name="lua_debug",label="Debug console", + param="lua_debug", + func="Floater.ToggleOrBringToFront", + parent_menu=MENU_NAME} + +UI.addMenuItem{name="lua_scripts",label="Scripts", + param="lua_scripts", + func="Floater.ToggleOrBringToFront", + parent_menu=MENU_NAME} + +--Add menu separator to the 'LUA Menu' under added menu items +UI.addMenuSeparator{parent_menu=MENU_NAME} + +--Add two new menu branch 'About...' to the 'LUA Menu' +local BRANCH_NAME = "about_branch" +UI.addMenuBranch{name="about_branch",label="About...",parent_menu=MENU_NAME} + +--Add two new menu items to the 'About...' branch +UI.addMenuItem{name="lua_info",label="Lua...", + param="https://www.lua.org/about.html", + func="Advanced.ShowURL", + parent_menu=BRANCH_NAME} + +UI.addMenuItem{name="lua_info",label="Luau...", + param="https://luau-lang.org/", + func="Advanced.ShowURL", + parent_menu=BRANCH_NAME} diff --git a/indra/newview/scripts/lua/testmod.lua b/indra/newview/scripts/lua/testmod.lua new file mode 100644 index 0000000000..60f7f80db1 --- /dev/null +++ b/indra/newview/scripts/lua/testmod.lua @@ -0,0 +1,2 @@ +print('loaded scripts/lua/testmod.lua') +return function () return 'hello from scripts/lua/testmod.lua' end diff --git a/indra/newview/skins/default/xui/en/floater_lua_debug.xml b/indra/newview/skins/default/xui/en/floater_lua_debug.xml new file mode 100644 index 0000000000..15027f1647 --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_lua_debug.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<floater + can_minimize="false" + can_resize="true" + can_close="true" + bevel_style="in" + height="220" + layout="topleft" + name="LUA debug" + save_rect="true" + title="LUA DEBUG" + single_instance="true" + width="535"> + <text + type="string" + length="1" + follows="left|top" + font="SansSerif" + height="30" + layout="topleft" + left="10" + left_delta="10" + name="editor_path_label" + top="10" + width="100"> + LUA string: + </text> + <line_editor + border_style="line" + border_thickness="1" + follows="left|top|right" + font="SansSerif" + height="20" + layout="topleft" + left="10" + max_length_bytes="300" + name="lua_cmd" + select_on_focus="true" + top_delta="30" + width="435" /> + <button + follows="right|top" + height="25" + label="Execute" + layout="topleft" + left_pad="5" + name="execute_btn" + top_delta="-2" + width="75" /> + + <text_editor + enabled="false" + left="10" + height="95" + layout="topleft" + name="result_text" + follows="all" + max_length="65536" + width="515" + top_delta="40" + word_wrap="true" /> + <text + type="string" + length="1" + follows="left|bottom" + font="SansSerif" + height="30" + layout="topleft" + left="10" + name="path_label" + top_pad="15" + width="100"> + File Path: + </text> + <line_editor + border_style="line" + enabled="false" + border_thickness="1" + follows="left|bottom|right" + font="SansSerif" + height="20" + layout="topleft" + left_delta="65" + max_length_bytes="300" + name="script_path" + select_on_focus="true" + top_delta="-2" + width="320" /> + <button + follows="right|bottom" + height="25" + label="Browse..." + label_selected="Browse..." + layout="topleft" + left_pad="5" + name="browse_btn" + top_delta="-2" + width="70" /> + <button + follows="right|bottom" + height="25" + label="Run" + label_selected="Run" + layout="topleft" + left_pad="5" + name="run_btn" + width="50" /> +</floater> diff --git a/indra/newview/skins/default/xui/en/floater_lua_scripts.xml b/indra/newview/skins/default/xui/en/floater_lua_scripts.xml new file mode 100644 index 0000000000..6859201650 --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_lua_scripts.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<floater + can_minimize="false" + can_resize="true" + can_close="true" + bevel_style="in" + height="220" + min_height="220" + layout="topleft" + name="LUA scripts" + save_rect="true" + title="LUA Scripts" + single_instance="true" + width="555" + min_width="555"> + <scroll_list + column_padding="0" + draw_stripes="true" + draw_heading="true" + height="200" + left="10" + follows="all" + layout="topleft" + sort_column="script_name" + name="scripts_list" + top_pad="10" + width="535"> + <scroll_list.columns + label="Name" + name="script_name" + width="180" /> + <scroll_list.columns + label="Path" + name="script_path"/> + </scroll_list> +</floater> diff --git a/indra/newview/skins/default/xui/en/floater_settings_debug.xml b/indra/newview/skins/default/xui/en/floater_settings_debug.xml index a93be6a18d..0b8190df7e 100644 --- a/indra/newview/skins/default/xui/en/floater_settings_debug.xml +++ b/indra/newview/skins/default/xui/en/floater_settings_debug.xml @@ -43,6 +43,20 @@ label="Setting" name="setting" /> </scroll_list> + <button + follows="right|bottom" + layout="topleft" + image_hover_unselected="Toolbar_Middle_Over" + image_overlay="Icon_Copy" + image_selected="Toolbar_Middle_Selected" + image_unselected="Toolbar_Middle_Off" + name="copy_btn" + tool_tip="Copy to clipboard" + top_delta="8" + left_pad="10" + visible="false" + height="20" + width="20" /> <text type="string" length="1" @@ -51,12 +65,11 @@ layout="topleft" name="setting_name_txt" font="SansSerifSmallBold" - top_delta="8" - left_pad="10" + left_pad="4" visible="false" use_ellipses="true" text_color="White" - width="240"> + width="225"> Debug setting name </text> <text_editor @@ -67,6 +80,7 @@ name="comment_text" follows="left|top" width="240" + left="320" top_delta="20" word_wrap="true" /> <radio_group @@ -198,4 +212,15 @@ name="hide_default" width="330"> </check_box> + <text_editor + read_only="true" + visible="false" + height="115" + layout="topleft" + name="llsd_text" + follows="left|top" + width="240" + left="320" + top="180" + word_wrap="true" /> </floater> diff --git a/indra/newview/skins/default/xui/en/menu_lua_scripts.xml b/indra/newview/skins/default/xui/en/menu_lua_scripts.xml new file mode 100644 index 0000000000..645fee405d --- /dev/null +++ b/indra/newview/skins/default/xui/en/menu_lua_scripts.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<context_menu + name="Scripts"> + <menu_item_call + label="Open Containing Folder" + layout="topleft" + name="open_folder"> + <menu_item_call.on_click + function="Script.OpenFolder" /> + </menu_item_call> + <menu_item_separator/> + <menu_item_call + label="Terminate script" + layout="topleft" + name="terminate"> + <menu_item_call.on_click + function="Script.Terminate" /> + </menu_item_call> +</context_menu> diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml index 2d04b3e0fe..effd19b708 100644 --- a/indra/newview/skins/default/xui/en/strings.xml +++ b/indra/newview/skins/default/xui/en/strings.xml @@ -534,7 +534,9 @@ http://secondlife.com/support for help fixing this problem. <string name="ChangeYourDefaultAnimations">Change your default animations</string> <string name="ForceSitAvatar">Force your avatar to sit</string> <string name="ChangeEnvSettings">Change your environment settings</string> - + <string name="ScriptBy" value="Script by "/> + <string name="ScriptStr" value="Script: "/> + <string name="NotConnected">Not Connected</string> <string name="AgentNameSubst">(You)</string> <!-- Substitution for agent name --> <string name="JoinAnExperience"/><!-- intentionally blank --> @@ -4027,6 +4029,10 @@ Please check http://status.secondlifegrid.net to see if there is a known problem <string name="DeleteItem">Delete selected item?</string> <string name="EmptyOutfitText">There are no items in this outfit</string> + <string name="OutfitNotFound" value="Couldn't find outfit "/> + <string name="OutfitNotAdded" value="Can't add to COF outfit "/> + <string name="OutfitNotReplaced" value="Can't replace COF with outfit "/> + <string name="SystemFolderNotWorn" value="Can't wear system folder "/> <!-- External editor status codes --> <string name="ExternalEditorNotSet">Select an editor by setting the environment variable LL_SCRIPT_EDITOR or the ExternalEditor setting. diff --git a/indra/newview/tests/cppfeatures_test.cpp b/indra/newview/tests/cppfeatures_test.cpp index f5ea3a522b..ca94dcfc95 100644 --- a/indra/newview/tests/cppfeatures_test.cpp +++ b/indra/newview/tests/cppfeatures_test.cpp @@ -283,7 +283,7 @@ void cpp_features_test_object_t::test<8>() ensure("init member inline 1", ii.mFoo==10); InitInlineWithConstructor iici; - ensure("init member inline 2", iici.mFoo=10); + ensure("init member inline 2", iici.mFoo==10); ensure("init member inline 3", iici.mBar==25); } diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp new file mode 100644 index 0000000000..8d1333815b --- /dev/null +++ b/indra/newview/tests/llluamanager_test.cpp @@ -0,0 +1,523 @@ +/** + * @file llluamanager_test.cpp + * @author Nat Goodspeed + * @date 2023-09-28 + * @brief Test for llluamanager. + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Copyright (c) 2023, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +//#include "llviewerprecompiledheaders.h" +// associated header +#include "../newview/llluamanager.h" +// STL headers +// std headers +#include <vector> +// external library headers +// other Linden headers +#include "../llcommon/tests/StringVec.h" +#include "../test/lltut.h" +#include "llapp.h" +#include "llcontrol.h" +#include "lldate.h" +#include "llevents.h" +#include "lleventcoro.h" +#include "llsdutil.h" +#include "lluri.h" +#include "lluuid.h" +#include "lua_function.h" +#include "lualistener.h" +#include "stringize.h" + +class LLTestApp : public LLApp +{ +public: + bool init() override { return true; } + bool cleanup() override { return true; } + bool frame() override { return true; } +}; + +LLControlGroup gSavedSettings("Global"); + +/***************************************************************************** +* TUT +*****************************************************************************/ +namespace tut +{ + struct llluamanager_data + { + llluamanager_data() + { + // Load gSavedSettings from source tree + // indra/newview/tests/llluamanager_test.cpp => + // indra/newview + auto newview{ fsyspath(__FILE__).parent_path().parent_path() }; + auto settings{ newview / "app_settings" / "settings.xml" }; + // true suppresses implicit declare; implicit declare requires + // that every variable in settings.xml has a Comment, which many don't. + gSavedSettings.loadFromFile(settings.u8string(), true); + // At test time, since we don't have the app bundle available, + // extend LuaRequirePath to include the require directory in the + // source tree. + auto require{ (newview / "scripts" / "lua" / "require").u8string() }; + auto paths{ gSavedSettings.getLLSD("LuaRequirePath") }; + bool found = false; + for (const auto& path : llsd::inArray(paths)) + { + if (path.asString() == require) + { + found = true; + break; + } + } + if (! found) + { + paths.append(require); + gSavedSettings.setLLSD("LuaRequirePath", paths); + } + } + // We need an LLApp instance because LLLUAmanager uses coroutines, + // which suspend, and when a coroutine suspends it checks LLApp state, + // and if it's not APP_STATUS_RUNNING the coroutine terminates. + LLTestApp mApp; + }; + typedef test_group<llluamanager_data> llluamanager_group; + typedef llluamanager_group::object object; + llluamanager_group llluamanagergrp("llluamanager"); + + static struct LuaExpr + { + std::string desc, expr; + LLSD expect; + } lua_expressions[] = { + { "nil", "nil", LLSD() }, + { "true", "true", true }, + { "false", "false", false }, + { "int", "17", 17 }, + { "real", "3.14", 3.14 }, + { "string", "'string'", "string" }, + // can't synthesize Lua userdata in Lua code: that can only be + // constructed by a C function + { "empty table", "{}", LLSD() }, + { "nested empty table", "{ 1, 2, 3, {}, 5 }", + llsd::array(1, 2, 3, LLSD(), 5) }, + { "nested non-empty table", "{ 1, 2, 3, {a=0, b=1}, 5 }", + llsd::array(1, 2, 3, llsd::map("a", 0, "b", 1), 5) }, + }; + + template<> template<> + void object::test<1>() + { + set_test_name("test Lua results"); + for (auto& luax : lua_expressions) + { + auto [count, result] = + LLLUAmanager::waitScriptLine("return " + luax.expr); + auto desc{ stringize("waitScriptLine(", luax.desc, "): ") }; + // if count < 0, report Lua error message + ensure_equals(desc + result.asString(), count, 1); + ensure_equals(desc + "result", result, luax.expect); + } + } + + void from_lua(const std::string& desc, const std::string_view& construct, const LLSD& expect) + { + LLSD fromlua; + LLStreamListener pump("testpump", + [&fromlua](const LLSD& data){ fromlua = data; }); + const std::string lua(stringize( + "data = ", construct, "\n" + "LL.post_on('testpump', data)\n" + )); + auto [count, result] = LLLUAmanager::waitScriptLine(lua); + // We woke up again ourselves because the coroutine running Lua has + // finished. But our Lua chunk didn't actually return anything, so we + // expect count to be 0 and result to be undefined. + ensure_equals(desc + ": " + result.asString(), count, 0); + ensure_equals(desc, fromlua, expect); + } + + template<> template<> + void object::test<2>() + { + set_test_name("LLSD from post_on()"); + for (auto& luax : lua_expressions) + { + from_lua(luax.desc, luax.expr, luax.expect); + } + } + + template<> template<> + void object::test<3>() + { + set_test_name("test post_on(), get_event_pumps(), get_event_next()"); + StringVec posts; + LLStreamListener pump("testpump", + [&posts](const LLSD& data) + { posts.push_back(data.asString()); }); + const std::string lua( + "-- test post_on,get_event_pumps,get_event_next\n" + "LL.post_on('testpump', 'entry')\n" + "LL.post_on('testpump', 'get_event_pumps()')\n" + "replypump, cmdpump = LL.get_event_pumps()\n" + "LL.post_on('testpump', replypump)\n" + "LL.post_on('testpump', 'get_event_next()')\n" + "pump, data = LL.get_event_next()\n" + "LL.post_on('testpump', data)\n" + "LL.post_on('testpump', 'exit')\n" + ); + // It's important to let the startScriptLine() coroutine run + // concurrently with ours until we've had a chance to post() our + // reply. + auto future = LLLUAmanager::startScriptLine(lua); + StringVec expected{ + "entry", + "get_event_pumps()", + "", + "get_event_next()", + "message", + "exit" + }; + expected[2] = posts.at(2); + LL_DEBUGS() << "Found pumpname '" << expected[2] << "'" << LL_ENDL; + LLEventPump& luapump{ LLEventPumps::instance().obtain(expected[2]) }; + LL_DEBUGS() << "Found pump '" << luapump.getName() << "', type '" + << LLError::Log::classname(luapump) + << "': post('" << expected[4] << "')" << LL_ENDL; + luapump.post(expected[4]); + auto [count, result] = future.get(); + ensure_equals("post_on(): " + result.asString(), count, 0); + ensure_equals("post_on() sequence", posts, expected); + } + + void round_trip(const std::string& desc, const LLSD& send, const LLSD& expect) + { + LLEventMailDrop testpump("testpump"); + const std::string lua( + "-- test LLSD round trip\n" + "replypump, cmdpump = LL.get_event_pumps()\n" + "LL.post_on('testpump', replypump)\n" + "pump, data = LL.get_event_next()\n" + "return data\n" + ); + auto future = LLLUAmanager::startScriptLine(lua); + // We woke up again ourselves because the coroutine running Lua has + // reached the get_event_next() call, which suspends the calling C++ + // coroutine (including the Lua code running on it) until we post + // something to that reply pump. + auto luapump{ llcoro::suspendUntilEventOn(testpump).asString() }; + LLEventPumps::instance().post(luapump, send); + // The C++ coroutine running the Lua script is now ready to run. Run + // it so it will echo the LLSD back to us. + auto [count, result] = future.get(); + ensure_equals(stringize("round_trip(", desc, "): ", result.asString()), count, 1); + ensure_equals(desc, result, expect); + } + + // Define an RTItem to be used for round-trip LLSD testing: what it is, + // what we send to Lua, what we expect to get back. They could be the + // same. + struct RTItem + { + RTItem(const std::string& name, const LLSD& send, const LLSD& expect): + mName(name), + mSend(send), + mExpect(expect) + {} + RTItem(const std::string& name, const LLSD& both): + mName(name), + mSend(both), + mExpect(both) + {} + + std::string mName; + LLSD mSend, mExpect; + }; + + template<> template<> + void object::test<4>() + { + set_test_name("LLSD round trip"); + LLSD::Binary binary{ 3, 1, 4, 1, 5, 9, 2, 6, 5 }; + const char* uuid{ "01234567-abcd-0123-4567-0123456789ab" }; + const char* date{ "2023-10-04T21:06:00Z" }; + const char* uri{ "https://secondlife.com/index.html" }; + std::vector<RTItem> items{ + RTItem("undefined", LLSD()), + RTItem("true", true), + RTItem("false", false), + RTItem("int", 17), + RTItem("real", 3.14), + RTItem("int real", 27.0, 27), + RTItem("string", "string"), + RTItem("binary", binary), + RTItem("empty array", LLSD::emptyArray(), LLSD()), + RTItem("empty map", LLSD::emptyMap(), LLSD()), + RTItem("UUID", LLUUID(uuid), uuid), + RTItem("date", LLDate(date), date), + RTItem("uri", LLURI(uri), uri) + }; + // scalars + for (const auto& item: items) + { + round_trip(item.mName, item.mSend, item.mExpect); + } + + // array + LLSD send_array{ LLSD::emptyArray() }, expect_array{ LLSD::emptyArray() }; + for (const auto& item: items) + { + send_array.append(item.mSend); + expect_array.append(item.mExpect); + } + // exercise the array tail trimming below + send_array.append(items[0].mSend); + expect_array.append(items[0].mExpect); + // Lua takes a table value of nil to mean: don't store this key. An + // LLSD array containing undefined entries (converted to nil) leaves + // "holes" in the Lua table. These will be converted back to undefined + // LLSD entries -- except at the end. Trailing undefined entries are + // simply omitted from the table -- so the table converts back to a + // shorter LLSD array. We've constructed send_array and expect_array + // according to 'items' above -- but truncate from expect_array any + // trailing entries whose mSend will map to Lua nil. + while (expect_array.size() > 0 && + send_array[expect_array.size() - 1].isUndefined()) + { + expect_array.erase(LLSD::Integer(expect_array.size() - 1)); + } + round_trip("array", send_array, expect_array); + + // map + LLSD send_map{ LLSD::emptyMap() }, expect_map{ LLSD::emptyMap() }; + for (const auto& item: items) + { + send_map[item.mName] = item.mSend; + // see comment in the expect_array truncation loop above -- + // Lua never stores table entries with nil values + if (item.mSend.isDefined()) + { + expect_map[item.mName] = item.mExpect; + } + } + round_trip("map", send_map, expect_map); + + // deeply nested map: exceed Lua's default stack space (20), + // i.e. verify that we have the right checkstack() calls + for (int i = 0; i < 20; ++i) + { + LLSD new_send_map{ send_map }, new_expect_map{ expect_map }; + new_send_map["nested map"] = send_map; + new_expect_map["nested map"] = expect_map; + send_map = new_send_map; + expect_map = new_expect_map; + } + round_trip("nested map", send_map, expect_map); + } + + template<> template<> + void object::test<5>() + { + set_test_name("leap.request() from main thread"); + const std::string lua( + "-- leap.request() from main thread\n" + "\n" + "leap = require 'leap'\n" + "\n" + "return {\n" + " a=leap.request('echo', {data='a'}).data,\n" + " b=leap.request('echo', {data='b'}).data\n" + "}\n" + ); + + LLStreamListener pump( + "echo", + [](const LLSD& data) + { + LL_DEBUGS("Lua") << "echo pump got: " << data << LL_ENDL; + sendReply(data, data); + }); + + auto [count, result] = LLLUAmanager::waitScriptLine(lua); + ensure_equals("Lua script didn't return item", count, 1); + ensure_equals("echo failed", result, llsd::map("a", "a", "b", "b")); + } + + template<> template<> + void object::test<6>() + { + set_test_name("interleave leap.request() responses"); + const std::string lua( + "-- interleave leap.request() responses\n" + "\n" + "fiber = require('fiber')\n" + "leap = require('leap')\n" + "local function debug(...) end\n" + "-- debug = require('printf')\n" + "\n" + "-- negative priority ensures catchall is always last\n" + "catchall = leap.WaitFor(-1, 'catchall')\n" + "function catchall:filter(pump, data)\n" + " debug('catchall:filter(%s, %s)', pump, data)\n" + " return data\n" + "end\n" + "\n" + "-- but first, catch events with 'special' key\n" + "catch_special = leap.WaitFor(2, 'catch_special')\n" + "function catch_special:filter(pump, data)\n" + " debug('catch_special:filter(%s, %s)', pump, data)\n" + " return if data['special'] ~= nil then data else nil\n" + "end\n" + "\n" + "function drain(waitfor)\n" + " debug('%s start', waitfor.name)\n" + " -- It seems as though we ought to be able to code this loop\n" + " -- over waitfor:wait() as:\n" + " -- for item in waitfor.wait, waitfor do\n" + " -- However, that seems to stitch a detour through C code into\n" + " -- the coroutine call stack, which prohibits coroutine.yield():\n" + " -- 'attempt to yield across metamethod/C-call boundary'\n" + " -- So we resort to two different calls to waitfor:wait().\n" + " local item = waitfor:wait()\n" + " while item do\n" + " debug('%s caught %s', waitfor.name, item)\n" + " item = waitfor:wait()\n" + " end\n" + " debug('%s done', waitfor.name)\n" + "end\n" + "\n" + "function requester(name)\n" + " debug('requester(%s) start', name)\n" + " local response = leap.request('testpump', {name=name})\n" + " debug('requester(%s) got %s', name, response)\n" + " -- verify that the correct response was dispatched to this coroutine\n" + " assert(response.name == name)\n" + "end\n" + "\n" + "-- fiber.print_all()\n" + "fiber.launch('catchall', drain, catchall)\n" + "fiber.launch('catch_special', drain, catch_special)\n" + "fiber.launch('requester(a)', requester, 'a')\n" + "fiber.launch('requester(b)', requester, 'b')\n" + // A script can normally count on an implicit fiber.run() call + // because fiber.lua calls LL.atexit(fiber.run). But atexit() + // functions are called by ~LuaState(), which (in the code below) + // won't be called until *after* we expect to interact with the + // various fibers. So make an explicit call for test purposes. + "fiber.run()\n" + ); + + LLSD requests; + LLStreamListener pump( + "testpump", + [&requests](const LLSD& data) + { + LL_DEBUGS("Lua") << "testpump got: " << data << LL_ENDL; + requests.append(data); + }); + + auto future = LLLUAmanager::startScriptLine(lua); + // LuaState::expr() periodically interrupts a running chunk to ensure + // the rest of our coroutines get cycles. Nonetheless, for this test + // we have to wait until both requester() coroutines have posted and + // are waiting for a reply. + for (unsigned count=0; count < 100; ++count) + { + if (requests.size() == 2) + break; + llcoro::suspend(); + } + ensure_equals("didn't get both requests", requests.size(), 2); + auto replyname{ requests[0]["reply"].asString() }; + auto& replypump{ LLEventPumps::instance().obtain(replyname) }; + // moreover, we expect they arrived in the order they were created + ensure_equals("a wasn't first", requests[0]["name"].asString(), "a"); + ensure_equals("b wasn't second", requests[1]["name"].asString(), "b"); + replypump.post(llsd::map("special", "K")); + // respond to requester(b) FIRST + replypump.post(requests[1]); + replypump.post(llsd::map("name", "not special")); + // now respond to requester(a) + replypump.post(requests[0]); + // tell leap we're done + replypump.post(LLSD()); + auto [count, result] = future.get(); + ensure_equals("leap.lua: " + result.asString(), count, 0); + } + + template<> template<> + void object::test<7>() + { + set_test_name("stop hanging Lua script"); + const std::string lua( + "-- hanging Lua script should terminate\n" + "\n" + "LL.get_event_next()\n" + ); + auto future = LLLUAmanager::startScriptLine(lua); + // Poke LLTestApp to send its preliminary shutdown message. + mApp.setQuitting(); + // but now we have to give the startScriptLine() coroutine a chance to run + auto [count, result] = future.get(); + ensure_equals("killed Lua script terminated normally", count, -1); + ensure_contains("unexpected killed Lua script error", + result.asString(), "viewer is stopping"); + } + + template<> template<> + void object::test<8>() + { + set_test_name("stop looping Lua script"); + const std::string desc("looping Lua script should terminate"); + const std::string lua( + "-- " + desc + "\n" + "\n" + "while true do\n" + " x = 1\n" + "end\n" + ); + auto [count, result] = LLLUAmanager::waitScriptLine(lua); + // We expect the above erroneous script has been forcibly terminated + // because it ran too long without doing any actual work. + ensure_equals(desc + " count: " + result.asString(), count, -1); + ensure_contains(desc + " result", result.asString(), "terminated"); + } + + template <typename T> + struct Visible + { + Visible(T name): name(name) + { + LL_INFOS() << "Visible<" << LLError::Log::classname<T>() << ">('" << name << "')" << LL_ENDL; + } + Visible(const Visible&) = delete; + Visible& operator=(const Visible&) = delete; + ~Visible() + { + LL_INFOS() << "~Visible<" << LLError::Log::classname<T>() << ">('" << name << "')" << LL_ENDL; + } + T name; + }; + + template<> template<> + void object::test<9>() + { + set_test_name("track distinct lua_emplace<T>() types"); + LuaState L; + lua_emplace<Visible<std::string>>(L, "std::string 0"); + int st0tag = lua_userdatatag(L, -1); + lua_emplace<Visible<const char*>>(L, "const char* 0"); + int cp0tag = lua_userdatatag(L, -1); + lua_emplace<Visible<std::string>>(L, "std::string 1"); + int st1tag = lua_userdatatag(L, -1); + lua_emplace<Visible<const char*>>(L, "const char* 1"); + int cp1tag = lua_userdatatag(L, -1); + lua_settop(L, 0); + ensure_equals("lua_emplace<std::string>() tags diverge", st0tag, st1tag); + ensure_equals("lua_emplace<const char*>() tags diverge", cp0tag, cp1tag); + ensure_not_equals("lua_emplace<>() tags collide", st0tag, cp0tag); + } +} // namespace tut diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index b2f9654eb3..aa2b0b0e25 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -166,6 +166,12 @@ class ViewerManifest(LLManifest): self.path("*/*/*/*.js") self.path("*/*/*.html") + with self.prefix(src_dst="scripts/lua"): + self.path("*.lua") + self.path("*.xml") + with self.prefix(src_dst='require'): + self.path("*.lua") + #build_data.json. Standard with exception handling is fine. If we can't open a new file for writing, we have worse problems #platform is computed above with other arg parsing build_data_dict = {"Type":"viewer","Version":'.'.join(self.args['version']), @@ -281,7 +287,7 @@ class ViewerManifest(LLManifest): # A line that starts with a non-whitespace character is a name; all others describe contributions, so collect the names names = [] for line in lines : - if re.match("\S", line) : + if re.match(r"\S", line) : names.append(line.rstrip()) # It's not fair to always put the same people at the head of the list random.shuffle(names) diff --git a/indra/test/debug.h b/indra/test/debug.h index 1579bb9c86..ea9c634cc7 100644 --- a/indra/test/debug.h +++ b/indra/test/debug.h @@ -30,43 +30,56 @@ #define LL_DEBUG_H #include "print.h" +#include "stringize.h" +#include <exception> // std::uncaught_exceptions() /***************************************************************************** * Debugging stuff *****************************************************************************/ /** - * This class is intended to illuminate entry to a given block, exit from the - * same block and checkpoints along the way. It also provides a convenient - * place to turn std::cerr output on and off. - * - * If the environment variable LOGTEST is non-empty, each Debug instance will - * announce its construction and destruction, presumably at entry and exit to - * the block in which it's declared. Moreover, any arguments passed to its - * operator()() will be streamed to std::cerr, prefixed by the block - * description. + * Return true if the environment variable LOGTEST is non-empty. * * The variable LOGTEST is used because that's the environment variable * checked by test.cpp, our TUT main() program, to turn on LLError logging. It * is expected that Debug is solely for use in test programs. */ +inline +bool LOGTEST_enabled() +{ + auto LOGTEST{ getenv("LOGTEST") }; + // debug output enabled when LOGTEST is set AND non-empty + return LOGTEST && *LOGTEST; +} + +/** + * This class is intended to illuminate entry to a given block, exit from the + * same block and checkpoints along the way. It also provides a convenient + * place to turn std::cerr output on and off. + * + * If enabled, each Debug instance will announce its construction and + * destruction, presumably at entry and exit to the block in which it's + * declared. Moreover, any arguments passed to its operator()() will be + * streamed to std::cerr, prefixed by the block description. + */ class Debug { public: - Debug(const std::string& block): - mBlock(block), - mLOGTEST(getenv("LOGTEST")), - // debug output enabled when LOGTEST is set AND non-empty - mEnabled(mLOGTEST && *mLOGTEST) + template <typename... ARGS> + Debug(ARGS&&... args): + mBlock(stringize(std::forward<ARGS>(args)...)), + mEnabled(LOGTEST_enabled()) { (*this)("entry"); } // non-copyable Debug(const Debug&) = delete; + Debug& operator=(const Debug&) = delete; ~Debug() { - (*this)("exit"); + auto exceptional{ std::uncaught_exceptions()? "exceptional " : "" }; + (*this)(exceptional, "exit"); } template <typename... ARGS> @@ -80,7 +93,6 @@ public: private: const std::string mBlock; - const char* mLOGTEST; bool mEnabled; }; @@ -88,20 +100,19 @@ private: // of the Debug block. #define DEBUG Debug debug(LL_PRETTY_FUNCTION) -// These BEGIN/END macros are specifically for debugging output -- please -// don't assume you must use such for coroutines in general! They only help to -// make control flow (as well as exception exits) explicit. -#define BEGIN \ -{ \ - DEBUG; \ - try +/// If enabled, debug_expr(expression) gives you output concerning an inline +/// expression such as a class member initializer. +#define debug_expr(expr) debug_expr_(#expr, [&](){ return expr; }) -#define END \ - catch (...) \ - { \ - debug("*** exceptional "); \ - throw; \ - } \ +template <typename EXPR> +inline auto debug_expr_(const char* strexpr, EXPR&& lambda) +{ + if (! LOGTEST_enabled()) + return std::forward<EXPR>(lambda)(); + print("Before: ", strexpr); + auto result{ std::forward<EXPR>(lambda)() }; + print(strexpr, " -> ", result); + return result; } #endif /* ! defined(LL_DEBUG_H) */ diff --git a/indra/test/io.cpp b/indra/test/io.cpp index f77402065a..24e1a782d3 100644 --- a/indra/test/io.cpp +++ b/indra/test/io.cpp @@ -45,6 +45,7 @@ #include "llcommon.h" #include "lluuid.h" #include "llinstantmessage.h" +#include "stringize.h" namespace tut { @@ -1116,6 +1117,9 @@ namespace tut template<> template<> void fitness_test_object::test<5>() { + skip("Test is strongly timing dependent, " + "and on slow CI machines it fails way too often."); + const int retries = 100; // Set up the server LLPumpIO::chain_t chain; typedef LLCloneIOFactory<LLIOSleeper> sleeper_t; @@ -1129,9 +1133,12 @@ namespace tut chain.push_back(LLIOPipe::ptr_t(server)); mPump->addChain(chain, NEVER_CHAIN_EXPIRY_SECS); // We need to tickle the pump a little to set up the listen() - pump_loop(mPump, 0.1f); + for (int retry = 0; mPump->runningChains() < 1 && retry < retries; ++retry) + { + pump_loop(mPump, 0.1f); + } auto count = mPump->runningChains(); - ensure_equals("server chain onboard", count, 1); + ensure_equals("server chain 1 onboard", count, 1); LL_DEBUGS() << "** Server is up." << LL_ENDL; // Set up the client @@ -1140,9 +1147,12 @@ namespace tut bool connected = client->blockingConnect(server_host); ensure("Connected to server", connected); LL_DEBUGS() << "connected" << LL_ENDL; - pump_loop(mPump,0.1f); + for (int retry = 0; mPump->runningChains() < 2 && retry < retries; ++retry) + { + pump_loop(mPump,0.1f); + } count = mPump->runningChains(); - ensure_equals("server chain onboard", count, 2); + ensure_equals("server chain 2 onboard", count, 2); LL_DEBUGS() << "** Client is connected." << LL_ENDL; // We have connected, since the socket reader does not block, @@ -1156,20 +1166,32 @@ namespace tut chain.clear(); // pump for a bit and make sure all 3 chains are running - pump_loop(mPump,0.1f); + for (int retry = 0; mPump->runningChains() < 3 && retry < retries; ++retry) + { + pump_loop(mPump, 0.1f); + } count = mPump->runningChains(); - // ensure_equals("client chain onboard", count, 3); commented out because it fails frequently - appears to be timing sensitive + ensure_equals("client chain onboard", count, 3); LL_DEBUGS() << "** request should have been sent." << LL_ENDL; // pump for long enough the the client socket closes, and the // server socket should not be closed yet. - pump_loop(mPump,0.2f); + for (int retry = 0; mPump->runningChains() == 3 && retry < retries; ++retry) + { + pump_loop(mPump, 0.1f); + } + // We used to test for count == 2 here, but on a slow test machine it + // can happen that not just one but two chains close before we reach + // this point. count = mPump->runningChains(); - ensure_equals("client chain timed out ", count, 2); + ensure(stringize("client chain timed out: count ", count), count < 3); LL_DEBUGS() << "** client chain should be closed." << LL_ENDL; // At this point, the socket should be closed by the timeout - pump_loop(mPump,1.0f); + for (int retry = 0; mPump->runningChains() > 1 && retry < retries; ++retry) + { + pump_loop(mPump, 0.1f); + } count = mPump->runningChains(); ensure_equals("accepted socked close", count, 1); LL_DEBUGS() << "** Sleeper should have timed out.." << LL_ENDL; diff --git a/indra/test/llevents_tut.cpp b/indra/test/llevents_tut.cpp index bf5cd3f853..1f723c84b6 100644 --- a/indra/test/llevents_tut.cpp +++ b/indra/test/llevents_tut.cpp @@ -428,7 +428,7 @@ void events_object::test<9>() { set_test_name("listen(boost::bind(...TempListener...))"); // listen() can't do anything about a plain TempListener instance: - // it's not managed with shared_ptr, nor is it an LLEventTrackable subclass + // it's not managed with shared_ptr bool live = false; LLEventPump& heaptest(pumps.obtain("heaptest")); LLBoundListener connection; @@ -452,60 +452,4 @@ void events_object::test<9>() heaptest.stopListening("temp"); } -class TempTrackableListener: public TempListener, public LLEventTrackable -{ -public: - TempTrackableListener(const std::string& name, bool& liveFlag): - TempListener(name, liveFlag) - {} -}; - -template<> template<> -void events_object::test<10>() -{ - set_test_name("listen(boost::bind(...TempTrackableListener ref...))"); - bool live = false; - LLEventPump& heaptest(pumps.obtain("heaptest")); - LLBoundListener connection; - { - TempTrackableListener tempListener("temp", live); - ensure("TempTrackableListener constructed", live); - connection = heaptest.listen(tempListener.getName(), - boost::bind(&TempTrackableListener::call, - boost::ref(tempListener), _1)); - heaptest.post(1); - check_listener("received", tempListener, 1); - } // presumably this will make tempListener go away? - // verify that - ensure("TempTrackableListener destroyed", ! live); - ensure("implicit disconnect", ! connection.connected()); - // now just make sure we don't blow up trying to access a freed object! - heaptest.post(2); -} - -template<> template<> -void events_object::test<11>() -{ - set_test_name("listen(boost::bind(...TempTrackableListener pointer...))"); - bool live = false; - LLEventPump& heaptest(pumps.obtain("heaptest")); - LLBoundListener connection; - { - TempTrackableListener* newListener(new TempTrackableListener("temp", live)); - ensure("TempTrackableListener constructed", live); - connection = heaptest.listen(newListener->getName(), - boost::bind(&TempTrackableListener::call, - newListener, _1)); - heaptest.post(1); - check_listener("received", *newListener, 1); - // explicitly destroy newListener - delete newListener; - } - // verify that - ensure("TempTrackableListener destroyed", ! live); - ensure("implicit disconnect", ! connection.connected()); - // now just make sure we don't blow up trying to access a freed object! - heaptest.post(2); -} - } // namespace tut diff --git a/indra/test/lltut.h b/indra/test/lltut.h index e56b4e8d1c..fbf60444be 100644 --- a/indra/test/lltut.h +++ b/indra/test/lltut.h @@ -31,6 +31,8 @@ #include "is_approx_equal_fraction.h" // instead of llmath.h #include <cstring> +#include <string> +#include <vector> class LLDate; class LLSD; diff --git a/indra/test/print.h b/indra/test/print.h index 7577698cc8..6906eae581 100644 --- a/indra/test/print.h +++ b/indra/test/print.h @@ -23,7 +23,9 @@ struct NONL_t {}; inline void print() { +#ifdef LL_TEST std::cerr << std::endl; +#endif } // print(NONL) is a no-op @@ -35,8 +37,10 @@ void print(NONL_t) template <typename T, typename... ARGS> void print(T&& first, ARGS&&... rest) { +#ifdef LL_TEST std::cerr << first; print(std::forward<ARGS>(rest)...); +#endif } #endif /* ! defined(LL_PRINT_H) */ diff --git a/indra/test/test.cpp b/indra/test/test.cpp index 172b6e3542..0a817c32dd 100644 --- a/indra/test/test.cpp +++ b/indra/test/test.cpp @@ -35,13 +35,14 @@ */ #include "linden_common.h" -#include "llerrorcontrol.h" -#include "lltut.h" #include "chained_callback.h" -#include "stringize.h" -#include "namedtempfile.h" +#include "fsyspath.h" +#include "llerrorcontrol.h" #include "lltrace.h" #include "lltracethreadrecorder.h" +#include "lltut.h" +#include "namedtempfile.h" +#include "stringize.h" #include "apr_pools.h" #include "apr_getopt.h" @@ -522,6 +523,29 @@ int main(int argc, char **argv) // LOGTEST overrides default, but can be overridden by --debug. const char* LOGTEST = getenv("LOGTEST"); + // Sometimes we must rebuild much of the viewer before we get to the + // specific test we want to monitor, and some viewer integration tests are + // quite verbose. In addition to noticing plain LOGTEST= (for all tests), + // also notice LOGTEST_progname= (for a specific test). + // (Why doesn't MSVC notice fsyspath::operator std::string()? + // Why must we explicitly call fsyspath::string()?) + std::string basename(fsyspath(argv[0]).stem().string()); + // don't make user set LOGTEST_INTEGRATION_TEST_progname or (worse) + // LOGTEST_PROJECT_foo_TEST_bar -- only LOGTEST_progname or LOGTEST_bar + auto _TEST_ = basename.find("_TEST_"); + if (_TEST_ != std::string::npos) + { + basename.erase(0, _TEST_+6); + } + std::string LOGTEST_prog_key("LOGTEST_" + basename); + const char* LOGTEST_prog = getenv(LOGTEST_prog_key.c_str()); +// std::cout << LOGTEST_prog_key << "='" << (LOGTEST_prog? LOGTEST_prog : "") << "'" << std::endl; + if (LOGTEST_prog && *LOGTEST_prog) + { + LOGTEST = LOGTEST_prog; + std::cout << "LOGTEST='" << LOGTEST << "' from " << LOGTEST_prog_key << std::endl; + } + // values used for options parsing apr_status_t apr_err; const char* opt_arg = NULL; diff --git a/indra/test/writestr.h b/indra/test/writestr.h new file mode 100755 index 0000000000..af8be5a3aa --- /dev/null +++ b/indra/test/writestr.h @@ -0,0 +1,39 @@ +/** + * @file writestr.h + * @author Nat Goodspeed + * @date 2024-05-21 + * @brief writestr() function for when iostream isn't set up + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_WRITESTR_H) +#define LL_WRITESTR_H + +#include "stringize.h" + +#ifndef LL_WINDOWS + +#include <unistd.h> + +#else // LL_WINDOWS + +#include <io.h> +inline +int write(int fd, const void* buffer, unsigned int count) +{ + return _write(fd, buffer, count); +} + +#endif // LL_WINDOWS + +template <typename... ARGS> +auto writestr(int fd, ARGS&&... args) +{ + std::string str{ stringize(std::forward<ARGS>(args)..., '\n') }; + return write(fd, str.data(), str.length()); +} + +#endif /* ! defined(LL_WRITESTR_H) */ diff --git a/indra/viewer_components/login/lllogin.cpp b/indra/viewer_components/login/lllogin.cpp index feebecf4cb..bdabab70e0 100644 --- a/indra/viewer_components/login/lllogin.cpp +++ b/indra/viewer_components/login/lllogin.cpp @@ -46,7 +46,9 @@ class LLLogin::Impl { public: Impl(): - mPump("login", true) // Create the module's event pump with a tweaked (unique) name. + // Create the module's event pump, and do not tweak the name. Multiple + // parties depend on this LLEventPump having exactly the name "login". + mPump("login", false) { mValidAuthResponse["status"] = LLSD(); mValidAuthResponse["errorcode"] = LLSD(); diff --git a/indra/viewer_components/login/tests/lllogin_test.cpp b/indra/viewer_components/login/tests/lllogin_test.cpp index 8aea3b37aa..f051f8c67f 100644 --- a/indra/viewer_components/login/tests/lllogin_test.cpp +++ b/indra/viewer_components/login/tests/lllogin_test.cpp @@ -66,7 +66,7 @@ * Helper classes *****************************************************************************/ // This is a listener to receive results from lllogin. -class LoginListener: public LLEventTrackable +class LoginListener { std::string mName; LLSD mLastEvent; @@ -137,7 +137,7 @@ public: } }; -class LLXMLRPCListener: public LLEventTrackable +class LLXMLRPCListener { std::string mName; LLSD mEvent; |