summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--autobuild.xml42
-rw-r--r--indra/llcommon/CMakeLists.txt4
-rw-r--r--indra/llcommon/coro_scheduler.cpp164
-rw-r--r--indra/llcommon/coro_scheduler.h73
-rw-r--r--indra/llcommon/llcallbacklist.cpp2
-rw-r--r--indra/llcommon/throttle.cpp34
-rw-r--r--indra/llcommon/throttle.h138
-rw-r--r--indra/newview/groupchatlistener.cpp97
-rw-r--r--indra/newview/groupchatlistener.h23
-rw-r--r--indra/newview/llappviewer.cpp4
-rw-r--r--indra/newview/llgroupactions.cpp8
-rw-r--r--indra/newview/llstartup.cpp2
-rw-r--r--indra/newview/lltoastimpanel.cpp5
-rw-r--r--indra/newview/scripts/lua/require/LLAgent.lua11
-rw-r--r--indra/newview/scripts/lua/require/LLChat.lua25
-rw-r--r--indra/newview/scripts/lua/test_flycam.lua38
-rw-r--r--indra/newview/scripts/lua/test_group_chat.lua15
17 files changed, 620 insertions, 65 deletions
diff --git a/autobuild.xml b/autobuild.xml
index bc5c979c95..00f9fb2470 100644
--- a/autobuild.xml
+++ b/autobuild.xml
@@ -1709,11 +1709,11 @@
<key>archive</key>
<map>
<key>hash</key>
- <string>59bf3d96f9df4b6981c406abac5c46ae276f9b15</string>
+ <string>e48f291e2eeb1dbab39f26db5ac9b4c2b2d1c57d</string>
<key>hash_algorithm</key>
<string>sha1</string>
<key>url</key>
- <string>https://github.com/secondlife/3p-luau/releases/download/v0.609-9567db9/luau-0.609-9567db9-darwin64-9567db9.tar.zst</string>
+ <string>https://github.com/secondlife/3p-luau/releases/download/v0.638-r2/luau-0.638.0-r2-darwin64-10356321602.tar.zst</string>
</map>
<key>name</key>
<string>darwin64</string>
@@ -1723,11 +1723,11 @@
<key>archive</key>
<map>
<key>hash</key>
- <string>549516ada483ddf276183f66b0a7c3d01e4ef1aa</string>
+ <string>3a797d8dae685b25ca787111a370b99d9e57d77c</string>
<key>hash_algorithm</key>
<string>sha1</string>
<key>url</key>
- <string>https://github.com/secondlife/3p-luau/releases/download/v0.609-9567db9/luau-0.609-9567db9-linux64-9567db9.tar.zst</string>
+ <string>https://github.com/secondlife/3p-luau/releases/download/v0.638-r2/luau-0.638.0-r2-linux64-10356321602.tar.zst</string>
</map>
<key>name</key>
<string>linux64</string>
@@ -1737,11 +1737,11 @@
<key>archive</key>
<map>
<key>hash</key>
- <string>43e2cc2e6e94299f89655435002864925b640e16</string>
+ <string>a8857313496622134b00899141bbe6542f754164</string>
<key>hash_algorithm</key>
<string>sha1</string>
<key>url</key>
- <string>https://github.com/secondlife/3p-luau/releases/download/v0.609-9567db9/luau-0.609-9567db9-windows64-9567db9.tar.zst</string>
+ <string>https://github.com/secondlife/3p-luau/releases/download/v0.638-r2/luau-0.638.0-r2-windows64-10356321602.tar.zst</string>
</map>
<key>name</key>
<string>windows64</string>
@@ -1754,7 +1754,7 @@
<key>copyright</key>
<string>Copyright (c) 2019-2023 Roblox Corporation, Copyright (c) 1994–2019 Lua.org, PUC-Rio.</string>
<key>version</key>
- <string>0.609-9567db9</string>
+ <string>0.638.0-r2</string>
<key>name</key>
<string>luau</string>
<key>description</key>
@@ -1836,18 +1836,6 @@
</map>
<key>mikktspace</key>
<map>
- <key>canonical_repo</key>
- <string>https://bitbucket.org/lindenlab/3p-mikktspace</string>
- <key>copyright</key>
- <string>Copyright (C) 2011 by Morten S. Mikkelsen, Copyright (C) 2022 Blender Authors</string>
- <key>description</key>
- <string>Mikktspace Tangent Generator</string>
- <key>license</key>
- <string>Apache 2.0</string>
- <key>license_file</key>
- <string>mikktspace.txt</string>
- <key>name</key>
- <string>mikktspace</string>
<key>platforms</key>
<map>
<key>darwin64</key>
@@ -1893,8 +1881,20 @@
<string>windows64</string>
</map>
</map>
+ <key>license</key>
+ <string>Apache 2.0</string>
+ <key>license_file</key>
+ <string>mikktspace.txt</string>
+ <key>copyright</key>
+ <string>Copyright (C) 2011 by Morten S. Mikkelsen, Copyright (C) 2022 Blender Authors</string>
<key>version</key>
<string>1</string>
+ <key>name</key>
+ <string>mikktspace</string>
+ <key>canonical_repo</key>
+ <string>https://bitbucket.org/lindenlab/3p-mikktspace</string>
+ <key>description</key>
+ <string>Mikktspace Tangent Generator</string>
</map>
<key>minizip-ng</key>
<map>
@@ -2811,6 +2811,8 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<string>LICENSE</string>
<key>copyright</key>
<string>Copyright (c) 2000-2012, Linden Research, Inc.</string>
+ <key>version</key>
+ <string>3.0-f14b5ec</string>
<key>name</key>
<string>viewer-manager</string>
<key>description</key>
@@ -2819,8 +2821,6 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<string>https://bitbucket.org/lindenlab/vmp-standalone</string>
<key>source_type</key>
<string>hg</string>
- <key>version</key>
- <string>3.0-f14b5ec</string>
</map>
<key>vlc-bin</key>
<map>
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index 20670d7ebe..8472eac9f6 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -18,6 +18,7 @@ include(Tracy)
set(llcommon_SOURCE_FILES
apply.cpp
commoncontrol.cpp
+ coro_scheduler.cpp
hbxxh.cpp
indra_constants.cpp
lazyeventapi.cpp
@@ -112,6 +113,7 @@ set(llcommon_SOURCE_FILES
lua_function.cpp
lualistener.cpp
threadpool.cpp
+ throttle.cpp
u64.cpp
workqueue.cpp
StackWalker.cpp
@@ -125,6 +127,7 @@ set(llcommon_HEADER_FILES
chrono.h
classic_callback.h
commoncontrol.h
+ coro_scheduler.h
ctype_workaround.h
fix_macros.h
fsyspath.h
@@ -263,6 +266,7 @@ set(llcommon_HEADER_FILES
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..337162cbd5
--- /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 F32 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.
+ F32 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..a7572ccf4d
--- /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 F32 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.
+ F32 mTimeslice{ DEFAULT_TIMESLICE };
+ // Timestamp as of the last time we suspended the main fiber.
+ F32 mMainLast{ 0 };
+ // Timestamp of start time
+ F32 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/llcallbacklist.cpp b/indra/llcommon/llcallbacklist.cpp
index 555c793333..647b268b8b 100644
--- a/indra/llcommon/llcallbacklist.cpp
+++ b/indra/llcommon/llcallbacklist.cpp
@@ -391,7 +391,7 @@ public:
TimersListener(const LazyEventAPIParams& params): LLEventAPI(params) {}
// Forbid a script from requesting callbacks too quickly.
- static constexpr LLSD::Real MINTIMER{ 1.0 };
+ static constexpr LLSD::Real MINTIMER{ 0.010 };
void scheduleAfter(const LLSD& params);
void scheduleEvery(const LLSD& params);
diff --git a/indra/llcommon/throttle.cpp b/indra/llcommon/throttle.cpp
new file mode 100644
index 0000000000..d5f7d5dcab
--- /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..481d1d8f72
--- /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/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/llappviewer.cpp b/indra/newview/llappviewer.cpp
index ff29f64aeb..c259275d8f 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"
@@ -765,7 +766,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
diff --git a/indra/newview/llgroupactions.cpp b/indra/newview/llgroupactions.cpp
index 24ae90e3ae..e901631d94 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/llstartup.cpp b/indra/newview/llstartup.cpp
index 3cf0def66e..d49e0d9ba2 100644
--- a/indra/newview/llstartup.cpp
+++ b/indra/newview/llstartup.cpp
@@ -400,10 +400,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/lltoastimpanel.cpp b/indra/newview/lltoastimpanel.cpp
index f7e2d49e13..b7353d6960 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/scripts/lua/require/LLAgent.lua b/indra/newview/scripts/lua/require/LLAgent.lua
index bc9a6b23a0..5ee092f2f6 100644
--- a/indra/newview/scripts/lua/require/LLAgent.lua
+++ b/indra/newview/scripts/lua/require/LLAgent.lua
@@ -11,6 +11,17 @@ 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
diff --git a/indra/newview/scripts/lua/require/LLChat.lua b/indra/newview/scripts/lua/require/LLChat.lua
index 78dca765e8..bc0fc86d22 100644
--- a/indra/newview/scripts/lua/require/LLChat.lua
+++ b/indra/newview/scripts/lua/require/LLChat.lua
@@ -2,8 +2,13 @@ local leap = require 'leap'
local LLChat = {}
-function LLChat.sendNearby(msg)
- leap.send('LLChatBar', {op='sendChat', message=msg})
+-- ***************************************************************************
+-- 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)
@@ -14,4 +19,20 @@ 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/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..eaff07ed14
--- /dev/null
+++ b/indra/newview/scripts/lua/test_group_chat.lua
@@ -0,0 +1,15 @@
+LLChat = require 'LLChat'
+LLAgent = require 'LLAgent'
+popup = require 'popup'
+
+local OK = 'OK_okcancelbuttons'
+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 = popup:alertYesCancel('Started group chat with ' .. group_info.name .. ' group. Send greetings?')
+if next(response) == OK then
+ LLChat.sendGroupIM('Greetings', group_info.id)
+end