summaryrefslogtreecommitdiff
path: root/indra/newview
diff options
context:
space:
mode:
Diffstat (limited to 'indra/newview')
-rw-r--r--indra/newview/CMakeLists.txt35
-rw-r--r--indra/newview/app_settings/cmd_line.xml28
-rw-r--r--indra/newview/app_settings/settings.xml35
-rw-r--r--indra/newview/llappviewer.cpp60
-rw-r--r--indra/newview/llfilepicker.cpp5
-rw-r--r--indra/newview/llfilepicker.h1
-rw-r--r--indra/newview/llfloaterluadebug.cpp155
-rw-r--r--indra/newview/llfloaterluadebug.h72
-rw-r--r--indra/newview/llfloaterluascripts.cpp131
-rw-r--r--indra/newview/llfloaterluascripts.h54
-rw-r--r--indra/newview/llfloatersettingsdebug.cpp20
-rw-r--r--indra/newview/llfloatersettingsdebug.h4
-rw-r--r--indra/newview/llinventoryfunctions.cpp13
-rw-r--r--indra/newview/llinventoryfunctions.h16
-rw-r--r--indra/newview/llinventorymodel.cpp11
-rw-r--r--indra/newview/llluamanager.cpp509
-rw-r--r--indra/newview/llluamanager.h124
-rw-r--r--indra/newview/llstartup.cpp3
-rw-r--r--indra/newview/lltoolplacer.cpp8
-rw-r--r--indra/newview/lltoolplacer.h8
-rw-r--r--indra/newview/lluilistener.cpp2
-rw-r--r--indra/newview/lluilistener.h3
-rw-r--r--indra/newview/llviewercontrollistener.cpp7
-rw-r--r--indra/newview/llviewerfloaterreg.cpp5
-rw-r--r--indra/newview/llviewermenu.cpp147
-rw-r--r--indra/newview/llviewermenu.h1
-rw-r--r--indra/newview/llviewermenufile.cpp18
-rw-r--r--indra/newview/llviewermenufile.h2
-rw-r--r--indra/newview/scripts/lua/ErrorQueue.lua34
-rw-r--r--indra/newview/scripts/lua/Floater.lua151
-rw-r--r--indra/newview/scripts/lua/LLDebugSettings.lua24
-rw-r--r--indra/newview/scripts/lua/LLFloaterAbout.lua11
-rw-r--r--indra/newview/scripts/lua/LLGesture.lua23
-rw-r--r--indra/newview/scripts/lua/Queue.lua47
-rw-r--r--indra/newview/scripts/lua/UI.lua16
-rw-r--r--indra/newview/scripts/lua/WaitQueue.lua85
-rw-r--r--indra/newview/scripts/lua/coro.lua67
-rw-r--r--indra/newview/scripts/lua/fiber.lua338
-rw-r--r--indra/newview/scripts/lua/inspect.lua371
-rw-r--r--indra/newview/scripts/lua/leap.lua446
-rw-r--r--indra/newview/scripts/lua/luafloater_demo.xml93
-rw-r--r--indra/newview/scripts/lua/luafloater_gesture_list.xml21
-rw-r--r--indra/newview/scripts/lua/printf.lua19
-rw-r--r--indra/newview/scripts/lua/qtest.lua146
-rw-r--r--indra/newview/scripts/lua/startup.lua101
-rw-r--r--indra/newview/scripts/lua/test_LLFloaterAbout.lua6
-rw-r--r--indra/newview/scripts/lua/test_LLGesture.lua26
-rw-r--r--indra/newview/scripts/lua/test_luafloater_demo.lua77
-rw-r--r--indra/newview/scripts/lua/test_luafloater_demo2.lua39
-rw-r--r--indra/newview/scripts/lua/test_luafloater_gesture_list.lua75
-rw-r--r--indra/newview/scripts/lua/test_luafloater_gesture_list2.lua27
-rw-r--r--indra/newview/scripts/lua/testmod.lua2
-rw-r--r--indra/newview/scripts/lua/util.lua44
-rw-r--r--indra/newview/skins/default/xui/en/floater_lua_debug.xml117
-rw-r--r--indra/newview/skins/default/xui/en/floater_lua_scripts.xml36
-rw-r--r--indra/newview/skins/default/xui/en/floater_settings_debug.xml18
-rw-r--r--indra/newview/skins/default/xui/en/menu_lua_scripts.xml19
-rw-r--r--indra/newview/skins/default/xui/en/menu_viewer.xml21
-rw-r--r--indra/newview/tests/llluamanager_test.cpp468
-rwxr-xr-xindra/newview/viewer_manifest.py3
60 files changed, 4321 insertions, 127 deletions
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index a6ae041935..f2d5ed26ee 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -48,6 +48,7 @@ include(VulkanGltf)
include(ZLIBNG)
include(URIPARSER)
include(LLPrimitive)
+include(Lualibs)
if (NOT HAVOK_TPV)
# When using HAVOK_TPV, the library is precompiled, so no need for this
@@ -242,6 +243,8 @@ set(viewer_SOURCE_FILES
llfloaterlandholdings.cpp
llfloaterlinkreplace.cpp
llfloaterloadprefpreset.cpp
+ llfloaterluadebug.cpp
+ llfloaterluascripts.cpp
llfloatermarketplacelistings.cpp
llfloatermap.cpp
llfloatermediasettings.cpp
@@ -371,6 +374,7 @@ set(viewer_SOURCE_FILES
lllogchat.cpp
llloginhandler.cpp
lllogininstance.cpp
+ llluamanager.cpp
llmachineid.cpp
llmanip.cpp
llmaniprotate.cpp
@@ -899,6 +903,8 @@ set(viewer_HEADER_FILES
llfloaterlandholdings.h
llfloaterlinkreplace.h
llfloaterloadprefpreset.h
+ llfloaterluadebug.h
+ llfloaterluascripts.h
llfloatermap.h
llfloatermarketplacelistings.h
llfloatermediasettings.h
@@ -1026,6 +1032,7 @@ set(viewer_HEADER_FILES
lllogchat.h
llloginhandler.h
lllogininstance.h
+ llluamanager.h
llmachineid.h
llmanip.h
llmaniprotate.h
@@ -1804,20 +1811,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
@@ -1846,9 +1839,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)
@@ -1909,14 +1899,15 @@ target_link_libraries(${VIEWER_BINARY_NAME}
llcorehttp
llcommon
llmeshoptimizer
- ll::ndof
lllogin
llprimitive
llappearance
${LLPHYSICSEXTENSIONS_LIBRARIES}
ll::bugsplat
- ll::tracy
ll::icu4c
+ ll::lualibs
+ ll::ndof
+ ll::tracy
)
if( TARGET ll::intel_memops )
@@ -2315,6 +2306,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}"
@@ -2369,4 +2365,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 340334aee8..534d1d594d 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 2f7c256b49..68eb3093be 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>
@@ -5398,6 +5398,28 @@
<key>Value</key>
<string>Monospace</string>
</map>
+ <key>LuaChunk</key>
+ <map>
+ <key>Comment</key>
+ <string>Zero or more Lua chunks to run</string>
+ <key>Persist</key>
+ <integer>0</integer>
+ <key>Type</key>
+ <string>LLSD</string>
+ <key>Value</key>
+ <array />
+ </map>
+ <key>LuaScript</key>
+ <map>
+ <key>Comment</key>
+ <string>Zero or more Lua script files to run</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>
@@ -17339,6 +17361,17 @@
<key>Value</key>
<integer>3</integer>
</map>
+ <key>AutorunLuaScriptName</key>
+ <map>
+ <key>Comment</key>
+ <string>Script name to autorun after login.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>String</string>
+ <key>Value</key>
+ <string>default.lua</string>
+ </map>
<key>ResetUIScaleOnFirstRun</key>
<map>
<key>Comment</key>
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index 0200ea6ad7..6cc2deea80 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -60,6 +60,7 @@
#include "llslurl.h"
#include "llstartup.h"
#include "llfocusmgr.h"
+#include "llluamanager.h"
#include "llurlfloaterdispatchhandler.h"
#include "llviewerjoystick.h"
#include "llallocator.h"
@@ -384,6 +385,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
@@ -1192,22 +1196,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.
@@ -1215,8 +1207,21 @@ 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)
+ {
+ // no completion callback: we don't need to know
+ LLLUAmanager::runScriptFile(script);
+ });
if (gSavedSettings.getBOOL("QAMode") && gSavedSettings.getS32("QAModeEventHostPort") > 0)
{
@@ -1289,6 +1294,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.
diff --git a/indra/newview/llfilepicker.cpp b/indra/newview/llfilepicker.cpp
index 4ad136e13a..68952c96c5 100644
--- a/indra/newview/llfilepicker.cpp
+++ b/indra/newview/llfilepicker.cpp
@@ -64,6 +64,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
@@ -236,6 +237,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 38daff9937..f00f076d1c 100644
--- a/indra/newview/llfilepicker.h
+++ b/indra/newview/llfilepicker.h
@@ -89,6 +89,7 @@ public:
FFLOAD_EXE = 14, // Note: EXE will be treated as ALL on Windows and Linux but not on Darwin
FFLOAD_MATERIAL = 15,
FFLOAD_MATERIAL_TEXTURE = 16,
+ FFLOAD_LUA = 17,
};
enum ESaveFilter
diff --git a/indra/newview/llfloaterluadebug.cpp b/indra/newview/llfloaterluadebug.cpp
new file mode 100644
index 0000000000..6fd11dd1c7
--- /dev/null
+++ b/indra/newview/llfloaterluadebug.cpp
@@ -0,0 +1,155 @@
+/**
+ * @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 "llcheckboxctrl.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"
+
+
+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()
+{
+ mResultOutput->setValue("");
+
+ std::string cmd = mLineInput->getText();
+ cleanLuaState();
+ LLLUAmanager::runScriptLine(mState, 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)
+{
+ mResultOutput->setValue("");
+
+ std::string filepath = filenames[0];
+ if (!filepath.empty())
+ {
+ mScriptPath->setText(filepath);
+ LLLUAmanager::runScriptFile(filepath, [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 == 1)
+ {
+ // single result
+ mResultOutput->pasteTextWithLinebreaks(stringize(result));
+ return;
+ }
+ // 0 or multiple results
+ const char* sep = "";
+ for (const auto& item : llsd::inArray(result))
+ {
+ mResultOutput->insertText(sep);
+ mResultOutput->pasteTextWithLinebreaks(stringize(item));
+ sep = ", ";
+ }
+}
+
+void LLFloaterLUADebug::cleanLuaState()
+{
+ if(getChild<LLCheckBoxCtrl>("clean_lua_state")->get())
+ {
+ //Reinit to clean lua_State
+ mState.initLuaState();
+ }
+}
diff --git a/indra/newview/llfloaterluadebug.h b/indra/newview/llfloaterluadebug.h
new file mode 100644
index 0000000000..7418174570
--- /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);
+ void cleanLuaState();
+
+ LLTempBoundListener mOutConnection;
+
+ LLTextEditor* mResultOutput;
+ LLLineEditor* mLineInput;
+ LLLineEditor* mScriptPath;
+ LuaState mState;
+};
+
+#endif // LL_LLFLOATERLUADEBUG_H
+
diff --git a/indra/newview/llfloaterluascripts.cpp b/indra/newview/llfloaterluascripts.cpp
new file mode 100644
index 0000000000..39d5816b0d
--- /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);
+ }
+ });
+ mCommitCallbackRegistrar.add("Script.Terminate", [this](LLUICtrl*, const LLSD &userdata)
+ {
+ if (mScriptList->hasSelectedItem())
+ {
+ std::string coro_name = mScriptList->getSelectedValue();
+ LLCoros::instance().killreq(coro_name);
+ }
+ });
+}
+
+
+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..932c5c78dd
--- /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/llfloatersettingsdebug.cpp b/indra/newview/llfloatersettingsdebug.cpp
index 3c7f341613..e1df6a4b1f 100644
--- a/indra/newview/llfloatersettingsdebug.cpp
+++ b/indra/newview/llfloatersettingsdebug.cpp
@@ -34,6 +34,7 @@
#include "llcolorswatch.h"
#include "llviewercontrol.h"
#include "lltexteditor.h"
+#include "llclipboard.h"
LLFloaterSettingsDebug::LLFloaterSettingsDebug(const LLSD& key)
@@ -52,6 +53,8 @@ BOOL LLFloaterSettingsDebug::postBuild()
enableResizeCtrls(true, false, true);
mComment = getChild<LLTextEditor>("comment_text");
+ mSettingName = getChild<LLTextBox>("setting_name_txt");
+ mCopyBtn = getChild<LLButton>("copy_btn");
getChild<LLFilterEditor>("filter_input")->setCommitCallback(boost::bind(&LLFloaterSettingsDebug::setSearchFilter, this, _2));
@@ -59,6 +62,8 @@ BOOL LLFloaterSettingsDebug::postBuild()
mSettingList->setCommitOnSelectionChange(TRUE);
mSettingList->setCommitCallback(boost::bind(&LLFloaterSettingsDebug::onSettingSelect, this));
+ mCopyBtn->setCommitCallback([this](LLUICtrl *ctrl, const LLSD &param) { onClickCopy(); });
+
updateList();
gSavedSettings.getControl("DebugSettingsHideDefault")->getCommitSignal()->connect(boost::bind(&LLFloaterSettingsDebug::updateList, this, false));
@@ -203,9 +208,10 @@ void LLFloaterSettingsDebug::updateControl(LLControlVariable* controlp)
//hide combo box only for non booleans, otherwise this will result in the combo box closing every frame
getChildView("boolean_combo")->setVisible( type == TYPE_BOOLEAN);
getChildView("default_btn")->setVisible(true);
- getChildView("setting_name_txt")->setVisible(true);
- getChild<LLTextBox>("setting_name_txt")->setText(controlp->getName());
- getChild<LLTextBox>("setting_name_txt")->setToolTip(controlp->getName());
+ mSettingName->setVisible(true);
+ mSettingName->setText(controlp->getName());
+ mSettingName->setToolTip(controlp->getName());
+ mCopyBtn->setVisible(true);
mComment->setVisible(true);
std::string old_text = mComment->getText();
@@ -632,7 +638,13 @@ void LLFloaterSettingsDebug::hideUIControls()
getChildView("val_text")->setVisible(false);
getChildView("default_btn")->setVisible(false);
getChildView("boolean_combo")->setVisible(false);
- getChildView("setting_name_txt")->setVisible(false);
+ mSettingName->setVisible(false);
+ mCopyBtn->setVisible(false);
mComment->setVisible(false);
}
+void LLFloaterSettingsDebug::onClickCopy()
+{
+ std::string setting_name = mSettingName->getText();
+ LLClipboard::instance().copyToClipboard(utf8str_to_wstring(setting_name), 0, setting_name.size());
+}
diff --git a/indra/newview/llfloatersettingsdebug.h b/indra/newview/llfloatersettingsdebug.h
index 888eaadcbd..6ff3e344b4 100644
--- a/indra/newview/llfloatersettingsdebug.h
+++ b/indra/newview/llfloatersettingsdebug.h
@@ -31,6 +31,7 @@
#include "llfloater.h"
class LLScrollListCtrl;
+class LLTextBox;
class LLFloaterSettingsDebug
: public LLFloater
@@ -46,6 +47,7 @@ public:
void onCommitSettings();
void onClickDefault();
+ void onClickCopy();
bool matchesSearchFilter(std::string setting_name);
bool isSettingHidden(LLControlVariable* control);
@@ -67,6 +69,8 @@ private:
protected:
class LLTextEditor* mComment;
+ LLTextBox* mSettingName;
+ LLButton* mCopyBtn;
std::string mSearchFilter;
};
diff --git a/indra/newview/llinventoryfunctions.cpp b/indra/newview/llinventoryfunctions.cpp
index ea0566f5c4..7d1252ee31 100644
--- a/indra/newview/llinventoryfunctions.cpp
+++ b/indra/newview/llinventoryfunctions.cpp
@@ -2686,6 +2686,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 5a833eab8c..edc83147f2 100644
--- a/indra/newview/llinventoryfunctions.h
+++ b/indra/newview/llinventoryfunctions.h
@@ -390,6 +390,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/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp
index 205e5f3489..3a930684a4 100644
--- a/indra/newview/llinventorymodel.cpp
+++ b/indra/newview/llinventorymodel.cpp
@@ -4835,6 +4835,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/llluamanager.cpp b/indra/newview/llluamanager.cpp
new file mode 100644
index 0000000000..97779a12ad
--- /dev/null
+++ b/indra/newview/llluamanager.cpp
@@ -0,0 +1,509 @@
+/**
+ * @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 "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>
+
+std::map<std::string, std::string> LLLUAmanager::sScriptNames;
+
+lua_function(sleep, "sleep(seconds): pause the running coroutine")
+{
+ F32 seconds = lua_tonumber(L, -1);
+ lua_pop(L, 1);
+ llcoro::suspendUntilTimeout(seconds);
+ lluau::set_interrupts_counter(L, 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.
+ luaL_checkstack(L, 2, nullptr);
+ 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")
+{
+ 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()).")
+{
+ luaL_checkstack(L, 2, nullptr);
+ 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.")
+{
+ luaL_checkstack(L, 2, nullptr);
+ auto listener{ LuaState::obtainListener(L) };
+ const auto& [pump, data]{ listener->getNext() };
+ lua_pushstdstring(L, pump);
+ lua_pushllsd(L, data);
+ lluau::set_interrupts_counter(L, 0);
+ return 2;
+}
+
+LLCoros::Future<std::pair<int, LLSD>>
+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<std::pair<int, LLSD>>>() };
+ runScriptFile(filename,
+ [promise](int count, LLSD result)
+ { promise->set_value({ count, result }); });
+ return LLCoros::getFuture(*promise);
+}
+
+std::pair<int, LLSD> LLLUAmanager::waitScriptFile(const std::string& filename)
+{
+ return startScriptFile(filename).get();
+}
+
+void LLLUAmanager::runScriptFile(const std::string &filename, 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, result_cb, finished_cb]()
+ {
+ ScriptObserver observer(LLCoros::getName(), filename);
+ llifstream in_file;
+ in_file.open(filename.c_str());
+
+ if (in_file.is_open())
+ {
+ // 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);
+ }
+ }
+ });
+}
+
+void LLLUAmanager::runScriptLine(const std::string& chunk, script_finished_fn cb)
+{
+ // A script_finished_fn is used to initialize the LuaState.
+ // It will be called when the LuaState is destroyed.
+ LuaState L(cb);
+ runScriptLine(L, chunk);
+}
+
+void LLLUAmanager::runScriptLine(const std::string& chunk, script_result_fn cb)
+{
+ LuaState L;
+ // A script_result_fn will be called when LuaState::expr() completes.
+ runScriptLine(L, chunk, cb);
+}
+
+LLCoros::Future<std::pair<int, LLSD>>
+LLLUAmanager::startScriptLine(LuaState& L, 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<std::pair<int, LLSD>>>() };
+ runScriptLine(L, chunk,
+ [promise](int count, LLSD result)
+ { promise->set_value({ count, result }); });
+ return LLCoros::getFuture(*promise);
+}
+
+std::pair<int, LLSD> LLLUAmanager::waitScriptLine(LuaState& L, const std::string& chunk)
+{
+ return startScriptLine(L, chunk).get();
+}
+
+void LLLUAmanager::runScriptLine(LuaState& L, const std::string& chunk, script_result_fn 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{ stringize("lua: ", shortchunk) };
+ LLCoros::instance().launch(desc, [&L, desc, chunk, cb]()
+ {
+ auto [count, result] = L.expr(desc, chunk);
+ if (cb)
+ {
+ cb(count, result);
+ }
+ });
+}
+
+void LLLUAmanager::runScriptOnLogin()
+{
+#ifndef LL_TEST
+ std::string filename = gSavedSettings.getString("AutorunLuaScriptName");
+ if (filename.empty())
+ {
+ LL_INFOS() << "Script name wasn't set." << LL_ENDL;
+ return;
+ }
+
+ filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename);
+ if (!gDirUtilp->fileExists(filename))
+ {
+ LL_INFOS() << filename << " was not found." << LL_ENDL;
+ return;
+ }
+
+ runScriptFile(filename);
+#endif // ! LL_TEST
+}
+
+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")
+{
+ 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");
+}
+
+/**
+ * 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;
+};
+
+// 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();
+ }
+
+ std::vector<fsyspath> lib_paths
+ {
+ gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua"),
+#ifdef LL_TEST
+ // Build-time tests don't have the app bundle - use source tree.
+ fsyspath(__FILE__).parent_path() / "scripts" / "lua",
+#endif
+ };
+
+ for (const auto& path : lib_paths)
+ {
+ std::string absolutePathOpt = (path / 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.
+ // Module needs to run in a new thread, isolated from the rest.
+ // Note: we create ML on main thread so that it doesn't inherit environment of L.
+ lua_State *GL = lua_mainthread(L);
+// lua_State *ML = lua_newthread(GL);
+ // Try loading modules on Lua's main thread instead.
+ lua_State *ML = GL;
+ // lua_newthread() pushed the new thread object on GL's stack. Move to L's.
+// lua_xmove(GL, L, 1);
+
+ // new thread needs to have the globals sandboxed
+// luaL_sandboxthread(ML);
+
+ {
+ // 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.
+ if (lluau::loadstring(ML, desc, code) == LUA_OK)
+ {
+ // luau uses Lua 5.3's version of lua_resume():
+ // run the coroutine on ML, "from" L, passing no arguments.
+// int status = lua_resume(ML, L, 0);
+ // we expect one return value
+ int status = lua_pcall(ML, 0, 1, 0);
+
+ if (status == LUA_OK)
+ {
+ if (lua_gettop(ML) == 0)
+ lua_pushfstring(ML, "module %s must return a value", desc.data());
+ else if (!lua_istable(ML, -1) && !lua_isfunction(ML, -1))
+ lua_pushfstring(ML, "module %s must return a table or function, not %s",
+ desc.data(), lua_typename(ML, lua_type(ML, -1)));
+ }
+ else if (status == LUA_YIELD)
+ {
+ lua_pushfstring(ML, "module %s can not yield", desc.data());
+ }
+ else if (!lua_isstring(ML, -1))
+ {
+ lua_pushfstring(ML, "unknown error while running module %s", desc.data());
+ }
+ }
+ }
+ // 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);
+ }
+ // remove ML from L's stack
+// lua_remove(L, -2);
+// // DON'T call lua_close(ML)! Since ML is only a thread of L, corrupts L too!
+// lua_close(ML);
+}
diff --git a/indra/newview/llluamanager.h b/indra/newview/llluamanager.h
new file mode 100644
index 0000000000..af9dcf70c2
--- /dev/null
+++ b/indra/newview/llluamanager.h
@@ -0,0 +1,124 @@
+/**
+ * @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;
+
+ static void runScriptFile(const std::string &filename, 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<std::pair<int, LLSD>>
+ 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 std::pair<int, LLSD> waitScriptFile(const std::string& filename);
+
+ static void runScriptLine(const std::string &chunk, script_finished_fn cb = {});
+ static void runScriptLine(const std::string &chunk, script_result_fn cb);
+ static void runScriptLine(LuaState& L, const std::string &chunk, script_result_fn 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<std::pair<int, LLSD>>
+ startScriptLine(LuaState& L, 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 std::pair<int, LLSD> waitScriptLine(LuaState& L, const std::string& chunk);
+
+ static void runScriptOnLogin();
+
+ static const std::map<std::string, std::string> getScriptNames() { return sScriptNames; }
+
+ 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/llstartup.cpp b/indra/newview/llstartup.cpp
index a0324ca82a..a8ba75ab5f 100644
--- a/indra/newview/llstartup.cpp
+++ b/indra/newview/llstartup.cpp
@@ -208,6 +208,7 @@
#include "llstacktrace.h"
#include "threadpool.h"
+#include "llluamanager.h"
#include "llperfstats.h"
@@ -2419,6 +2420,8 @@ bool idle_startup()
LLPerfStats::StatsRecorder::setAutotuneInit();
+ LLLUAmanager::runScriptOnLogin();
+
return TRUE;
}
diff --git a/indra/newview/lltoolplacer.cpp b/indra/newview/lltoolplacer.cpp
index 7cdd7cc5c8..a4e6d20181 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, &regionp );
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
@@ -194,6 +198,8 @@ BOOL LLToolPlacer::addObject( LLPCode pcode, S32 x, S32 y, U8 use_physics )
U8 material = LL_MCODE_WOOD;
BOOL create_selected = FALSE;
LLVolumeParams volume_params;
+
+ U8 state = 0;
switch (pcode)
{
diff --git a/indra/newview/lltoolplacer.h b/indra/newview/lltoolplacer.h
index ad59cb0daa..d20d682710 100644
--- a/indra/newview/lltoolplacer.h
+++ b/indra/newview/lltoolplacer.h
@@ -50,12 +50,16 @@ 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;
private:
- BOOL addObject( LLPCode pcode, S32 x, S32 y, U8 use_physics );
- BOOL raycastForNewObjPos( S32 x, S32 y, LLViewerObject** hit_obj, S32* hit_face,
+
+ static BOOL raycastForNewObjPos(S32 x, S32 y, LLViewerObject **hit_obj, S32 *hit_face,
BOOL* b_hit_land, LLVector3* ray_start_region, LLVector3* ray_end_region, LLViewerRegion** region );
BOOL addDuplicate(S32 x, S32 y);
};
diff --git a/indra/newview/lluilistener.cpp b/indra/newview/lluilistener.cpp
index 956f5cf187..97a2be6aa3 100644
--- a/indra/newview/lluilistener.cpp
+++ b/indra/newview/lluilistener.cpp
@@ -67,7 +67,7 @@ void LLUIListener::call(const LLSD& event) const
// 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;
+ LL_WARNS("LLUIListener") << "function '" << event["function"] << "' not found" << LL_ENDL;
}
else
{
diff --git a/indra/newview/lluilistener.h b/indra/newview/lluilistener.h
index 08724024dc..e53984dae2 100644
--- a/indra/newview/lluilistener.h
+++ b/indra/newview/lluilistener.h
@@ -39,7 +39,8 @@ class LLUIListener: public LLEventAPI
public:
LLUIListener();
-private:
+// FIXME These fields are intended to be private, changed here to support very hacky code in llluamanager.cpp
+public:
void call(const LLSD& event) const;
void getValue(const LLSD&event) const;
};
diff --git a/indra/newview/llviewercontrollistener.cpp b/indra/newview/llviewercontrollistener.cpp
index 8820f9ec56..16c4084a77 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 1b34bed2da..60add691c5 100644
--- a/indra/newview/llviewerfloaterreg.cpp
+++ b/indra/newview/llviewerfloaterreg.cpp
@@ -94,6 +94,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"
@@ -402,6 +404,9 @@ void LLViewerFloaterReg::registerFloaters()
LLFloaterReg::add("land_holdings", "floater_land_holdings.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterLandHoldings>);
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>);
diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp
index 625d32f59d..52128a09ab 100644
--- a/indra/newview/llviewermenu.cpp
+++ b/indra/newview/llviewermenu.cpp
@@ -173,6 +173,8 @@ extern BOOL gShaderProfileFrame;
// Globals
//
+LLUIListener sUIListener;
+
LLMenuBarGL *gMenuBarView = NULL;
LLViewerMenuHolderGL *gMenuHolder = NULL;
LLMenuGL *gPopupMenuView = NULL;
@@ -351,8 +353,6 @@ public:
static LLMenuParcelObserver* gMenuParcelObserver = NULL;
-static LLUIListener sUIListener;
-
LLMenuParcelObserver::LLMenuParcelObserver()
{
LLViewerParcelMgr::getInstance()->addObserver(this);
@@ -8987,80 +8987,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 handle_env_setting_event(std::string event_name)
+{
+ 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().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");
+ }
+ return true;
+}
+
+class LLWorldEnvSettings : public view_listener_t
+{
bool handleEvent(const LLSD& userdata)
{
- 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().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");
- }
+ handle_env_setting_event(userdata.asString());
return true;
}
diff --git a/indra/newview/llviewermenu.h b/indra/newview/llviewermenu.h
index 7142763451..b43de3fe3f 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 5461e0f362..a1dc7119ce 100644
--- a/indra/newview/llviewermenufile.cpp
+++ b/indra/newview/llviewermenufile.cpp
@@ -840,16 +840,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 6b9df6df28..d41aa23829 100644
--- a/indra/newview/llviewermenufile.h
+++ b/indra/newview/llviewermenufile.h
@@ -72,6 +72,8 @@ void assign_defaults_and_show_upload_message(
const std::string& display_name,
std::string& description);
+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/scripts/lua/ErrorQueue.lua b/indra/newview/scripts/lua/ErrorQueue.lua
new file mode 100644
index 0000000000..6ed1c10d5c
--- /dev/null
+++ b/indra/newview/scripts/lua/ErrorQueue.lua
@@ -0,0 +1,34 @@
+-- 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 dbg = require('printf')
+local function dbg(...) end
+
+local ErrorQueue = WaitQueue:new()
+
+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/Floater.lua b/indra/newview/scripts/lua/Floater.lua
new file mode 100644
index 0000000000..76efd47c43
--- /dev/null
+++ b/indra/newview/scripts/lua/Floater.lua
@@ -0,0 +1,151 @@
+-- Floater base class
+
+local leap = require 'leap'
+local fiber = require 'fiber'
+
+-- 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
+
+function Floater:show()
+ local event = leap.request('LLFloaterReg', self._command)
+ self._pump = event.command_name
+ -- we use the returned reqid to claim subsequent unsolicited events
+ local 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
+
+ local waitfor = leap.WaitFor:new(-1, self.name)
+ function waitfor:filter(pump, data)
+ if data.reqid == reqid then
+ return data
+ end
+ end
+
+ fiber.launch(
+ self.name,
+ function ()
+ event = waitfor:wait()
+ while event and self:handleEvents(event) do
+ event = waitfor:wait()
+ end
+ 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 show().
+ 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/LLDebugSettings.lua b/indra/newview/scripts/lua/LLDebugSettings.lua
new file mode 100644
index 0000000000..c809dfff91
--- /dev/null
+++ b/indra/newview/scripts/lua/LLDebugSettings.lua
@@ -0,0 +1,24 @@
+leap = require 'leap'
+
+function check_response(res)
+ if res.error then
+ error(res.error)
+ end
+ return res
+end
+
+local LLDebugSettings = {}
+
+function LLDebugSettings.set(name, value)
+ check_response(leap.request('LLViewerControl', {op='set', group='Global', key=name, value=value}))
+end
+
+function LLDebugSettings.toggle(name)
+ check_response(leap.request('LLViewerControl', {op='toggle', group='Global', key=name}))
+end
+
+function LLDebugSettings.get(name)
+ return check_response(leap.request('LLViewerControl', {op='get', group='Global', key=name}))['value']
+end
+
+return LLDebugSettings
diff --git a/indra/newview/scripts/lua/LLFloaterAbout.lua b/indra/newview/scripts/lua/LLFloaterAbout.lua
new file mode 100644
index 0000000000..44afee2e5c
--- /dev/null
+++ b/indra/newview/scripts/lua/LLFloaterAbout.lua
@@ -0,0 +1,11 @@
+-- Engage the LLFloaterAbout LLEventAPI
+
+leap = require 'leap'
+
+local LLFloaterAbout = {}
+
+function LLFloaterAbout.getInfo()
+ return leap.request('LLFloaterAbout', {op='getInfo'})
+end
+
+return LLFloaterAbout
diff --git a/indra/newview/scripts/lua/LLGesture.lua b/indra/newview/scripts/lua/LLGesture.lua
new file mode 100644
index 0000000000..cb410446d7
--- /dev/null
+++ b/indra/newview/scripts/lua/LLGesture.lua
@@ -0,0 +1,23 @@
+-- Engage the LLGesture LLEventAPI
+
+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/Queue.lua b/indra/newview/scripts/lua/Queue.lua
new file mode 100644
index 0000000000..5ab2a8a72c
--- /dev/null
+++ b/indra/newview/scripts/lua/Queue.lua
@@ -0,0 +1,47 @@
+-- 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 Queue = {}
+
+function Queue:new()
+ local obj = setmetatable({}, self)
+ self.__index = self
+
+ obj._first = 0
+ obj._last = -1
+ obj._queue = {}
+
+ return obj
+end
+
+-- 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/UI.lua b/indra/newview/scripts/lua/UI.lua
new file mode 100644
index 0000000000..f851632bad
--- /dev/null
+++ b/indra/newview/scripts/lua/UI.lua
@@ -0,0 +1,16 @@
+-- Engage the UI LLEventAPI
+
+leap = require 'leap'
+
+local UI = {}
+
+function UI.call(func, parameter)
+ -- 'call' is fire-and-forget
+ leap.send('UI', {op='call', ['function']=func, parameter=parameter})
+end
+
+function UI.getValue(path)
+ return leap.request('UI', {op='getValue', path=path})['value']
+end
+
+return UI
diff --git a/indra/newview/scripts/lua/WaitQueue.lua b/indra/newview/scripts/lua/WaitQueue.lua
new file mode 100644
index 0000000000..ad4fdecf43
--- /dev/null
+++ b/indra/newview/scripts/lua/WaitQueue.lua
@@ -0,0 +1,85 @@
+-- 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 dbg = require('printf')
+local function dbg(...) end
+
+local WaitQueue = Queue:new()
+
+function WaitQueue:new()
+ local obj = Queue:new()
+ setmetatable(obj, self)
+ self.__index = self
+
+ obj._waiters = {}
+ obj._closed = false
+ return obj
+end
+
+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/coro.lua b/indra/newview/scripts/lua/coro.lua
new file mode 100644
index 0000000000..616a797e95
--- /dev/null
+++ b/indra/newview/scripts/lua/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/fiber.lua b/indra/newview/scripts/lua/fiber.lua
new file mode 100644
index 0000000000..9057e6c890
--- /dev/null
+++ b/indra/newview/scripts/lua/fiber.lua
@@ -0,0 +1,338 @@
+-- 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 dbg = printf
+local function dbg(...) end
+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.
+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
+
+return fiber
diff --git a/indra/newview/scripts/lua/inspect.lua b/indra/newview/scripts/lua/inspect.lua
new file mode 100644
index 0000000000..9900a0b81b
--- /dev/null
+++ b/indra/newview/scripts/lua/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/leap.lua b/indra/newview/scripts/lua/leap.lua
new file mode 100644
index 0000000000..ade91789f0
--- /dev/null
+++ b/indra/newview/scripts/lua/leap.lua
@@ -0,0 +1,446 @@
+-- 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 function dbg(...) end
+-- local dbg = require('printf')
+
+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.
+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'}
+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:new(reqid)
+ pending[reqid] = waitfor
+ -- Pass reqid to send() to stamp it into (a copy of) the request data.
+ dbg('requestSetup(%s, %s)', pump, data)
+ 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 ok then
+ return response
+ else
+ error(response)
+ end
+end
+
+-- Send the specified request LLSD, expecting an arbitrary number of replies.
+-- Each one is yielded on receipt. If you omit checklast, this is an infinite
+-- generator; it's up to the caller to recognize when the last reply has been
+-- received, and stop resuming for more.
+--
+-- If you pass checklast=<callable accepting(event)>, each response event is
+-- passed to that callable (after the yield). When the callable returns
+-- True, the generator terminates in the usual way.
+--
+-- See request() remarks about ["reqid"].
+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)
+ local ok, response, resumed_with
+ repeat
+ ok, response = pcall(waitfor.wait, waitfor)
+ if not ok then
+ break
+ end
+ -- can resume(false) to terminate generate() and clean up
+ resumed_with = coroutine.yield(response)
+ until (checklast and checklast(response)) or (resumed_with == false)
+ -- If we break the above loop, whether or not due to error, clean up.
+ pending[reqid] = nil
+ if not ok then
+ error(response)
+ end
+end
+
+local function cleanup(message)
+ -- we're done: clean up all pending coroutines
+ for i, waitfor in pairs(pending) do
+ waitfor:close()
+ end
+ for i, waitfor in pairs(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, 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
+ return unsolicited(pump, data)
+ end
+ -- have reqid; do we have a WaitForReqid?
+ local waitfor = pending[reqid]
+ if waitfor == nil then
+ return unsolicited(pump, data)
+ end
+ -- found the right WaitForReqid object, let it handle the event
+ 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)
+ for i, w in pairs(waitfors) do
+ if w == waitfor then
+ waitfors[i] = nil
+ break
+ end
+ 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:new()
+ obj._registered = false
+ -- if no priority, then don't enable() - remember 0 is truthy
+ if priority then
+ obj:enable()
+ end
+
+ return obj
+end
+
+-- 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._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:new()
+
+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:new(nil, 'WaitForReqid(' .. reqid .. ')')
+ setmetatable(obj, self)
+ self.__index = self
+
+ return obj
+end
+
+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
+
+return leap
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/printf.lua b/indra/newview/scripts/lua/printf.lua
new file mode 100644
index 0000000000..584cd4f391
--- /dev/null
+++ b/indra/newview/scripts/lua/printf.lua
@@ -0,0 +1,19 @@
+-- printf(...) is short for print(string.format(...))
+
+local inspect = require 'inspect'
+
+local function printf(...)
+ -- 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(table.unpack(args)))
+end
+
+return printf
diff --git a/indra/newview/scripts/lua/qtest.lua b/indra/newview/scripts/lua/qtest.lua
new file mode 100644
index 0000000000..009446d0c3
--- /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:new()
+q2 = Queue:new()
+
+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:new()
+q2 = WaitQueue:new()
+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:new()
+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:new()
+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/startup.lua b/indra/newview/scripts/lua/startup.lua
new file mode 100644
index 0000000000..4311bb9a60
--- /dev/null
+++ b/indra/newview/scripts/lua/startup.lua
@@ -0,0 +1,101 @@
+-- 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'
+
+-- ---------------------------------------------------------------------------
+-- 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 = 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:new(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'})
+
+-- ---------------------------------------------------------------------------
+startup = {}
+
+-- 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/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_luafloater_demo.lua b/indra/newview/scripts/lua/test_luafloater_demo.lua
new file mode 100644
index 0000000000..ab638dcdd1
--- /dev/null
+++ b/indra/newview/scripts/lua/test_luafloater_demo.lua
@@ -0,0 +1,77 @@
+XML_FILE_PATH = LL.abspath("luafloater_demo.xml")
+
+scriptparts = string.split(LL.source_path(), '/')
+scriptname = scriptparts[#scriptparts]
+print('Running ' .. scriptname)
+
+leap = require 'leap'
+fiber = require 'fiber'
+startup = require 'startup'
+
+--event pump for sending actions to the floater
+local COMMAND_PUMP_NAME = ""
+local reqid
+--table of floater UI events
+event_list=leap.request("LLFloaterReg", {op="getFloaterEvents"}).events
+
+local function _event(event_name)
+ if not table.find(event_list, event_name) then
+ LL.print_warning("Incorrect event name: " .. event_name)
+ end
+ return event_name
+end
+
+function post(action)
+ leap.send(COMMAND_PUMP_NAME, action)
+end
+
+function getCurrentTime()
+ local currentTime = os.date("*t")
+ return string.format("%02d:%02d:%02d", currentTime.hour, currentTime.min, currentTime.sec)
+end
+
+function handleEvents(event_data)
+ post({action="add_text", ctrl_name="events_editor", value = event_data})
+ if event_data.event == _event("commit") then
+ if event_data.ctrl_name == "disable_ctrl" then
+ post({action="set_enabled", ctrl_name="open_btn", value = (1 - event_data.value)})
+ elseif event_data.ctrl_name == "title_cmb" then
+ post({action="set_title", value= event_data.value})
+ elseif event_data.ctrl_name == "open_btn" then
+ floater_name = leap.request(COMMAND_PUMP_NAME, {action="get_value", ctrl_name='openfloater_cmd'})['value']
+ leap.send("LLFloaterReg", {name = floater_name, op = "showInstance"})
+ end
+ elseif event_data.event == _event("double_click") then
+ if event_data.ctrl_name == "show_time_lbl" then
+ post({action="set_value", ctrl_name="time_lbl", value= getCurrentTime()})
+ end
+ elseif event_data.event == _event("floater_close") then
+ LL.print_warning("Floater was closed")
+ return false
+ end
+ return true
+end
+
+startup.wait('STATE_LOGIN_WAIT')
+local key = {xml_path = XML_FILE_PATH, op = "showLuaFloater"}
+--sign for additional events for defined control {<control_name>= {action1, action2, ...}}
+key.extra_events={show_time_lbl = {_event("right_mouse_down"), _event("double_click")}}
+local resp = leap.request("LLFloaterReg", key)
+COMMAND_PUMP_NAME = resp.command_name
+reqid = resp.reqid
+
+catch_events = leap.WaitFor:new(-1, "all_events")
+function catch_events:filter(pump, data)
+ if data.reqid == reqid then
+ return data
+ end
+end
+
+function process_events(waitfor)
+ event_data = waitfor:wait()
+ while event_data and handleEvents(event_data) do
+ event_data = waitfor:wait()
+ end
+end
+
+fiber.launch("catch_events", process_events, catch_events)
diff --git a/indra/newview/scripts/lua/test_luafloater_demo2.lua b/indra/newview/scripts/lua/test_luafloater_demo2.lua
new file mode 100644
index 0000000000..9e24237d28
--- /dev/null
+++ b/indra/newview/scripts/lua/test_luafloater_demo2.lua
@@ -0,0 +1,39 @@
+local Floater = require 'Floater'
+local leap = require 'leap'
+local startup = require 'startup'
+
+local flt = Floater:new(
+ '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 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..3d9a9b0ad4
--- /dev/null
+++ b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua
@@ -0,0 +1,75 @@
+XML_FILE_PATH = LL.abspath("luafloater_gesture_list.xml")
+
+scriptparts = string.split(LL.source_path(), '/')
+scriptname = scriptparts[#scriptparts]
+print('Running ' .. scriptname)
+
+leap = require 'leap'
+fiber = require 'fiber'
+LLGesture = require 'LLGesture'
+startup = require 'startup'
+
+--event pump for sending actions to the floater
+local COMMAND_PUMP_NAME = ""
+local reqid
+--table of floater UI events
+event_list=leap.request("LLFloaterReg", {op="getFloaterEvents"}).events
+
+local function _event(event_name)
+ if not table.find(event_list, event_name) then
+ LL.print_warning("Incorrect event name: " .. event_name)
+ end
+ return event_name
+end
+
+function post(action)
+ leap.send(COMMAND_PUMP_NAME, action)
+end
+
+function handleEvents(event_data)
+ if event_data.event == _event("floater_close") then
+ return false
+ end
+
+ if event_data.event == _event("post_build") then
+ COMMAND_PUMP_NAME = event_data.command_name
+ reqid = event_data.reqid
+ gestures_uuid = LLGesture.getActiveGestures()
+ local action_data = {}
+ action_data.action = "add_list_element"
+ action_data.ctrl_name = "gesture_list"
+ 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
+ post(action_data)
+ elseif event_data.event == _event("double_click") then
+ if event_data.ctrl_name == "gesture_list" then
+ LLGesture.startGesture(event_data.value)
+ end
+ end
+ return true
+end
+
+startup.wait('STATE_STARTED')
+local key = {xml_path = XML_FILE_PATH, op = "showLuaFloater"}
+--receive additional events for defined control {<control_name>= {action1, action2, ...}}
+key.extra_events={gesture_list = {_event("double_click")}}
+handleEvents(leap.request("LLFloaterReg", key))
+
+catch_events = leap.WaitFor:new(-1, "all_events")
+function catch_events:filter(pump, data)
+ if data.reqid == reqid then
+ return data
+ end
+end
+
+function process_events(waitfor)
+ event_data = waitfor:wait()
+ while event_data and handleEvents(event_data) do
+ event_data = waitfor:wait()
+ end
+end
+
+fiber.launch("catch_events", process_events, catch_events)
diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua
new file mode 100644
index 0000000000..d702d09c51
--- /dev/null
+++ b/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua
@@ -0,0 +1,27 @@
+local Floater = require 'Floater'
+local LLGesture = require 'LLGesture'
+local startup = require 'startup'
+
+local flt = Floater:new(
+ "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/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/scripts/lua/util.lua b/indra/newview/scripts/lua/util.lua
new file mode 100644
index 0000000000..a2191288f6
--- /dev/null
+++ b/indra/newview/scripts/lua/util.lua
@@ -0,0 +1,44 @@
+-- utility functions, in alpha order
+
+local util = {}
+
+-- 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
+
+return util
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..a028a1802c
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/floater_lua_debug.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<floater
+ can_minimize="false"
+ can_resize="false"
+ 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|bottom"
+ font="SansSerif"
+ height="30"
+ layout="topleft"
+ left="10"
+ left_delta="10"
+ name="editor_path_label"
+ top="10"
+ width="100">
+ LUA string:
+ </text>
+ <check_box
+ follows="left|bottom"
+ height="15"
+ label="Use clean lua_State"
+ layout="topleft"
+ top="10"
+ right ="-70"
+ name="clean_lua_state"
+ width="70"/>
+ <line_editor
+ border_style="line"
+ border_thickness="1"
+ follows="left|bottom"
+ 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="left|bottom"
+ 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="left|top"
+ 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"
+ 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="left|bottom"
+ height="25"
+ label="Browse..."
+ label_selected="Browse..."
+ layout="topleft"
+ left_pad="5"
+ name="browse_btn"
+ top_delta="-2"
+ width="70" />
+ <button
+ follows="left|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..6fd8f2b255 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,8 +65,7 @@
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"
@@ -67,6 +80,7 @@
name="comment_text"
follows="left|top"
width="240"
+ left="320"
top_delta="20"
word_wrap="true" />
<radio_group
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/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml
index 53f703e56d..5ae97b729e 100644
--- a/indra/newview/skins/default/xui/en/menu_viewer.xml
+++ b/indra/newview/skins/default/xui/en/menu_viewer.xml
@@ -2537,6 +2537,27 @@ function="World.EnvPreset"
parameter="scene monitor" />
</menu_item_check>
<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>
+ <menu_item_separator/>
<menu_item_call
label="Region Info to Debug Console"
diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp
new file mode 100644
index 0000000000..cf1bf25b5c
--- /dev/null
+++ b/indra/newview/tests/llluamanager_test.cpp
@@ -0,0 +1,468 @@
+/**
+ * @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 "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; }
+};
+
+template <typename CALLABLE>
+auto listener(CALLABLE&& callable)
+{
+ return [callable=std::forward<CALLABLE>(callable)]
+ (const LLSD& data)
+ {
+ callable(data);
+ return false;
+ };
+}
+
+/*****************************************************************************
+* TUT
+*****************************************************************************/
+namespace tut
+{
+ struct llluamanager_data
+ {
+ // 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");
+ LuaState L;
+ for (auto& luax : lua_expressions)
+ {
+ auto [count, result] =
+ LLLUAmanager::waitScriptLine(L, "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",
+ listener([&fromlua](const LLSD& data){ fromlua = data; }));
+ const std::string lua(stringize(
+ "data = ", construct, "\n"
+ "LL.post_on('testpump', data)\n"
+ ));
+ LuaState L;
+ auto [count, result] = LLLUAmanager::waitScriptLine(L, 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",
+ listener([&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"
+ );
+ LuaState L;
+ // 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(L, 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"
+ );
+ LuaState L;
+ auto future = LLLUAmanager::startScriptLine(L, 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(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",
+ listener([](const LLSD& data)
+ {
+ LL_DEBUGS("Lua") << "echo pump got: " << data << LL_ENDL;
+ sendReply(data, data);
+ }));
+
+ LuaState L;
+ auto [count, result] = LLLUAmanager::waitScriptLine(L, 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"
+ "-- debug = require('printf')\n"
+ "local function debug(...) end\n"
+ "\n"
+ "-- negative priority ensures catchall is always last\n"
+ "catchall = leap.WaitFor:new(-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:new(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"
+ );
+
+ LLSD requests;
+ LLStreamListener pump(
+ "testpump",
+ listener([&requests](const LLSD& data)
+ {
+ LL_DEBUGS("Lua") << "testpump got: " << data << LL_ENDL;
+ requests.append(data);
+ }));
+
+ LuaState L;
+ auto future = LLLUAmanager::startScriptLine(L, lua);
+ auto replyname{ L.obtainListener()->getReplyName() };
+ auto& replypump{ LLEventPumps::instance().obtain(replyname) };
+ // 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);
+ // 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"
+ );
+ LuaState L;
+ auto future = LLLUAmanager::startScriptLine(L, 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_equals("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"
+ );
+ LuaState L;
+ auto [count, result] = LLLUAmanager::waitScriptLine(L, 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");
+ }
+} // namespace tut
diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py
index 4de4dc8fc5..b60e349e90 100755
--- a/indra/newview/viewer_manifest.py
+++ b/indra/newview/viewer_manifest.py
@@ -166,6 +166,9 @@ class ViewerManifest(LLManifest):
self.path("*/*/*/*.js")
self.path("*/*/*.html")
+ with self.prefix(src_dst="scripts/lua"):
+ 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']),