diff options
author | Maxim Nikolenko <mnikolenko@productengine.com> | 2024-01-31 18:43:23 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-31 18:43:23 +0200 |
commit | eb0c19b2746890eb4dbfd6eac045699d0e5842dd (patch) | |
tree | 12b5789d97efbdc6ebb259ffc83461da38a442f3 /indra/newview | |
parent | e2ba2f9c453d4e913b0deb5dfee2a07c55034655 (diff) | |
parent | ab1f2c2f6f9b854b95db3733fd6ff6d02e677ebd (diff) |
Merge branch DRTVWR-589-luau into luau-scripting
LuaU scripting project
Diffstat (limited to 'indra/newview')
24 files changed, 1948 insertions, 97 deletions
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 7a70d0b6e6..ba2f252593 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -47,6 +47,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 @@ -239,6 +240,7 @@ set(viewer_SOURCE_FILES llfloaterlandholdings.cpp llfloaterlinkreplace.cpp llfloaterloadprefpreset.cpp + llfloaterluadebug.cpp llfloatermarketplacelistings.cpp llfloatermap.cpp llfloatermediasettings.cpp @@ -368,6 +370,7 @@ set(viewer_SOURCE_FILES lllogchat.cpp llloginhandler.cpp lllogininstance.cpp + llluamanager.cpp llmachineid.cpp llmanip.cpp llmaniprotate.cpp @@ -893,6 +896,7 @@ set(viewer_HEADER_FILES llfloaterlandholdings.h llfloaterlinkreplace.h llfloaterloadprefpreset.h + llfloaterluadebug.h llfloatermap.h llfloatermarketplacelistings.h llfloatermediasettings.h @@ -1020,6 +1024,7 @@ set(viewer_HEADER_FILES lllogchat.h llloginhandler.h lllogininstance.h + llluamanager.h llmachineid.h llmanip.h llmaniprotate.h @@ -1815,20 +1820,6 @@ if (WINDOWS) if (PACKAGE) add_custom_command( - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/event_host.tar.bz2 - 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 @@ -1857,9 +1848,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.bz2) endif (PACKAGE) elseif (DARWIN) @@ -1927,6 +1915,7 @@ target_link_libraries(${VIEWER_BINARY_NAME} ${LLPHYSICSEXTENSIONS_LIBRARIES} ll::bugsplat ll::tracy + ll::lualibs ) if( TARGET ll::intel_memops ) @@ -2303,6 +2292,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}" diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 00b59f9a4d..5e4cd4e57b 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -17361,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/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..58639490db --- /dev/null +++ b/indra/newview/llfloaterluadebug.cpp @@ -0,0 +1,113 @@ +/** + * @file llfloaterluadebug.cpp + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llfloaterluadebug.h" + +#include "lllineeditor.h" +#include "lltexteditor.h" +#include "llviewermenufile.h" // LLFilePickerReplyThread + +#include "llagent.h" +#include "llappearancemgr.h" +#include "llfloaterreg.h" +#include "llfloaterimnearbychat.h" + +#include "llluamanager.h" + + +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->insertText(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)); + + return TRUE; +} + +LLFloaterLUADebug::~LLFloaterLUADebug() +{} + +void LLFloaterLUADebug::onExecuteClicked() +{ + mResultOutput->setValue(""); + + std::string cmd = mLineInput->getText(); + LLLUAmanager::runScriptLine(cmd, [this](std::string msg) + { + mResultOutput->insertText(msg); + }); +} + +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](std::string msg) + { + mResultOutput->insertText(msg); + }); + } +} + diff --git a/indra/newview/llfloaterluadebug.h b/indra/newview/llfloaterluadebug.h new file mode 100644 index 0000000000..1e26828c83 --- /dev/null +++ b/indra/newview/llfloaterluadebug.h @@ -0,0 +1,67 @@ +/** + * @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" + +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: + LLTempBoundListener mOutConnection; + + LLTextEditor* mResultOutput; + LLLineEditor* mLineInput; + LLLineEditor* mScriptPath; +}; + +#endif // LL_LLFLOATERLUADEBUG_H + diff --git a/indra/newview/llinventoryfunctions.cpp b/indra/newview/llinventoryfunctions.cpp index 4aeacae6ed..56e680a602 100644 --- a/indra/newview/llinventoryfunctions.cpp +++ b/indra/newview/llinventoryfunctions.cpp @@ -2645,6 +2645,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 925217dda3..d63d4179cf 100644 --- a/indra/newview/llinventoryfunctions.h +++ b/indra/newview/llinventoryfunctions.h @@ -367,6 +367,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 05aa2e423f..1220f1c45e 100644 --- a/indra/newview/llinventorymodel.cpp +++ b/indra/newview/llinventorymodel.cpp @@ -4829,6 +4829,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..3e3ce45cb0 --- /dev/null +++ b/indra/newview/llluamanager.cpp @@ -0,0 +1,1120 @@ +/** + * @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 "hexdump.h" +#include "llerror.h" +#include "lleventcoro.h" +#include "lleventfilter.h" +#include "llevents.h" +#include "llinstancetracker.h" +#include "llleaplistener.h" +#include "lluuid.h" +#include "stringize.h" + +// skip all these link dependencies for integration testing +#ifndef LL_TEST +#include "lluilistener.h" +#include "llviewercontrol.h" + +// FIXME extremely hacky way to get to the UI Listener framework. There's +// a cleaner way. +extern LLUIListener sUIListener; +#endif // ! LL_TEST + +#include <boost/algorithm/string/replace.hpp> + +#include "luau/luacode.h" +#include "luau/lua.h" +#include "luau/luaconf.h" +#include "luau/lualib.h" + +#define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n))) +#define lua_rawlen lua_objlen + +#include <algorithm> +#include <cstdlib> // std::rand() +#include <cstring> // std::memcpy() +#include <map> +#include <memory> // std::unique_ptr +#include <sstream> +#include <string_view> +#include <vector> + +std::string lua_tostdstring(lua_State* L, int index); +void lua_pushstdstring(lua_State* L, const std::string& str); +LLSD lua_tollsd(lua_State* L, int index); +void lua_pushllsd(lua_State* L, const LLSD& data); + +/** + * LuaListener is based on LLLeap. It serves an analogous function. + * + * Each LuaListener instance has an int key, generated randomly to + * inconvenience malicious Lua scripts wanting to mess with others. The idea + * is that a given lua_State stores in its Registry: + * - "event.listener": the int key of the corresponding LuaListener, if any + * - "event.function": the Lua function to be called with incoming events + * The original thought was that LuaListener would itself store the Lua + * function -- but surprisingly, there is no C/C++ type in the API that stores + * a Lua function. + * + * (We considered storing in "event.listener" the LuaListener pointer itself + * as a light userdata, but the problem would be if Lua code overwrote that. + * We want to prevent any Lua script from crashing the viewer, intentionally + * or otherwise. Safer to use a key lookup.) + * + * Like LLLeap, each LuaListener instance also has an associated + * LLLeapListener to respond to LLEventPump management commands. + */ +class LuaListener: public LLInstanceTracker<LuaListener, int> +{ + using super = LLInstanceTracker<LuaListener, int>; +public: + LuaListener(lua_State* L): + super(getUniqueKey()), + mListener( + new LLLeapListener( + [L](LLEventPump& pump, const std::string& listener) + { return connect(L, pump, listener); })) + { + mReplyConnection = connect(L, mReplyPump, "LuaListener"); + } + + LuaListener(const LuaListener&) = delete; + LuaListener& operator=(const LuaListener&) = delete; + + ~LuaListener() + { + LL_DEBUGS("Lua") << "~LuaListener('" << mReplyPump.getName() << "')" << LL_ENDL; + } + + std::string getReplyName() const { return mReplyPump.getName(); } + std::string getCommandName() const { return mListener->getPumpName(); } + +private: + static int getUniqueKey() + { + // Find a random key that does NOT already correspond to a LuaListener + // instance. Passing a duplicate key to LLInstanceTracker would do Bad + // Things. + int key; + do + { + key = std::rand(); + } while (LuaListener::getInstance(key)); + // This is theoretically racy, if we were instantiating new + // LuaListeners on multiple threads. Don't. + return key; + } + + static LLBoundListener connect(lua_State* L, LLEventPump& pump, const std::string& listener) + { + return pump.listen( + listener, + [L, pumpname=pump.getName()](const LLSD& data) + { return call_lua(L, pumpname, data); }); + } + + static bool call_lua(lua_State* L, const std::string& pump, const LLSD& data) + { + LL_INFOS("Lua") << "LuaListener::call_lua('" << pump << "', " << data << ")" << LL_ENDL; + if (! lua_checkstack(L, 3)) + { + LL_WARNS("Lua") << "Cannot extend Lua stack to call listen_events() callback" + << LL_ENDL; + return false; + } + // push the registered Lua callback function stored in our registry as + // "event.function" + lua_getfield(L, LUA_REGISTRYINDEX, "event.function"); + llassert(lua_isfunction(L, -1)); + // pass pump name + lua_pushstdstring(L, pump); + // then the data blob + lua_pushllsd(L, data); + // call the registered Lua listener function; allow it to return bool; + // no message handler + auto status = lua_pcall(L, 2, 1, 0); + bool result{ false }; + if (status != LUA_OK) + { + LL_WARNS("Lua") << "Error in listen_events() callback: " + << lua_tostdstring(L, -1) << LL_ENDL; + } + else + { + result = lua_toboolean(L, -1); + } + // discard either the error message or the bool return value + lua_pop(L, 1); + return result; + } + +#ifndef LL_TEST + LLEventStream mReplyPump{ LLUUID::generateNewID().asString() }; +#else + LLEventLogProxyFor<LLEventStream> mReplyPump{ "luapump", false }; +#endif + LLTempBoundListener mReplyConnection; + std::unique_ptr<LLLeapListener> mListener; +}; + +/** + * LuaPopper is an RAII struct whose role is to pop some number of entries + * from the Lua stack if the calling function exits early. + */ +struct LuaPopper +{ + LuaPopper(lua_State* L, int count): + mState(L), + mCount(count) + {} + + LuaPopper(const LuaPopper&) = delete; + LuaPopper& operator=(const LuaPopper&) = delete; + + ~LuaPopper() + { + if (mCount) + { + lua_pop(mState, mCount); + } + } + + void disarm() { set(0); } + void set(int count) { mCount = count; } + + lua_State* mState; + int mCount; +}; + +/** + * LuaFunction is a base class containing a static registry of its static + * subclass call() methods. call() is NOT virtual: instead, each subclass + * constructor passes a pointer to its distinct call() method to the base- + * class constructor, along with a name by which to register that method. + * + * The init() method walks the registry and registers each such name with the + * passed lua_State. + */ +class LuaFunction +{ +public: + LuaFunction(const std::string_view& name, lua_CFunction function) + { + getRegistry().emplace(name, function); + } + + static void init(lua_State* L) + { + for (const auto& pair: getRegistry()) + { + lua_register(L, pair.first.c_str(), pair.second); + } + } + + static lua_CFunction get(const std::string& key) + { + // use find() instead of subscripting to avoid creating an entry for + // unknown key + const auto& registry{ getRegistry() }; + auto found{ registry.find(key) }; + return (found == registry.end())? nullptr : found->second; + } + +private: + using Registry = std::map<std::string, lua_CFunction>; + static Registry& getRegistry() + { + // use a function-local static to ensure it's initialized + static Registry registry; + return registry; + } +}; + +/** + * lua_function(name) is a macro to facilitate defining C++ functions + * available to Lua. It defines a subclass of LuaFunction and declares a + * static instance of that subclass, thereby forcing the compiler to call its + * constructor at module initialization time. The constructor passes the + * stringized instance name to its LuaFunction base-class constructor, along + * with a pointer to the static subclass call() method. It then emits the + * call() method definition header, to be followed by a method body enclosed + * in curly braces as usual. + */ +#define lua_function(name) \ +static struct name##_ : public LuaFunction \ +{ \ + name##_(): LuaFunction(#name, &call) {} \ + static int call(lua_State* L); \ +} name; \ +int name##_::call(lua_State* L) +// { +// ... supply method body here, referencing 'L' ... +// } + +/* +// This function consumes ALL Lua stack arguments and returns concatenated +// message string +std::string lua_print_msg_args(lua_State* L, const std::string_view& level) +{ + // On top of existing Lua arguments, push 'where' info + luaL_checkstack(L, 1, 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; p <= lua_gettop(L); ++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(). + // Rotate downwards, producing stack args 2 .. N, tostring(), arg1. + lua_rotate(L, 1, -1); + // pop tostring() and arg1, pushing tostring(arg1) + // (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); + // stack now holds args 2 .. N, tostring(arg1) + out << lua_tostring(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)); + return msg; +} +*/ + +std::string lua_print_msg(lua_State *L, const std::string_view &level) +{ + lua_getglobal(L, "tostring"); + + lua_pushvalue(L, -1); /* function to be called */ + lua_pushvalue(L, 1); /* value to print */ + lua_call(L, 1, 1); + std::string msg = lua_tostring(L, -1); + + LLEventPumps::instance().obtain("lua output").post(stringize(level, ": ", msg)); + return msg; +} + +lua_function(print_debug) +{ + LL_DEBUGS("Lua") << lua_print_msg(L, "DEBUG") << LL_ENDL; + return 0; +} + +// also used for print(); see LuaState constructor +lua_function(print_info) +{ + LL_INFOS("Lua") << lua_print_msg(L, "INFO") << LL_ENDL; + return 0; +} + +lua_function(print_warning) +{ + LL_WARNS("Lua") << lua_print_msg(L, "WARN") << LL_ENDL; + return 0; +} + +#ifndef LL_TEST + +lua_function(run_ui_command) +{ + int top = lua_gettop(L); + std::string func_name; + if (top >= 1) + { + func_name = lua_tostring(L,1); + } + std::string parameter; + if (top >= 2) + { + parameter = lua_tostring(L,2); + } + LL_WARNS("LUA") << "running ui func " << func_name << " parameter " << parameter << LL_ENDL; + LLSD event; + event["function"] = func_name; + if (!parameter.empty()) + { + event["parameter"] = parameter; + } + sUIListener.call(event); + + lua_settop(L, 0); + return 0; +} +#endif // ! LL_TEST + +lua_function(post_on) +{ + std::string pumpname{ lua_tostdstring(L, 1) }; + LLSD data{ lua_tollsd(L, 2) }; + lua_pop(L, 2); + LL_INFOS("Lua") << "post_on('" << pumpname << "', " << data << ")" << LL_ENDL; + LLEventPumps::instance().obtain(pumpname).post(data); + return 0; +} + +lua_function(listen_events) +{ + if (! lua_isfunction(L, 1)) + { + luaL_typeerror(L, 1, "function"); + return 0; + } + luaL_checkstack(L, 2, nullptr); + + // Get the lua_State* for the main thread of this state, in case we were + // called from a coroutine thread. We're going to make callbacks into Lua + // code, and we want to do it on the main thread rather than a (possibly + // suspended) coroutine thread. + // Registry table is at pseudo-index LUA_REGISTRYINDEX + // Main thread is at registry key LUA_RIDX_MAINTHREAD + auto regtype {lua_rawgeti(L, LUA_REGISTRYINDEX, 1 /*LUA_RIDX_MAINTHREAD*/)}; + // Not finding the main thread at the documented place isn't a user error, + // it's a Problem + llassert_always(regtype == LUA_TTHREAD); + lua_State* mainthread{ lua_tothread(L, -1) }; + // pop the main thread + lua_pop(L, 1); + + luaL_checkstack(mainthread, 1, nullptr); + LuaListener::ptr_t listener; + // Does the main thread already have a LuaListener stored in the registry? + // That is, has this Lua chunk already called listen_events()? + auto keytype{ lua_getfield(mainthread, LUA_REGISTRYINDEX, "event.listener") }; + llassert(keytype == LUA_TNIL || keytype == LUA_TNUMBER); + if (keytype == LUA_TNUMBER) + { + // We do already have a LuaListener. Retrieve it. + int isint; + listener = LuaListener::getInstance(lua_tointegerx(mainthread, -1, &isint)); + // pop the int "event.listener" key + lua_pop(mainthread, 1); + // Nobody should have destroyed this LuaListener instance! + llassert(isint && listener); + } + else + { + // pop the nil "event.listener" key + lua_pop(mainthread, 1); + // instantiate a new LuaListener, binding the mainthread state -- but + // use a no-op deleter: we do NOT want to delete this new LuaListener + // on return from listen_events()! + listener.reset(new LuaListener(mainthread), [](LuaListener*){}); + // set its key in the field where we'll look for it later + lua_pushinteger(mainthread, listener->getKey()); + lua_setfield(mainthread, LUA_REGISTRYINDEX, "event.listener"); + } + + // Now that we've found or created our LuaListener, store the passed Lua + // function as the callback. Beware: our caller passed the function on L's + // stack, but we want to store it on the mainthread registry. + if (L != mainthread) + { + // push 1 value (the Lua function) from L's stack to mainthread's + lua_xmove(L, mainthread, 1); + } + lua_setfield(mainthread, LUA_REGISTRYINDEX, "event.function"); + + // 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(await_event) +{ + // await_event(pumpname [, timeout [, value to return if timeout (default nil)]]) + auto pumpname{ lua_tostdstring(L, 1) }; + LLSD result; + if (lua_gettop(L) > 1) + { + auto timeout{ lua_tonumber(L, 2) }; + // with no 3rd argument, should be LLSD() + auto dftval{ lua_tollsd(L, 3) }; + lua_settop(L, 0); + result = llcoro::suspendUntilEventOnWithTimeout(pumpname, timeout, dftval); + } + else + { + // no timeout + lua_pop(L, 1); + result = llcoro::suspendUntilEventOn(pumpname); + } + lua_pushllsd(L, result); + return 1; +} + +/** + * RAII class to manage the lifespan of a lua_State + */ +class LuaState +{ +public: + LuaState(const std::string_view& desc, LLLUAmanager::script_finished_fn cb): + mDesc(desc), + mCallback(cb), + mState(luaL_newstate()) + { + luaL_openlibs(mState); + LuaFunction::init(mState); + // Try to make print() write to our log. + lua_register(mState, "print", LuaFunction::get("print_info")); + } + + LuaState(const LuaState&) = delete; + LuaState& operator=(const LuaState&) = delete; + + ~LuaState() + { + // Did somebody call listen_events() on this LuaState? + // That is, is there a LuaListener key in its registry? + auto keytype{ lua_getfield(mState, LUA_REGISTRYINDEX, "event.listener") }; + if (keytype == LUA_TNUMBER) + { + // We do have a LuaListener. Retrieve it. + int isint; + auto listener{ LuaListener::getInstance(lua_tointegerx(mState, -1, &isint)) }; + // pop the int "event.listener" key + lua_pop(mState, 1); + // if we got a LuaListener instance, destroy it + // (if (! isint), lua_tointegerx() returned 0, but key 0 might + // validly designate someone ELSE's LuaListener) + if (isint && listener) + { + auto lptr{ listener.get() }; + listener.reset(); + delete lptr; + } + } + + lua_close(mState); + + if (mCallback) + { + // mError potentially set by previous checkLua() call(s) + mCallback(mError); + } + } + + bool checkLua(int r) + { + if (r != LUA_OK) + { + mError = lua_tostring(mState, -1); + lua_pop(mState, 1); + + LL_WARNS() << mDesc << ": " << mError << LL_ENDL; + return false; + } + return true; + } + + operator lua_State*() const { return mState; } + +private: + std::string mDesc; + LLLUAmanager::script_finished_fn mCallback; + lua_State* mState; + std::string mError; +}; + +void LLLUAmanager::runScriptFile(const std::string& filename, script_finished_fn cb) +{ + std::string desc{ stringize("runScriptFile('", filename, "')") }; + LLCoros::instance().launch(desc, [desc, filename, cb]() + { + LuaState L(desc, cb); + + auto LUA_sleep_func = [](lua_State *L) + { + F32 seconds = lua_tonumber(L, -1); + lua_pop(L, 1); + llcoro::suspendUntilTimeout(seconds); + return 0; + }; + + lua_register(L, "sleep", LUA_sleep_func); + + llifstream in_file; + in_file.open(filename.c_str()); + + if (in_file.is_open()) + { + std::string text((std::istreambuf_iterator<char>(in_file)), std::istreambuf_iterator<char>()); + + size_t bytecodeSize = 0; + char *bytecode = luau_compile(text.c_str(), text.length(), NULL, &bytecodeSize); + L.checkLua(luau_load(L, desc.c_str(), bytecode, bytecodeSize, 0)); + free(bytecode); + + L.checkLua(lua_pcall(L, 0, 0, 0)); + + in_file.close(); + } + else + { + LL_WARNS("Lua") << "unable to open script file '" << filename << "'" << LL_ENDL; + } + }); +} + +void LLLUAmanager::runScriptLine(const std::string& cmd, script_finished_fn cb) +{ + // find a suitable abbreviation for the cmd string + std::string_view shortcmd{ cmd }; + const size_t shortlen = 40; + std::string::size_type eol = shortcmd.find_first_of("\r\n"); + if (eol != std::string::npos) + shortcmd = shortcmd.substr(0, eol); + if (shortcmd.length() > shortlen) + shortcmd = stringize(shortcmd.substr(0, shortlen), "..."); + + std::string desc{ stringize("runScriptLine('", shortcmd, "')") }; + LLCoros::instance().launch(desc, [desc, cmd, cb]() + { + LuaState L(desc, cb); + + size_t bytecodeSize = 0; + char *bytecode = luau_compile(cmd.c_str(), cmd.length(), NULL, &bytecodeSize); + L.checkLua(luau_load(L, desc.c_str(), bytecode, bytecodeSize, 0)); + free(bytecode); + L.checkLua(lua_pcall(L, 0, 0, 0)); + }); +} + +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 lua_tostdstring(lua_State* L, int index) +{ + size_t len; + const char* strval{ lua_tolstring(L, index, &len) }; + return { strval, len }; +} + +void lua_pushstdstring(lua_State* L, const std::string& str) +{ + luaL_checkstack(L, 1, nullptr); + lua_pushlstring(L, str.c_str(), str.length()); +} + +// Usage: std::cout << lua_what(L, stackindex) << ...; +// Reports on the Lua value found at the passed stackindex. +// If cast to std::string, returns the corresponding string value. +class lua_what +{ +public: + lua_what(lua_State* state, int idx): + L(state), + index(idx) + {} + + friend std::ostream& operator<<(std::ostream& out, const lua_what& self) + { + switch (lua_type(self.L, self.index)) + { + case LUA_TNONE: + // distinguish acceptable but non-valid index + out << "none"; + break; + + case LUA_TNIL: + out << "nil"; + break; + + case LUA_TBOOLEAN: + { + auto oldflags { out.flags() }; + out << std::boolalpha << lua_toboolean(self.L, self.index); + out.flags(oldflags); + break; + } + + case LUA_TNUMBER: + out << lua_tonumber(self.L, self.index); + break; + + case LUA_TSTRING: + out << std::quoted(lua_tostdstring(self.L, self.index)); + break; + + case LUA_TUSERDATA: + { + const S32 maxlen = 20; + S32 binlen{ lua_rawlen(self.L, self.index) }; + LLSD::Binary binary(std::min(maxlen, binlen)); + std::memcpy(binary.data(), lua_touserdata(self.L, self.index), binary.size()); + out << LL::hexdump(binary); + if (binlen > maxlen) + { + out << "...(" << (binlen - maxlen) << " more)"; + } + break; + } + + case LUA_TLIGHTUSERDATA: + out << lua_touserdata(self.L, self.index); + break; + + default: + // anything else, don't bother trying to report value, just type + out << lua_typename(self.L, lua_type(self.L, self.index)); + break; + } + return out; + } + + operator std::string() const { return stringize(*this); } + +private: + lua_State* L; + int index; +}; + +// Usage: std::cout << lua_stack(L) << ...; +// Reports on the contents of the Lua stack. +// If cast to std::string, returns the corresponding string value. +class lua_stack +{ +public: + lua_stack(lua_State* state): + L(state) + {} + + friend std::ostream& operator<<(std::ostream& out, const lua_stack& self) + { + const char* sep = "stack: ["; + for (int index = 1; index <= lua_gettop(self.L); ++index) + { + out << sep << lua_what(self.L, index); + sep = ", "; + } + out << ']'; + return out; + } + + operator std::string() const { return stringize(*this); } + +private: + lua_State* L; +}; + +// log exit from any block declaring an instance of DebugExit, regardless of +// how control leaves that block +struct DebugExit +{ + DebugExit(const std::string& name): mName(name) {} + DebugExit(const DebugExit&) = delete; + DebugExit& operator=(const DebugExit&) = delete; + ~DebugExit() + { + LL_DEBUGS("Lua") << "exit " << mName << LL_ENDL; + } + + std::string mName; +}; + +// By analogy with existing lua_tomumble() functions, return an LLSD object +// corresponding to the Lua object at stack index 'index' in state L. +// This function assumes that a Lua caller is fully aware that they're trying +// to call a viewer function. In other words, the caller must specifically +// construct Lua data convertible to LLSD. +// +// For proper error handling, we REQUIRE that the Lua runtime be compiled as +// C++ so errors are raised as C++ exceptions rather than as longjmp() calls: +// http://www.lua.org/manual/5.4/manual.html#4.4 +// "Internally, Lua uses the C longjmp facility to handle errors. (Lua will +// use exceptions if you compile it as C++; search for LUAI_THROW in the +// source code for details.)" +// Some blocks within this function construct temporary C++ objects in the +// expectation that these objects will be properly destroyed even if code +// reached by that block raises a Lua error. +LLSD lua_tollsd(lua_State* L, int index) +{ + LL_DEBUGS("Lua") << "lua_tollsd(" << index << ") of " << lua_gettop(L) << " stack entries: " + << lua_what(L, index) << LL_ENDL; + DebugExit log_exit("lua_tollsd()"); + switch (lua_type(L, index)) + { + case LUA_TNONE: + // Should LUA_TNONE be an error instead of returning isUndefined()? + case LUA_TNIL: + return {}; + + case LUA_TBOOLEAN: + return bool(lua_toboolean(L, index)); + + case LUA_TNUMBER: + { + // check if integer truncation leaves the number intact + int isint; + lua_Integer intval{ lua_tointegerx(L, index, &isint) }; + if (isint) + { + return LLSD::Integer(intval); + } + else + { + return lua_tonumber(L, index); + } + } + + case LUA_TSTRING: + return lua_tostdstring(L, index); + + case LUA_TUSERDATA: + { + LLSD::Binary binary(lua_rawlen(L, index)); + std::memcpy(binary.data(), lua_touserdata(L, index), binary.size()); + return binary; + } + + case LUA_TTABLE: + { + // A Lua table correctly constructed to convert to LLSD will have + // either consecutive integer keys starting at 1, which we represent + // as an LLSD array (with Lua key 1 at C++ index 0), or will have + // all string keys. + // + // In the belief that Lua table traversal skips "holes," that is, it + // doesn't report any key/value pair whose value is nil, we allow a + // table with integer keys >= 1 but with "holes." This produces an + // LLSD array with isUndefined() entries at unspecified keys. There + // would be no other way for a Lua caller to construct an + // isUndefined() LLSD array entry. However, to guard against crazy int + // keys, we forbid gaps larger than a certain size: crazy int keys + // could result in a crazy large contiguous LLSD array. + // + // Possible looseness could include: + // - A mix of integer and string keys could produce an LLSD map in + // which the integer keys are converted to string. (Key conversion + // must be performed in C++, not Lua, to avoid confusing + // lua_next().) + // - However, since in Lua t[0] and t["0"] are distinct table entries, + // do not consider converting numeric string keys to int to return + // an LLSD array. + // But until we get more experience with actual Lua scripts in + // practice, let's say that any deviation is a Lua coding error. + // An important property of the strict definition above is that most + // conforming data blobs can make a round trip across the language + // boundary and still compare equal. A non-conforming data blob would + // lose that property. + // Known exceptions to round trip identity: + // - Empty LLSD map and empty LLSD array convert to empty Lua table. + // But empty Lua table converts to isUndefined() LLSD object. + // - LLSD::Real with integer value returns as LLSD::Integer. + // - LLSD::UUID, LLSD::Date and LLSD::URI all convert to Lua string, + // and so return as LLSD::String. + // - Lua does not store any table key whose value is nil. An LLSD + // array with isUndefined() entries produces a Lua table with + // "holes" in the int key sequence; this converts back to an LLSD + // array containing corresponding isUndefined() entries -- except + // when one or more of the final entries isUndefined(). These are + // simply dropped, producing a shorter LLSD array than the original. + // - For the same reason, any keys in an LLSD map whose value + // isUndefined() are simply discarded in the converted Lua table. + // This converts back to an LLSD map lacking those keys. + // - If it's important to preserve the original length of an LLSD + // array whose final entries are undefined, or the full set of keys + // for an LLSD map some of whose values are undefined, store an + // LLSD::emptyArray() or emptyMap() instead. These will be + // represented in Lua as empty table, which should convert back to + // undefined LLSD. Naturally, though, those won't survive a second + // round trip. + + // This is the most important of the luaL_checkstack() calls because a + // deeply nested Lua structure will enter this case at each level, and + // we'll need another 2 stack slots to traverse each nested table. + luaL_checkstack(L, 2, nullptr); + // BEFORE we push nil to initialize the lua_next() traversal, convert + // 'index' to absolute! Our caller might have passed a relative index; + // we do, below: lua_tollsd(L, -1). If 'index' is -1, then when we + // push nil, what we find at index -1 is nil, not the table! + index = lua_absindex(L, index); + LL_DEBUGS("Lua") << "checking for empty table" << LL_ENDL; + lua_pushnil(L); // first key + LL_DEBUGS("Lua") << lua_stack(L) << LL_ENDL; + if (! lua_next(L, index)) + { + // it's a table, but the table is empty -- no idea if it should be + // modeled as empty array or empty map -- return isUndefined(), + // which can be consumed as either + LL_DEBUGS("Lua") << "empty table" << LL_ENDL; + return {}; + } + // key is at stack index -2, value at index -1 + // from here until lua_next() returns 0, have to lua_pop(2) if we + // return early + LuaPopper popper(L, 2); + // Remember the type of the first key + auto firstkeytype{ lua_type(L, -2) }; + LL_DEBUGS("Lua") << "table not empty, first key type " << lua_typename(L, firstkeytype) + << LL_ENDL; + switch (firstkeytype) + { + case LUA_TNUMBER: + { + // First Lua key is a number: try to convert table to LLSD array. + // This is tricky because we don't know in advance the size of the + // array. The Lua reference manual says that lua_rawlen() is the + // same as the length operator '#'; but the length operator states + // that it might stop at any "hole" in the subject table. + // Moreover, the Lua next() function (and presumably lua_next()) + // traverses a table in unspecified order, even for numeric keys + // (emphasized in the doc). + // Make a preliminary pass over the whole table to validate and to + // collect keys. + std::vector<LLSD::Integer> keys; + // Try to determine the length of the table. If the length + // operator is truthful, avoid allocations while we grow the keys + // vector. Even if it's not, we can still grow the vector, albeit + // a little less efficiently. + keys.reserve(lua_objlen(L, index)); + do + { + auto arraykeytype{ lua_type(L, -2) }; + switch (arraykeytype) + { + case LUA_TNUMBER: + { + int isint; + lua_Integer intkey{ lua_tointegerx(L, -2, &isint) }; + if (! isint) + { + // key isn't an integer - this doesn't fit our LLSD + // array constraints + luaL_error(L, "Expected integer array key, got %f instead", lua_tonumber(L, -2)); + return 0; + } + if (intkey < 1) + { + luaL_error(L, "array key %d out of bounds", int(intkey)); + return 0; + } + + keys.push_back(LLSD::Integer(intkey)); + break; + } + + case LUA_TSTRING: + // break out strings specially to report the value + luaL_error(L, "Cannot convert string array key '%s' to LLSD", lua_tostring(L, -2)); + return 0; + + default: + luaL_error(L, "Cannot convert %s array key to LLSD", lua_typename(L, arraykeytype)); + return 0; + } + + // remove value, keep key for next iteration + lua_pop(L, 1); + } while (lua_next(L, index) != 0); + popper.disarm(); + // Table keys are all integers: are they reasonable integers? + // Arbitrary max: may bite us, but more likely to protect us + size_t array_max{ 10000 }; + if (keys.size() > array_max) + { + luaL_error(L, "Conversion from Lua to LLSD array limited to %d entries", int(array_max)); + return 0; + } + // We know the smallest key is >= 1. Check the largest. We also + // know the vector is NOT empty, else we wouldn't have gotten here. + std::sort(keys.begin(), keys.end()); + LLSD::Integer highkey = *keys.rbegin(); + if ((highkey - LLSD::Integer(keys.size())) > 100) + { + // Looks like we've gone beyond intentional array gaps into + // crazy key territory. + luaL_error(L, "Gaps in Lua table too large for conversion to LLSD array"); + return 0; + } + LL_DEBUGS("Lua") << "collected " << keys.size() << " keys, max " << highkey << LL_ENDL; + // right away expand the result array to the size we'll need + LLSD result{ LLSD::emptyArray() }; + result[highkey - 1] = LLSD(); + // Traverse the table again, and this time populate result array. + lua_pushnil(L); // first key + while (lua_next(L, index)) + { + // key at stack index -2, value at index -1 + // We've already validated lua_tointegerx() for each key. + auto key{ lua_tointeger(L, -2) }; + LL_DEBUGS("Lua") << "key " << key << ':' << LL_ENDL; + // Don't forget to subtract 1 from Lua key for LLSD subscript! + result[LLSD::Integer(key) - 1] = lua_tollsd(L, -1); + // remove value, keep key for next iteration + lua_pop(L, 1); + } + return result; + } + + case LUA_TSTRING: + { + // First Lua key is a string: try to convert table to LLSD map + LLSD result{ LLSD::emptyMap() }; + do + { + auto mapkeytype{ lua_type(L, -2) }; + if (mapkeytype != LUA_TSTRING) + { + luaL_error(L, "Cannot convert %s map key to LLSD", lua_typename(L, mapkeytype)); + return 0; + } + + auto key{ lua_tostdstring(L, -2) }; + LL_DEBUGS("Lua") << "map key " << std::quoted(key) << ':' << LL_ENDL; + result[key] = lua_tollsd(L, -1); + // remove value, keep key for next iteration + lua_pop(L, 1); + } while (lua_next(L, index) != 0); + popper.disarm(); + return result; + } + + default: + // First Lua key isn't number or string: sorry + luaL_error(L, "Cannot convert %s table key to LLSD", lua_typename(L, firstkeytype)); + return 0; + } + } + + default: + // Other Lua entities (e.g. function, C function, light userdata, + // thread, userdata) are not convertible to LLSD, indicating a coding + // error in the caller. + luaL_error(L, "Cannot convert type %s to LLSD", luaL_typename(L, index)); + return 0; + } +} + +// By analogy with existing lua_pushmumble() functions, push onto state L's +// stack a Lua object corresponding to the passed LLSD object. +void lua_pushllsd(lua_State* L, const LLSD& data) +{ + // might need 2 slots for array or map + luaL_checkstack(L, 2, nullptr); + switch (data.type()) + { + case LLSD::TypeUndefined: + lua_pushnil(L); + break; + + case LLSD::TypeBoolean: + lua_pushboolean(L, data.asBoolean()); + break; + + case LLSD::TypeInteger: + lua_pushinteger(L, data.asInteger()); + break; + + case LLSD::TypeReal: + lua_pushnumber(L, data.asReal()); + break; + + case LLSD::TypeBinary: + { + auto binary{ data.asBinary() }; + std::memcpy(lua_newuserdata(L, binary.size()), + binary.data(), binary.size()); + break; + } + + case LLSD::TypeMap: + { + // push a new table with space for our non-array keys + lua_createtable(L, 0, data.size()); + for (const auto& pair: llsd::inMap(data)) + { + // push value -- so now table is at -2, value at -1 + lua_pushllsd(L, pair.second); + // pop value, assign to table[key] + lua_setfield(L, -2, pair.first.c_str()); + } + break; + } + + case LLSD::TypeArray: + { + // push a new table with space for array entries + lua_createtable(L, data.size(), 0); + lua_Integer key{ 0 }; + for (const auto& item: llsd::inArray(data)) + { + // push new array value: table at -2, value at -1 + lua_pushllsd(L, item); + // pop value, assign table[key] = value + lua_rawseti(L, -2, ++key); + } + break; + } + + case LLSD::TypeString: + case LLSD::TypeUUID: + case LLSD::TypeDate: + case LLSD::TypeURI: + default: + { + lua_pushstdstring(L, data.asString()); + break; + } + } +} diff --git a/indra/newview/llluamanager.h b/indra/newview/llluamanager.h new file mode 100644 index 0000000000..08d9876ce2 --- /dev/null +++ b/indra/newview/llluamanager.h @@ -0,0 +1,45 @@ +/** + * @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 <functional> +#include <string> + +class LLLUAmanager +{ +public: + typedef std::function<void(std::string msg)> script_finished_fn; + + static void runScriptFile(const std::string &filename, script_finished_fn cb = script_finished_fn()); + static void runScriptLine(const std::string &cmd, script_finished_fn cb = script_finished_fn()); + + static void runScriptOnLogin(); +}; + + +#endif diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index d0b76848f7..6c5a8fab8c 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" @@ -2422,6 +2423,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, ®ionp ); if( !success ) { return FALSE; } + return rezNewObject(pcode,hit_obj, hit_face, b_hit_land, ray_start_region, ray_end_region, regionp, use_physics); +} +BOOL LLToolPlacer::rezNewObject(LLPCode pcode, LLViewerObject * hit_obj, S32 hit_face, BOOL b_hit_land, LLVector3 ray_start_region, + LLVector3 ray_end_region, LLViewerRegion* regionp, U8 use_physics) +{ if( hit_obj && (hit_obj->isAvatar() || hit_obj->isAttachment()) ) { // Can't create objects on avatars or attachments @@ -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/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index 3a08f748d6..60de65858b 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -92,6 +92,7 @@ #include "llfloaterlandholdings.h" #include "llfloaterlinkreplace.h" #include "llfloaterloadprefpreset.h" +#include "llfloaterluadebug.h" #include "llfloatermap.h" #include "llfloatermarketplacelistings.h" #include "llfloatermediasettings.h" @@ -395,6 +396,8 @@ 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("mem_leaking", "floater_mem_leaking.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterMemLeak>); diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index 9db9d97ddc..f3e52caee4 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -172,6 +172,8 @@ extern BOOL gShaderProfileFrame; // Globals // +LLUIListener sUIListener; + LLMenuBarGL *gMenuBarView = NULL; LLViewerMenuHolderGL *gMenuHolder = NULL; LLMenuGL *gPopupMenuView = NULL; @@ -348,8 +350,6 @@ public: static LLMenuParcelObserver* gMenuParcelObserver = NULL; -static LLUIListener sUIListener; - LLMenuParcelObserver::LLMenuParcelObserver() { LLViewerParcelMgr::getInstance()->addObserver(this); @@ -8905,80 +8905,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 ff2ee693fd..8f4755903b 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/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..f03739f7c2 --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_lua_debug.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<floater + can_minimize="false" + can_resize="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> + <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/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml index 660f4b62c7..62f0177286 100644 --- a/indra/newview/skins/default/xui/en/menu_viewer.xml +++ b/indra/newview/skins/default/xui/en/menu_viewer.xml @@ -2529,6 +2529,17 @@ 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_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..98a2726af7 --- /dev/null +++ b/indra/newview/tests/llluamanager_test.cpp @@ -0,0 +1,301 @@ +/** + * @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 "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"); + + template<> template<> + void object::test<1>() + { + set_test_name("test post_on(), listen_events(), await_event()"); + StringVec posts; + LLEventStream replypump("testpump"); + LLTempBoundListener conn( + replypump.listen("test<1>", + listener([&posts](const LLSD& data) + { posts.push_back(data.asString()); }))); + const std::string lua( + "-- test post_on,listen_events,await_event\n" + "post_on('testpump', 'entry')\n" + "callback = function(pump, data)\n" + " -- just echo the data we received\n" + " post_on('testpump', data)\n" + "end\n" + "post_on('testpump', 'listen_events()')\n" + "replypump, cmdpump = listen_events(callback)\n" + "post_on('testpump', replypump)\n" + "post_on('testpump', 'await_event()')\n" + "await_event(replypump)\n" + "post_on('testpump', 'exit')\n" + ); + LLLUAmanager::runScriptLine(lua); + StringVec expected{ + "entry", + "listen_events()", + "", + "await_event()", + "message", + "exit" + }; + for (int i = 0; i < 10 && posts.size() <= 2 && posts[2].empty(); ++i) + { + llcoro::suspend(); + } + 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]); + llcoro::suspend(); + ensure_equals("post_on() sequence", posts, expected); + } + + void from_lua(const std::string& desc, const std::string_view& construct, const LLSD& expect) + { + LLSD fromlua; + LLEventStream replypump("testpump"); + LLTempBoundListener conn( + replypump.listen("llluamanager_test", + listener([&fromlua](const LLSD& data){ fromlua = data; }))); + const std::string lua(stringize( + "-- test LLSD synthesized by Lua\n", + // we expect the caller's Lua snippet to construct a Lua object + // called 'data' + construct, "\n" + "post_on('testpump', data)\n" + )); + LLLUAmanager::runScriptLine(lua); + // At this point LLLUAmanager::runScriptLine() has launched a new C++ + // coroutine to run the passed Lua snippet, but that coroutine hasn't + // yet had a chance to run. Poke the coroutine scheduler until the Lua + // script has sent its data. + for (int i = 0; i < 10 && fromlua.isUndefined(); ++i) + { + llcoro::suspend(); + } + // We woke up again ourselves because the coroutine running Lua has + // finished. + ensure_equals(desc, fromlua, expect); + } + + template<> template<> + void object::test<2>() + { + set_test_name("LLSD from Lua"); + from_lua("nil", "data = nil", LLSD()); + from_lua("true", "data = true", true); + from_lua("false", "data = false", false); + from_lua("int", "data = 17", 17); + from_lua("real", "data = 3.14", 3.14); + from_lua("string", "data = 'string'", "string"); + // can't synthesize Lua userdata in Lua code: that can only be + // constructed by a C function + from_lua("empty table", "data = {}", LLSD()); + from_lua("nested empty table", "data = { 1, 2, 3, {}, 5 }", + llsd::array(1, 2, 3, LLSD(), 5)); + from_lua("nested non-empty table", "data = { 1, 2, 3, {a=0, b=1}, 5 }", + llsd::array(1, 2, 3, llsd::map("a", 0, "b", 1), 5)); + } + + void round_trip(const std::string& desc, const LLSD& send, const LLSD& expect) + { + LLSD reply; + LLEventStream replypump("testpump"); + LLTempBoundListener conn( + replypump.listen("llluamanager_test", + listener([&reply](const LLSD& post){ reply = post; }))); + const std::string lua( + "-- test LLSD round trip\n" + "callback = function(pump, data)\n" + " -- just echo the data we received\n" + " post_on('testpump', data)\n" + "end\n" + "replypump, cmdpump = listen_events(callback)\n" + "post_on('testpump', replypump)\n" + "await_event(replypump)\n" + ); + LLLUAmanager::runScriptLine(lua); + // At this point LLLUAmanager::runScriptLine() has launched a new C++ + // coroutine to run the passed Lua snippet, but that coroutine hasn't + // yet had a chance to run. Poke the coroutine scheduler until the Lua + // script has sent its reply pump name. + for (int i = 0; i < 10 && reply.isUndefined(); ++i) + { + llcoro::suspend(); + } + // We woke up again ourselves because the coroutine running Lua has + // reached the await_event() call, which suspends the calling C++ + // coroutine (including the Lua code running on it) until we post + // something to that reply pump. + auto luapump{ reply.asString() }; + reply.clear(); + 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. + llcoro::suspend(); + ensure_equals(desc, reply, 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<3>() + { + 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); + } +} // namespace tut |