From 03a10d367a8ef810483838b1931bdff2752ddfc0 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Wed, 25 Sep 2024 22:20:47 +0200 Subject: [PATCH 01/39] linux: added inotify version of serial port list for systems without systemd --- server/CMakeLists.txt | 3 + server/src/os/linux/isserialdevice.hpp | 39 +++++ .../os/linux/serialportlistimplinotify.cpp | 159 ++++++++++++++++++ .../os/linux/serialportlistimplinotify.hpp | 48 ++++++ .../os/linux/serialportlistimplsystemd.cpp | 7 +- server/src/os/serialportlist.cpp | 4 +- server/src/os/serialportlist.hpp | 8 +- 7 files changed, 261 insertions(+), 7 deletions(-) create mode 100644 server/src/os/linux/isserialdevice.hpp create mode 100644 server/src/os/linux/serialportlistimplinotify.cpp create mode 100644 server/src/os/linux/serialportlistimplinotify.hpp diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 4599735c..d9038fa8 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -215,6 +215,9 @@ if(LINUX) if(BUILD_TESTING) target_link_libraries(traintastic-server-test PRIVATE PkgConfig::LIBSYSTEMD) endif() + else() + # Use inotify for monitoring serial ports: + list(APPEND SOURCES "src/os/linux/serialportlistimplinotify.hpp" "src/os/linux/serialportlistimplinotify.cpp") endif() else() # socket CAN is only available on linux: diff --git a/server/src/os/linux/isserialdevice.hpp b/server/src/os/linux/isserialdevice.hpp new file mode 100644 index 00000000..1dc5793f --- /dev/null +++ b/server/src/os/linux/isserialdevice.hpp @@ -0,0 +1,39 @@ +/** + * server/src/os/linux/isserialdevice.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2024 Reinder Feenstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TRAINTASTIC_SERVER_OS_LINUX_ISSERIALDEVICE_HPP +#define TRAINTASTIC_SERVER_OS_LINUX_ISSERIALDEVICE_HPP + +#include "../../utils/startswith.hpp" + +namespace Linux { + +inline bool isSerialDevice(std::string_view devPath) +{ + return startsWith(devPath, "/dev/ttyS") || + startsWith(devPath, "/dev/ttyUSB") || + startsWith(devPath, "/dev/ttyACM"); +} + +} + +#endif diff --git a/server/src/os/linux/serialportlistimplinotify.cpp b/server/src/os/linux/serialportlistimplinotify.cpp new file mode 100644 index 00000000..ea93f88a --- /dev/null +++ b/server/src/os/linux/serialportlistimplinotify.cpp @@ -0,0 +1,159 @@ +/** + * server/src/os/linux/serialportlistimplinotify.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2024 Reinder Feenstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "serialportlistimplinotify.hpp" +#include "isserialdevice.hpp" +#include +#include +#include +#include "../../core/eventloop.hpp" +#include "../../utils/startswith.hpp" +#include "../../utils/setthreadname.hpp" + +namespace Linux { + +SerialPortListImplInotify::SerialPortListImplInotify(SerialPortList& list) + : SerialPortListImpl(list) + , m_stopEvent{eventfd(0, O_NONBLOCK)} + , m_thread{ + [this]() + { + setThreadName("serialport-inotify"); + + int inotifyFd = inotify_init1(IN_NONBLOCK); + if(inotifyFd == -1) + { + return; + } + + int watchFd = inotify_add_watch(inotifyFd, "/dev", IN_CREATE | IN_DELETE); + if(watchFd == -1) + { + close(inotifyFd); + return; + } + + // Polling for events: + pollfd fds[2]; + fds[0].fd = inotifyFd; + fds[0].events = POLLIN; + fds[1].fd = m_stopEvent; + fds[1].events = POLLIN; + + for(;;) + { + int r = poll(fds, 2, -1); // wait for events + if(r == -1) + { + if(errno == EINTR) + { + continue; // interrupted by signal + } + break; // poll failed + } + + if(fds[1].revents & POLLIN) // stop event + { + break; + } + else if(fds[0].revents & POLLIN) // inotify event + { + handleInotifyEvents(inotifyFd); + } + } + + // clean up: + inotify_rm_watch(inotifyFd, watchFd); + close(inotifyFd); + }} +{ +} + +SerialPortListImplInotify::~SerialPortListImplInotify() +{ + eventfd_write(m_stopEvent, 1); + m_thread.join(); + close(m_stopEvent); +} + +void SerialPortListImplInotify::handleInotifyEvents(int inotifyFd) +{ + char buffer[1024]; + ssize_t length = read(inotifyFd, buffer, sizeof(buffer)); + if(length < 0) + { + return; + } + + for(ssize_t i = 0; i < length;) + { + const auto* event = reinterpret_cast(&buffer[i]); + const auto devPath = std::string("/dev/") + event->name; + + if(event->mask & IN_CREATE) + { + if(isSerialDevice(devPath)) + { + EventLoop::call( + [this, devPath]() + { + addToList(devPath); + }); + } + } + else if(event->mask & IN_DELETE) + { + if(isSerialDevice(devPath)) + { + EventLoop::call( + [this, devPath]() + { + removeFromList(devPath); + }); + } + } + + i += sizeof(inotify_event) + event->len; + } +} + +std::vector SerialPortListImplInotify::get() const +{ + std::vector devices; + const std::filesystem::path devDir("/dev"); + + if(std::filesystem::exists(devDir) && std::filesystem::is_directory(devDir)) + { + for(const auto& entry : std::filesystem::directory_iterator(devDir)) + { + const auto& devPath = entry.path().string(); + if(isSerialDevice(devPath)) + { + devices.push_back(devPath); + } + } + } + + return devices; +} + +} diff --git a/server/src/os/linux/serialportlistimplinotify.hpp b/server/src/os/linux/serialportlistimplinotify.hpp new file mode 100644 index 00000000..4e71390d --- /dev/null +++ b/server/src/os/linux/serialportlistimplinotify.hpp @@ -0,0 +1,48 @@ +/** + * server/src/os/linux/serialportlistimplinotify.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2024 Reinder Feenstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TRAINTASTIC_SERVER_OS_LINUX_SERIALPORTLISTIMPLINOTIFY_HPP +#define TRAINTASTIC_SERVER_OS_LINUX_SERIALPORTLISTIMPLINOTIFY_HPP + +#include "../serialportlistimpl.hpp" +#include + +namespace Linux { + +class SerialPortListImplInotify final : public SerialPortListImpl +{ + private: + int m_stopEvent; + std::thread m_thread; + + void handleInotifyEvents(int inotifyFd); + + public: + SerialPortListImplInotify(SerialPortList& list); + ~SerialPortListImplInotify() final; + + std::vector get() const final; +}; + +} + +#endif diff --git a/server/src/os/linux/serialportlistimplsystemd.cpp b/server/src/os/linux/serialportlistimplsystemd.cpp index 124bc118..0d16b97a 100644 --- a/server/src/os/linux/serialportlistimplsystemd.cpp +++ b/server/src/os/linux/serialportlistimplsystemd.cpp @@ -21,6 +21,7 @@ */ #include "serialportlistimplsystemd.hpp" +#include "isserialdevice.hpp" #include "../../core/eventloop.hpp" #include "../../utils/startswith.hpp" #include "../../utils/setthreadname.hpp" @@ -42,11 +43,7 @@ static std::string_view getDevPath(sd_device* device) static bool isSerialDevice(sd_device* device) { - auto devPath = getDevPath(device); - return - startsWith(devPath, "/dev/ttyS") || - startsWith(devPath, "/dev/ttyUSB") || - startsWith(devPath, "/dev/ttyACM"); + return isSerialDevice(getDevPath(device)); } diff --git a/server/src/os/serialportlist.cpp b/server/src/os/serialportlist.cpp index 8b77236b..6bdde346 100644 --- a/server/src/os/serialportlist.cpp +++ b/server/src/os/serialportlist.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2022 Reinder Feenstra + * Copyright (C) 2022,2024 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -26,6 +26,8 @@ #endif #ifdef HAS_LIBSYSTEMD #include "linux/serialportlistimplsystemd.hpp" +#elif defined(__linux__) + #include "linux/serialportlistimplinotify.hpp" #elif defined(WIN32) #include "windows/serialportlistimplwin32.hpp" #else diff --git a/server/src/os/serialportlist.hpp b/server/src/os/serialportlist.hpp index f8373ee2..6bb7ebfa 100644 --- a/server/src/os/serialportlist.hpp +++ b/server/src/os/serialportlist.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2022 Reinder Feenstra + * Copyright (C) 2022,2024 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -33,6 +33,10 @@ class SerialPortListImpl; namespace Linux { class SerialPortListImplSystemD; } +#elif defined(__linux__) +namespace Linux { + class SerialPortListImplInotify; +} #elif defined(WIN32) namespace Windows { class SerialPortListImplWin32; @@ -46,6 +50,8 @@ class SerialPortList private: #ifdef HAS_LIBSYSTEMD using Impl = Linux::SerialPortListImplSystemD; +#elif defined(__linux__) + using Impl = Linux::SerialPortListImplInotify; #elif defined(WIN32) using Impl = Windows::SerialPortListImplWin32; #else From 18617eb8d316754082df3982bdc1fab2cc0e89af Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Thu, 3 Oct 2024 18:52:17 +0200 Subject: [PATCH 02/39] Removed close world shortcut (ctrl+w/CMD+w) see #174 --- client/src/mainwindow.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/mainwindow.cpp b/client/src/mainwindow.cpp index 4416318b..cdc5d123 100644 --- a/client/src/mainwindow.cpp +++ b/client/src/mainwindow.cpp @@ -171,7 +171,6 @@ MainWindow::MainWindow(QWidget* parent) : if(const ObjectPtr& traintastic = m_connection->traintastic()) traintastic->callMethod("close_world"); }); - m_actionCloseWorld->setShortcut(QKeySequence::Close); menu->addSeparator(); m_actionImportWorld = menu->addAction(Theme::getIcon("world_import"), Locale::tr("qtapp.mainmenu:import_world") + "...", [this]() From 5a9efa3184ad3a2349c749cb5568cb6ddf126dd1 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 6 Oct 2024 22:50:36 +0200 Subject: [PATCH 03/39] added: Object::getEvent() --- server/src/core/object.cpp | 13 ++++++++++++- server/src/core/object.hpp | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/server/src/core/object.cpp b/server/src/core/object.cpp index aab377c9..c78493e9 100644 --- a/server/src/core/object.cpp +++ b/server/src/core/object.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2019-2021,2023 Reinder Feenstra + * Copyright (C) 2019-2021,2023-2024 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -23,6 +23,7 @@ #include "object.hpp" #include "idobject.hpp" #include "subobject.hpp" +#include "abstractevent.hpp" #include "abstractmethod.hpp" #include "abstractproperty.hpp" #include "abstractobjectproperty.hpp" @@ -97,6 +98,16 @@ AbstractVectorProperty* Object::getVectorProperty(std::string_view name) return dynamic_cast(getItem(name)); } +const AbstractEvent* Object::getEvent(std::string_view name) const +{ + return dynamic_cast(getItem(name)); +} + +AbstractEvent* Object::getEvent(std::string_view name) +{ + return dynamic_cast(getItem(name)); +} + void Object::load(WorldLoader& loader, const nlohmann::json& data) { for(auto& [name, value] : data.items()) diff --git a/server/src/core/object.hpp b/server/src/core/object.hpp index c04ceae7..a8d528c7 100644 --- a/server/src/core/object.hpp +++ b/server/src/core/object.hpp @@ -134,6 +134,8 @@ class Object : public std::enable_shared_from_this AbstractObjectProperty* getObjectProperty(std::string_view name); const AbstractVectorProperty* getVectorProperty(std::string_view name) const; AbstractVectorProperty* getVectorProperty(std::string_view name); + const AbstractEvent* getEvent(std::string_view name) const; + AbstractEvent* getEvent(std::string_view name); }; #endif From 43ee9bfc0e7863b10f9bdd22bb921db89ce95330 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 6 Oct 2024 22:51:14 +0200 Subject: [PATCH 04/39] added std::array overload for contains() --- server/src/utils/contains.hpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/src/utils/contains.hpp b/server/src/utils/contains.hpp index 6f761860..671ae1a4 100644 --- a/server/src/utils/contains.hpp +++ b/server/src/utils/contains.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2022 Reinder Feenstra + * Copyright (C) 2022,2024 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -23,8 +23,15 @@ #ifndef TRAINTASTIC_SERVER_UTILS_CONTAINS_HPP #define TRAINTASTIC_SERVER_UTILS_CONTAINS_HPP +#include #include +template +inline bool contains(const std::array& array, T value) +{ + return std::find(array.begin(), array.end(), value) != array.end(); +} + template inline bool contains(const std::vector& vector, T value) { From 2d74852ca38a35dbe2aeb02e0514b7f08f3ef7c2 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 6 Oct 2024 23:01:07 +0200 Subject: [PATCH 05/39] lua: added array's with enum/set names to be used for testing if a metatable name is a valid enum/set --- server/src/lua/enums.hpp | 8 ++++++++ server/src/lua/sets.hpp | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/server/src/lua/enums.hpp b/server/src/lua/enums.hpp index 6e2f6093..8a8b7fb9 100644 --- a/server/src/lua/enums.hpp +++ b/server/src/lua/enums.hpp @@ -79,6 +79,14 @@ struct Enums if constexpr(sizeof...(Ts) != 0) registerValues(L); } + + template + static constexpr std::array getMetaTableNames() + { + return std::array{EnumName::value...}; + } + + inline static const auto metaTableNames = getMetaTableNames(); }; } diff --git a/server/src/lua/sets.hpp b/server/src/lua/sets.hpp index 610ecf60..36599235 100644 --- a/server/src/lua/sets.hpp +++ b/server/src/lua/sets.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2022 Reinder Feenstra + * Copyright (C) 2022,2024 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -48,6 +48,14 @@ struct Sets if constexpr(sizeof...(Ts) != 0) registerValues(L); } + + template + static constexpr std::array getMetaTableNames() + { + return std::array{set_name_v...}; + } + + inline static const auto metaTableNames = getMetaTableNames(); }; } From 515c96ddd24eb05f0a453bc85250b898aacc563e Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 6 Oct 2024 23:08:14 +0200 Subject: [PATCH 06/39] lua: added persistent variable (pv) support this enables saving state between script start/stop and world save/load --- manual/luadoc/globals.json | 6 +- server/src/lua/error.hpp | 8 +- server/src/lua/persistentvariables.cpp | 371 +++++++++++++++++++++++++ server/src/lua/persistentvariables.hpp | 47 ++++ server/src/lua/sandbox.cpp | 17 +- server/src/lua/sandbox.hpp | 1 + server/src/lua/script.cpp | 15 + server/src/lua/script.hpp | 5 +- 8 files changed, 466 insertions(+), 4 deletions(-) create mode 100644 server/src/lua/persistentvariables.cpp create mode 100644 server/src/lua/persistentvariables.hpp diff --git a/manual/luadoc/globals.json b/manual/luadoc/globals.json index ee67139c..920a80c2 100644 --- a/manual/luadoc/globals.json +++ b/manual/luadoc/globals.json @@ -67,6 +67,10 @@ "type": "constant", "since": "0.1" }, + "pv": { + "type": "object", + "since": "0.3" + }, "world": { "type": "object", "since": "0.1" @@ -102,4 +106,4 @@ "type": "library", "since": "0.1" } -} \ No newline at end of file +} diff --git a/server/src/lua/error.hpp b/server/src/lua/error.hpp index 71ba0ab0..199b5dc4 100644 --- a/server/src/lua/error.hpp +++ b/server/src/lua/error.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2021-2023 Reinder Feenstra + * Copyright (C) 2021-2024 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -39,6 +39,12 @@ namespace Lua { [[noreturn]] inline void errorCantSetNonExistingProperty(lua_State* L) { luaL_error(L, "can't set non existing property"); abort(); } [[noreturn]] inline void errorCantSetReadOnlyProperty(lua_State* L) { luaL_error(L, "can't set read only property"); abort(); } +[[noreturn]] inline void errorCantStoreValueAsPersistentVariableUnsupportedType(lua_State* L) +{ + luaL_error(L, "can't store value as persistent variable, unsupported type"); + abort(); +} + [[noreturn]] inline void errorDeadObject(lua_State* L) { luaL_error(L, "dead object"); abort(); } [[noreturn]] inline void errorExpectedNArgumentsGotN(lua_State* L, int expected, int got) { luaL_error(L, "expected %d arguments, got %d", expected, got); abort(); } diff --git a/server/src/lua/persistentvariables.cpp b/server/src/lua/persistentvariables.cpp new file mode 100644 index 00000000..b565849b --- /dev/null +++ b/server/src/lua/persistentvariables.cpp @@ -0,0 +1,371 @@ +/** + * server/src/lua/persistent.cpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2024 Reinder Feenstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "persistentvariables.hpp" +#include "enums.hpp" +#include "error.hpp" +#include "event.hpp" +#include "method.hpp" +#include "object.hpp" +#include "sandbox.hpp" +#include "script.hpp" +#include "sets.hpp" +#include "test.hpp" +#include "vectorproperty.hpp" +#include "../core/abstractmethod.hpp" +#include "../utils/contains.hpp" +#include "../world/world.hpp" + +namespace Lua { + +static const char* metaTableName = "pv"; + +struct PersistentVariablesData +{ + int registryIndex; +}; + +void PersistentVariables::registerType(lua_State* L) +{ + luaL_newmetatable(L, metaTableName); + lua_pushcfunction(L, __index); + lua_setfield(L, -2, "__index"); + lua_pushcfunction(L, __newindex); + lua_setfield(L, -2, "__newindex"); + lua_pushcfunction(L, __gc); + lua_setfield(L, -2, "__gc"); + lua_pop(L, 1); +} + +void PersistentVariables::push(lua_State* L) +{ + auto* pv = static_cast(lua_newuserdatauv(L, sizeof(PersistentVariablesData), 1)); + luaL_getmetatable(L, metaTableName); + lua_setmetatable(L, -2); + lua_newtable(L); + pv->registryIndex = luaL_ref(L, LUA_REGISTRYINDEX); +} + +void PersistentVariables::push(lua_State* L, const nlohmann::json& pv) +{ + push(L); + + if(!pv.is_object()) + { + return; + } + + for(auto item : pv.items()) + { + switch(item.value().type()) + { + case nlohmann::json::value_t::null: + continue; // no need to set nil value + + case nlohmann::json::value_t::boolean: + lua_pushboolean(L, static_cast(item.value())); + break; + + case nlohmann::json::value_t::number_integer: + case nlohmann::json::value_t::number_unsigned: + lua_pushinteger(L, item.value()); + break; + + case nlohmann::json::value_t::number_float: + lua_pushnumber(L, item.value()); + break; + + case nlohmann::json::value_t::string: + { + const std::string s = item.value(); + lua_pushlstring(L, s.data(), s.size()); + break; + } + case nlohmann::json::value_t::object: + { + const auto& value = item.value(); + if(value.contains("type")) + { + const std::string type = value["type"]; + if(type == "object") + { + if(value.contains("id")) + { + const std::string id = value["id"]; + if(auto object = Sandbox::getStateData(L).script().world().getObjectByPath(id)) + { + Object::push(L, object); + break; + } + } + } + else if(type == "vector_property") + { + if(value.contains("object_id") && value.contains("name")) + { + const std::string id = value["object_id"]; + if(auto object = Sandbox::getStateData(L).script().world().getObjectByPath(id)) + { + const std::string name = value["name"]; + if(auto* property = object->getVectorProperty(name); property && property->isScriptReadable()) + { + VectorProperty::push(L, *property); + break; + } + } + } + } + else if(type == "method") + { + if(value.contains("object_id") && value.contains("name")) + { + const std::string id = value["object_id"]; + if(auto object = Sandbox::getStateData(L).script().world().getObjectByPath(id)) + { + const std::string name = value["name"]; + if(auto* method = object->getMethod(name); method && method->isScriptCallable()) + { + Method::push(L, *method); + break; + } + } + } + } + else if(type == "event") + { + if(value.contains("object_id") && value.contains("name")) + { + const std::string id = value["object_id"]; + if(auto object = Sandbox::getStateData(L).script().world().getObjectByPath(id)) + { + const std::string name = value["name"]; + if(auto* event = object->getEvent(name); event && event->isScriptable()) + { + Event::push(L, *event); + break; + } + } + } + } + else if(startsWith(type, "enum.")) + { + if(value.contains("value")) + { + pushEnum(L, type.substr(5).c_str(), value["value"]); + break; + } + } + else if(startsWith(type, "set.")) + { + if(value.contains("value")) + { + pushSet(L, type.substr(4).c_str(), value["value"]); + break; + } + } + } + continue; + } + case nlohmann::json::value_t::array: + case nlohmann::json::value_t::binary: + case nlohmann::json::value_t::discarded: + assert(false); + continue; + } + lua_setfield(L, -2, item.key().c_str()); + } +} + +nlohmann::json PersistentVariables::toJSON(lua_State* L, int index) +{ + auto result = nlohmann::json::object(); + + auto& pv = *static_cast(luaL_checkudata(L, index, metaTableName)); + lua_rawgeti(L, LUA_REGISTRYINDEX, pv.registryIndex); + assert(lua_istable(L, -1)); + + lua_pushnil(L); + while(lua_next(L, -2)) + { + const char* key = lua_tostring(L, -2); + switch(lua_type(L, -1)) + { + case LUA_TNIL: /*[[unlikely]]*/ + result.emplace(key, nullptr); + break; + + case LUA_TBOOLEAN: + result.emplace(key, lua_toboolean(L, -1) != 0); + break; + + case LUA_TNUMBER: + if(lua_isinteger(L, -1)) + { + result.emplace(key, lua_tointeger(L, -1)); + } + else + { + result.emplace(key, lua_tonumber(L, -1)); + } + break; + + case LUA_TSTRING: + result.emplace(key, std::string(lua_tostring(L, -1))); + break; + + case LUA_TUSERDATA: + if(auto object = test<::Object>(L, -1)) + { + auto value = nlohmann::json::object(); + value.emplace("type", "object"); + value.emplace("id", object->getObjectId()); + result.emplace(key, std::move(value)); + break; + } + else if(auto* vectorProperty = VectorProperty::test(L, -1)) + { + auto value = nlohmann::json::object(); + value.emplace("type", "vector_property"); + value.emplace("object_id", vectorProperty->object().getObjectId()); + value.emplace("name", vectorProperty->name()); + result.emplace(key, std::move(value)); + break; + } + else if(auto* method = Method::test(L, -1)) + { + auto value = nlohmann::json::object(); + value.emplace("type", "method"); + value.emplace("object_id", method->object().getObjectId()); + value.emplace("name", method->name()); + result.emplace(key, std::move(value)); + break; + } + else if(auto* event = Event::test(L, -1)) + { + auto value = nlohmann::json::object(); + value.emplace("type", "event"); + value.emplace("object_id", event->object().getObjectId()); + value.emplace("name", event->name()); + result.emplace(key, std::move(value)); + break; + } + else if(lua_getmetatable(L, -1)) + { + lua_getfield(L, -1, "__name"); + std::string_view name = lua_tostring(L, -1); + lua_pop(L, 2); + + if(contains(Enums::metaTableNames, name)) + { + auto value = nlohmann::json::object(); + value.emplace("type", std::string("enum.").append(name)); + value.emplace("value", checkEnum(L, -1, name.data())); + result.emplace(key, std::move(value)); + break; + } + else if(contains(Sets::metaTableNames, name)) + { + auto value = nlohmann::json::object(); + value.emplace("type", std::string("set.").append(name)); + value.emplace("value", checkSet(L, -1, name.data())); + result.emplace(key, std::move(value)); + break; + } + } + assert(false); + break; + + case LUA_TLIGHTUSERDATA: + case LUA_TTABLE: + case LUA_TFUNCTION: + case LUA_TTHREAD: + default: + assert(false); // unsupported + break; + } + lua_pop(L, 1); // pop value + } + + return result; +} + +int PersistentVariables::__index(lua_State* L) +{ + const auto& pv = *static_cast(lua_touserdata(L, 1)); + lua_rawgeti(L, LUA_REGISTRYINDEX, pv.registryIndex); + lua_insert(L, 2); // moves key to 3 + lua_rawget(L, 2); + return 1; +} + +int PersistentVariables::__newindex(lua_State* L) +{ + switch(lua_type(L, 3)) + { + case LUA_TNIL: + case LUA_TBOOLEAN: + case LUA_TNUMBER: + case LUA_TSTRING: + break; // supported + + case LUA_TUSERDATA: + if(test<::Object>(L, 3) || VectorProperty::test(L, 3) || Method::test(L, 3) || Event::test(L, 3)) + { + break; // supported + } + else if(lua_getmetatable(L, 3)) + { + lua_getfield(L, -1, "__name"); + std::string_view name = lua_tostring(L, -1); + lua_pop(L, 2); + + if(contains(Enums::metaTableNames, name) || contains(Sets::metaTableNames, name)) + { + break; // supported + } + } + errorCantStoreValueAsPersistentVariableUnsupportedType(L); + + case LUA_TLIGHTUSERDATA: + case LUA_TTABLE: + case LUA_TFUNCTION: + case LUA_TTHREAD: + default: + errorCantStoreValueAsPersistentVariableUnsupportedType(L); + } + + const auto& pv = *static_cast(lua_touserdata(L, 1)); + lua_rawgeti(L, LUA_REGISTRYINDEX, pv.registryIndex); + lua_insert(L, 2); // moves key to 3 and value to 4 + lua_rawset(L, 2); + return 0; +} + +int PersistentVariables::__gc(lua_State* L) +{ + auto* pv = static_cast(lua_touserdata(L, 1)); + luaL_unref(L, LUA_REGISTRYINDEX, pv->registryIndex); + pv->~PersistentVariablesData(); + return 0; +} + +} diff --git a/server/src/lua/persistentvariables.hpp b/server/src/lua/persistentvariables.hpp new file mode 100644 index 00000000..601d61be --- /dev/null +++ b/server/src/lua/persistentvariables.hpp @@ -0,0 +1,47 @@ +/** + * server/src/lua/persistentvariables.hpp + * + * This file is part of the traintastic source code. + * + * Copyright (C) 2024 Reinder Feenstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TRAINTASTIC_SERVER_LUA_PERSISTENTVARIABLES_HPP +#define TRAINTASTIC_SERVER_LUA_PERSISTENTVARIABLES_HPP + +#include +#include + +namespace Lua { + +class PersistentVariables +{ +private: + static int __index(lua_State* L); + static int __newindex(lua_State* L); + static int __gc(lua_State* L); + +public: + static void registerType(lua_State* L); + static void push(lua_State* L); + static void push(lua_State* L, const nlohmann::json& pv); + static nlohmann::json toJSON(lua_State* L, int index); +}; + +} + +#endif diff --git a/server/src/lua/sandbox.cpp b/server/src/lua/sandbox.cpp index 0aa03535..43264e7e 100644 --- a/server/src/lua/sandbox.cpp +++ b/server/src/lua/sandbox.cpp @@ -26,6 +26,7 @@ #include "event.hpp" #include "eventhandler.hpp" #include "log.hpp" +#include "persistentvariables.hpp" #include "class.hpp" #include "to.hpp" #include "type.hpp" @@ -43,7 +44,7 @@ #define LUA_SANDBOX "_sandbox" #define LUA_SANDBOX_GLOBALS "_sandbox_globals" -constexpr std::array readOnlyGlobals = {{ +constexpr std::array readOnlyGlobals = {{ // Lua baselib: "assert", "type", @@ -67,6 +68,7 @@ constexpr std::array readOnlyGlobals = {{ // Objects: "world", "log", + "pv", // Functions: "is_instance", // Type info: @@ -127,6 +129,7 @@ namespace Lua { void Sandbox::close(lua_State* L) { + syncPersistentVariables(L); delete *static_cast(lua_getextraspace(L)); // free state data lua_close(L); } @@ -162,6 +165,7 @@ SandboxPtr Sandbox::create(Script& script) *static_cast(lua_getextraspace(L)) = new StateData(script); // register types: + PersistentVariables::registerType(L); Enums::registerTypes(L); Sets::registerTypes(L); Object::registerTypes(L); @@ -226,6 +230,10 @@ SandboxPtr Sandbox::create(Script& script) Log::push(L); lua_setfield(L, -2, "log"); + // add persistent variables: + PersistentVariables::push(L, script.m_persistentVariables); + lua_setfield(L, -2, "pv"); + // add class types: lua_newtable(L); Class::registerValues(L); @@ -268,6 +276,13 @@ int Sandbox::getGlobal(lua_State* L, const char* name) return type; } +void Sandbox::syncPersistentVariables(lua_State* L) +{ + getGlobal(L, "pv"); + getStateData(L).script().m_persistentVariables = PersistentVariables::toJSON(L, -1); + lua_pop(L, 1); +} + int Sandbox::pcall(lua_State* L, int nargs, int nresults, int errfunc) { // check if the function has _ENV as first upvalue diff --git a/server/src/lua/sandbox.hpp b/server/src/lua/sandbox.hpp index 972aba50..7962999e 100644 --- a/server/src/lua/sandbox.hpp +++ b/server/src/lua/sandbox.hpp @@ -130,6 +130,7 @@ class Sandbox static StateData& getStateData(lua_State* L); static int getGlobal(lua_State* L, const char* name); static int pcall(lua_State* L, int nargs = 0, int nresults = 0, int errfunc = 0); + static void syncPersistentVariables(lua_State* L); }; } diff --git a/server/src/lua/script.cpp b/server/src/lua/script.cpp index dafae229..62a7ecd1 100644 --- a/server/src/lua/script.cpp +++ b/server/src/lua/script.cpp @@ -43,6 +43,7 @@ constexpr std::string_view dotLua = ".lua"; Script::Script(World& world, std::string_view _id) : IdObject(world, _id), m_sandbox{nullptr, nullptr}, + m_persistentVariables{nlohmann::json::object()}, name{this, "name", std::string(_id), PropertyFlags::ReadWrite | PropertyFlags::Store}, disabled{this, "disabled", false, PropertyFlags::ReadWrite | PropertyFlags::NoStore | PropertyFlags::NoScript, [this](bool value) @@ -92,6 +93,11 @@ void Script::load(WorldLoader& loader, const nlohmann::json& data) std::string s; if(loader.readFile(std::filesystem::path(scripts) / m_basename += dotLua, s)) code.loadJSON(s); + + if(const auto stateData = loader.getState(id); stateData.contains("persistent_variables")) + { + m_persistentVariables = stateData["persistent_variables"]; + } } void Script::save(WorldSaver& saver, nlohmann::json& data, nlohmann::json& stateData) const @@ -103,6 +109,15 @@ void Script::save(WorldSaver& saver, nlohmann::json& data, nlohmann::json& state m_basename = id; saver.writeFile(std::filesystem::path(scripts) / m_basename += dotLua, code); + + if(m_sandbox) + { + Sandbox::syncPersistentVariables(m_sandbox.get()); + } + if(!m_persistentVariables.empty()) + { + stateData["persistent_variables"] = m_persistentVariables; + } } void Script::addToWorld() diff --git a/server/src/lua/script.hpp b/server/src/lua/script.hpp index aca5c83a..88d9bb7d 100644 --- a/server/src/lua/script.hpp +++ b/server/src/lua/script.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2019-2023 Reinder Feenstra + * Copyright (C) 2019-2024 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -32,11 +32,14 @@ namespace Lua { class Script : public IdObject { + friend class Sandbox; + private: mutable std::string m_basename; //!< filename on disk for script protected: SandboxPtr m_sandbox; + nlohmann::json m_persistentVariables; void load(WorldLoader& loader, const nlohmann::json& data) final; void save(WorldSaver& saver, nlohmann::json& data, nlohmann::json& stateData) const final; From 47485f7513c37204b563f747f28367a4248ffdb9 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 6 Oct 2024 23:16:55 +0200 Subject: [PATCH 07/39] lua: fix: use single lua value per method, else compare don't work --- server/src/lua/method.cpp | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/server/src/lua/method.cpp b/server/src/lua/method.cpp index 9ec1fc4b..986212bc 100644 --- a/server/src/lua/method.cpp +++ b/server/src/lua/method.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2019-2020,2022-2023 Reinder Feenstra + * Copyright (C) 2019-2020,2022-2024 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -30,6 +30,8 @@ namespace Lua { +constexpr char const* methodsGlobal = "methods"; + struct MethodData { ObjectPtrWeak object; @@ -64,9 +66,18 @@ AbstractMethod* Method::test(lua_State* L, int index) void Method::push(lua_State* L, AbstractMethod& value) { - new(lua_newuserdata(L, sizeof(MethodData))) MethodData(value); - luaL_getmetatable(L, metaTableName); - lua_setmetatable(L, -2); + lua_getglobal(L, methodsGlobal); + lua_rawgetp(L, -1, &value); + if(lua_isnil(L, -1)) // method not in table + { + lua_pop(L, 1); // remove nil + new(lua_newuserdata(L, sizeof(MethodData))) MethodData(value); + luaL_setmetatable(L, metaTableName); + lua_pushvalue(L, -1); // copy userdata on stack + lua_rawsetp(L, -3, &value); // add method to table + } + lua_insert(L, lua_gettop(L) - 1); // swap table and userdata + lua_pop(L, 1); // remove table } void Method::registerType(lua_State* L) @@ -77,6 +88,15 @@ void Method::registerType(lua_State* L) lua_pushcfunction(L, __call); lua_setfield(L, -2, "__call"); lua_pop(L, 1); + + // weak table for method userdata: + lua_newtable(L); + lua_newtable(L); // metatable + lua_pushliteral(L, "__mode"); + lua_pushliteral(L, "v"); + lua_rawset(L, -3); + lua_setmetatable(L, -2); + lua_setglobal(L, methodsGlobal); } int Method::__gc(lua_State* L) From 04b5f3475dfbeb358448ba018c8fdeba971d99ba Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 6 Oct 2024 23:21:00 +0200 Subject: [PATCH 08/39] lua: fix: use single lua value per event, else compares don't work --- server/src/lua/event.cpp | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/server/src/lua/event.cpp b/server/src/lua/event.cpp index d83f9821..e9920dbc 100644 --- a/server/src/lua/event.cpp +++ b/server/src/lua/event.cpp @@ -31,6 +31,8 @@ namespace Lua { +constexpr char const* eventsGlobal = "events"; + struct EventData { ObjectPtrWeak object; @@ -65,9 +67,18 @@ AbstractEvent* Event::test(lua_State* L, int index) void Event::push(lua_State* L, AbstractEvent& value) { - new(lua_newuserdata(L, sizeof(EventData))) EventData(value); - luaL_getmetatable(L, metaTableName); - lua_setmetatable(L, -2); + lua_getglobal(L, eventsGlobal); + lua_rawgetp(L, -1, &value); + if(lua_isnil(L, -1)) // event not in table + { + lua_pop(L, 1); // remove nil + new(lua_newuserdata(L, sizeof(EventData))) EventData(value); + luaL_setmetatable(L, metaTableName); + lua_pushvalue(L, -1); // copy userdata on stack + lua_rawsetp(L, -3, &value); // add event to table + } + lua_insert(L, lua_gettop(L) - 1); // swap table and userdata + lua_pop(L, 1); // remove table } void Event::registerType(lua_State* L) @@ -80,6 +91,15 @@ void Event::registerType(lua_State* L) lua_pushcfunction(L, __gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); + + // weak table for event userdata: + lua_newtable(L); + lua_newtable(L); // metatable + lua_pushliteral(L, "__mode"); + lua_pushliteral(L, "v"); + lua_rawset(L, -3); + lua_setmetatable(L, -2); + lua_setglobal(L, eventsGlobal); } int Event::__index(lua_State* L) From de2823e65aac7492ba6e6c372a85b620bd378b87 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 6 Oct 2024 23:24:18 +0200 Subject: [PATCH 09/39] lua: fix: use single lua value per vector property, else compares don't work --- server/src/lua/vectorproperty.cpp | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/server/src/lua/vectorproperty.cpp b/server/src/lua/vectorproperty.cpp index 8f054e1e..1ba5184f 100644 --- a/server/src/lua/vectorproperty.cpp +++ b/server/src/lua/vectorproperty.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2023 Reinder Feenstra + * Copyright (C) 2023-2024 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -29,6 +29,8 @@ namespace Lua { +constexpr char const* vectorPropertiesGlobal = "vector_properties"; + struct VectorPropertyData { ObjectPtrWeak object; @@ -63,9 +65,18 @@ AbstractVectorProperty* VectorProperty::test(lua_State* L, int index) void VectorProperty::push(lua_State* L, AbstractVectorProperty& value) { - new(lua_newuserdata(L, sizeof(VectorPropertyData))) VectorPropertyData(value); - luaL_getmetatable(L, metaTableName); - lua_setmetatable(L, -2); + lua_getglobal(L, vectorPropertiesGlobal); + lua_rawgetp(L, -1, &value); + if(lua_isnil(L, -1)) // vector property not in table + { + lua_pop(L, 1); // remove nil + new(lua_newuserdata(L, sizeof(VectorPropertyData))) VectorPropertyData(value); + luaL_setmetatable(L, metaTableName); + lua_pushvalue(L, -1); // copy userdata on stack + lua_rawsetp(L, -3, &value); // add vector property to table + } + lua_insert(L, lua_gettop(L) - 1); // swap table and userdata + lua_pop(L, 1); // remove table } void VectorProperty::registerType(lua_State* L) @@ -78,6 +89,15 @@ void VectorProperty::registerType(lua_State* L) lua_pushcfunction(L, __gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); + + // weak table for vector property userdata: + lua_newtable(L); + lua_newtable(L); // metatable + lua_pushliteral(L, "__mode"); + lua_pushliteral(L, "v"); + lua_rawset(L, -3); + lua_setmetatable(L, -2); + lua_setglobal(L, vectorPropertiesGlobal); } int VectorProperty::__index(lua_State* L) From 46bf7183beff9e4f86e84cb81e8120f923849376 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 6 Oct 2024 23:42:27 +0200 Subject: [PATCH 10/39] test: lua persistent variables --- .../test/lua/script/persistentvariables.cpp | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 server/test/lua/script/persistentvariables.cpp diff --git a/server/test/lua/script/persistentvariables.cpp b/server/test/lua/script/persistentvariables.cpp new file mode 100644 index 00000000..daf4731b --- /dev/null +++ b/server/test/lua/script/persistentvariables.cpp @@ -0,0 +1,326 @@ +/** + * server/test/lua/script/persistentvariables.cpp + * + * This file is part of the traintastic test suite. + * + * Copyright (C) 2024 Reinder Feenstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include "../../../src/lua/scriptlist.hpp" +#include "../../../src/train/train.hpp" +#include "../../../src/train/trainlist.hpp" +#include "../../../src/utils/endswith.hpp" +#include "../../../src/world/world.hpp" + +TEST_CASE("Lua script: pv - save/restore - bool", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = true"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test == true)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - int", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = 42"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test == 42)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - float", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = math.pi"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test == math.pi)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - string", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = 'traintastic'"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test == 'traintastic')"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - enum", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = enum.world_event.RUN"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test == enum.world_event.RUN)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - set", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = set.world_state.RUN"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test == set.world_state.RUN)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - object", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = world"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test == world)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - vector property", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + auto train = world->trains->create(); + REQUIRE(train); + train->id = "train"; + + // set pv: + script->code = "pv.test = world.get_object('train').blocks"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test == world.get_object('train').blocks)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); + train.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - method", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = world.stop"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test == world.stop)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - event", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = world.on_event"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test == world.on_event)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - unsupported - function", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = function(a, b)\nreturn a+b\nend"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Error); + REQUIRE(endsWith(script->error.value(), "can't store value as persistent variable, unsupported type")); + + script.reset(); +} + +TEST_CASE("Lua script: pv - unsupported - table", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = {}"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Error); + REQUIRE(endsWith(script->error.value(), "can't store value as persistent variable, unsupported type")); + + script.reset(); +} From 357a13ef98ebc81d8ea34000b693edff1a70b4f5 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Mon, 7 Oct 2024 22:29:22 +0200 Subject: [PATCH 11/39] fix: missing includes --- server/test/lua/script/persistentvariables.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/test/lua/script/persistentvariables.cpp b/server/test/lua/script/persistentvariables.cpp index daf4731b..f2bbd383 100644 --- a/server/test/lua/script/persistentvariables.cpp +++ b/server/test/lua/script/persistentvariables.cpp @@ -21,6 +21,8 @@ */ #include +#include "../../../src/core/method.tpp" +#include "../../../src/core/objectproperty.tpp" #include "../../../src/lua/scriptlist.hpp" #include "../../../src/train/train.hpp" #include "../../../src/train/trainlist.hpp" From 0703a5cd5b26445ec75699561f5df94a7db8c205 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Mon, 7 Oct 2024 23:15:13 +0200 Subject: [PATCH 12/39] fix: constexpr -> inline const (constexpr doesn't work for older compilers) --- server/src/lua/enums.hpp | 2 +- server/src/lua/sets.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/lua/enums.hpp b/server/src/lua/enums.hpp index 8a8b7fb9..e7e2370b 100644 --- a/server/src/lua/enums.hpp +++ b/server/src/lua/enums.hpp @@ -81,7 +81,7 @@ struct Enums } template - static constexpr std::array getMetaTableNames() + inline static const std::array getMetaTableNames() { return std::array{EnumName::value...}; } diff --git a/server/src/lua/sets.hpp b/server/src/lua/sets.hpp index 36599235..61cca598 100644 --- a/server/src/lua/sets.hpp +++ b/server/src/lua/sets.hpp @@ -50,7 +50,7 @@ struct Sets } template - static constexpr std::array getMetaTableNames() + inline static const std::array getMetaTableNames() { return std::array{set_name_v...}; } From b7bc0acb51e7649d0194a5f88e57db045b9e1d46 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Mon, 7 Oct 2024 23:50:28 +0200 Subject: [PATCH 13/39] fix: missing includes --- server/src/lua/sets.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/lua/sets.hpp b/server/src/lua/sets.hpp index 61cca598..6a5dd970 100644 --- a/server/src/lua/sets.hpp +++ b/server/src/lua/sets.hpp @@ -24,6 +24,8 @@ #define TRAINTASTIC_SERVER_LUA_SETS_HPP #include "set.hpp" +#include +#include #include "../../src/set/worldstate.hpp" #define LUA_SETS \ From ae1902c18dcca3350d3720f03808c6f11e9f3eed Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Mon, 7 Oct 2024 23:52:54 +0200 Subject: [PATCH 14/39] lua: added clear persistent variables toolbar action --- .../gfx/dark/clear_persistent_variables.svg | 101 +++++++++++++++++ client/gfx/dark/dark.qrc | 1 + .../gfx/light/clear_persistent_variables.svg | 102 ++++++++++++++++++ client/gfx/light/light.qrc | 1 + .../src/widget/object/luascripteditwidget.cpp | 12 +++ server/src/lua/script.cpp | 13 ++- server/src/lua/script.hpp | 1 + shared/src/traintastic/enum/logmessage.hpp | 1 + shared/translations/en-us.json | 8 ++ 9 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 client/gfx/dark/clear_persistent_variables.svg create mode 100644 client/gfx/light/clear_persistent_variables.svg diff --git a/client/gfx/dark/clear_persistent_variables.svg b/client/gfx/dark/clear_persistent_variables.svg new file mode 100644 index 00000000..d67ae56a --- /dev/null +++ b/client/gfx/dark/clear_persistent_variables.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/client/gfx/dark/dark.qrc b/client/gfx/dark/dark.qrc index d5ee3158..36f3e615 100644 --- a/client/gfx/dark/dark.qrc +++ b/client/gfx/dark/dark.qrc @@ -92,5 +92,6 @@ board_tile.rail.nx_button.svg board_tile.misc.switch.svg board_tile.misc.label.svg + clear_persistent_variables.svg diff --git a/client/gfx/light/clear_persistent_variables.svg b/client/gfx/light/clear_persistent_variables.svg new file mode 100644 index 00000000..3526da55 --- /dev/null +++ b/client/gfx/light/clear_persistent_variables.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/client/gfx/light/light.qrc b/client/gfx/light/light.qrc index da7d6ba9..041892c6 100644 --- a/client/gfx/light/light.qrc +++ b/client/gfx/light/light.qrc @@ -66,5 +66,6 @@ board_tile.rail.nx_button.svg board_tile.misc.switch.svg board_tile.misc.label.svg + clear_persistent_variables.svg diff --git a/client/src/widget/object/luascripteditwidget.cpp b/client/src/widget/object/luascripteditwidget.cpp index 6f8cc798..7cc52274 100644 --- a/client/src/widget/object/luascripteditwidget.cpp +++ b/client/src/widget/object/luascripteditwidget.cpp @@ -29,6 +29,7 @@ #include #include #include +#include "../../misc/methodaction.hpp" #include "../../network/object.hpp" #include "../../network/property.hpp" #include "../../network/method.hpp" @@ -108,6 +109,17 @@ void LuaScriptEditWidget::buildForm() m_stop->setEnabled(value.toBool()); }); + + if(auto* method = m_object->getMethod("clear_persistent_variables")) + { + QWidget* spacer = new QWidget(this); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + spacer->show(); + toolbar->addWidget(spacer); + + toolbar->addAction(new MethodAction(Theme::getIcon("clear_persistent_variables"), *method, this)); + } + QWidget* spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); spacer->show(); diff --git a/server/src/lua/script.cpp b/server/src/lua/script.cpp index 62a7ecd1..a3647f19 100644 --- a/server/src/lua/script.cpp +++ b/server/src/lua/script.cpp @@ -66,6 +66,13 @@ Script::Script(World& world, std::string_view _id) : if(state == LuaScriptState::Running) stopSandbox(); }} + , clearPersistentVariables{*this, "clear_persistent_variables", + [this]() + { + m_persistentVariables = nlohmann::json::object(); + Log::log(*this, LogMessage::I9003_CLEARED_PERSISTENT_VARIABLES); + updateEnabled(); + }} { Attributes::addDisplayName(name, DisplayName::Object::name); Attributes::addEnabled(name, false); @@ -81,6 +88,8 @@ Script::Script(World& world, std::string_view _id) : m_interfaceItems.add(start); Attributes::addEnabled(stop, false); m_interfaceItems.add(stop); + Attributes::addEnabled(clearPersistentVariables, false); + m_interfaceItems.add(clearPersistentVariables); updateEnabled(); } @@ -171,14 +180,16 @@ void Script::worldEvent(WorldState worldState, WorldEvent worldEvent) void Script::updateEnabled() { const bool editable = contains(m_world.state.value(), WorldState::Edit) && state != LuaScriptState::Running; + const bool stoppedOrError = (state == LuaScriptState::Stopped) || (state == LuaScriptState::Error); Attributes::setEnabled(id, editable); Attributes::setEnabled(name, editable); Attributes::setEnabled(disabled, editable); Attributes::setEnabled(code, editable); - Attributes::setEnabled(start, state == LuaScriptState::Stopped || state == LuaScriptState::Error); + Attributes::setEnabled(start, stoppedOrError); Attributes::setEnabled(stop, state == LuaScriptState::Running); + Attributes::setEnabled(clearPersistentVariables, stoppedOrError && !m_persistentVariables.empty()); } void Script::setState(LuaScriptState value) diff --git a/server/src/lua/script.hpp b/server/src/lua/script.hpp index 88d9bb7d..35ffdebf 100644 --- a/server/src/lua/script.hpp +++ b/server/src/lua/script.hpp @@ -68,6 +68,7 @@ class Script : public IdObject Property error; ::Method start; ::Method stop; + ::Method clearPersistentVariables; }; } diff --git a/shared/src/traintastic/enum/logmessage.hpp b/shared/src/traintastic/enum/logmessage.hpp index 2d0e1142..8383e05b 100644 --- a/shared/src/traintastic/enum/logmessage.hpp +++ b/shared/src/traintastic/enum/logmessage.hpp @@ -90,6 +90,7 @@ enum class LogMessage : uint32_t I2005_X = LogMessageOffset::info + 2005, I9001_STOPPED_SCRIPT = LogMessageOffset::info + 9001, I9002_X = LogMessageOffset::info + 9002, //!< Lua version + I9003_CLEARED_PERSISTENT_VARIABLES = LogMessageOffset::info + 9003, I9999_X = LogMessageOffset::info + 9999, // Notice: diff --git a/shared/translations/en-us.json b/shared/translations/en-us.json index c34cdf44..c40c0808 100644 --- a/shared/translations/en-us.json +++ b/shared/translations/en-us.json @@ -6415,5 +6415,13 @@ { "term": "settings:load_last_world_on_startup", "definition": "Load last world on startup" + }, + { + "term": "message:I9003", + "definition": "Cleared persistent variables" + }, + { + "term": "lua.script:clear_persistent_variables", + "definition": "Clear persistent variables" } ] From a720b794bc4aaf329ecf7e1f8c9fc1d13a34d7fd Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sat, 12 Oct 2024 10:52:20 +0200 Subject: [PATCH 15/39] train: blocks now script readable --- manual/luadoc/object/train.json | 1 + server/src/train/train.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/manual/luadoc/object/train.json b/manual/luadoc/object/train.json index c2f9ebec..f32d7b5e 100644 --- a/manual/luadoc/object/train.json +++ b/manual/luadoc/object/train.json @@ -6,6 +6,7 @@ "powered": {}, "active": {}, "mode": {}, + "blocks": {}, "on_block_assigned": { "parameters": [ { diff --git a/server/src/train/train.cpp b/server/src/train/train.cpp index 60fd9617..8a35aac1 100644 --- a/server/src/train/train.cpp +++ b/server/src/train/train.cpp @@ -144,7 +144,7 @@ Train::Train(World& world, std::string_view _id) : }, std::bind(&Train::setTrainActive, this, std::placeholders::_1)}, mode{this, "mode", TrainMode::ManualUnprotected, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}, - blocks{*this, "blocks", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState}, + blocks{*this, "blocks", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}, notes{this, "notes", "", PropertyFlags::ReadWrite | PropertyFlags::Store} , onBlockAssigned{*this, "on_block_assigned", EventFlags::Scriptable} , onBlockReserved{*this, "on_block_reserved", EventFlags::Scriptable} From 86aa49ea457d7d6543b6ce27a7f9579e4d7a5ff0 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sat, 12 Oct 2024 10:55:54 +0200 Subject: [PATCH 16/39] lua: persistent variables now supports tables --- server/src/lua/error.hpp | 6 + server/src/lua/persistentvariables.cpp | 552 ++++++++++-------- server/src/lua/persistentvariables.hpp | 6 +- server/src/lua/sandbox.cpp | 9 +- server/src/lua/script.cpp | 3 +- .../test/lua/script/persistentvariables.cpp | 380 +++++++++++- 6 files changed, 715 insertions(+), 241 deletions(-) diff --git a/server/src/lua/error.hpp b/server/src/lua/error.hpp index 199b5dc4..22550aac 100644 --- a/server/src/lua/error.hpp +++ b/server/src/lua/error.hpp @@ -56,6 +56,12 @@ namespace Lua { [[noreturn]] inline void errorInternal(lua_State* L) { luaL_error(L, "internal error"); abort(); } +[[noreturn]] inline void errorTableContainsRecursion(lua_State* L) +{ + luaL_error(L, "table contains recursion"); + abort(); +} + [[noreturn]] inline void errorTableIsReadOnly(lua_State* L) { luaL_error(L, "table is readonly"); abort(); } } diff --git a/server/src/lua/persistentvariables.cpp b/server/src/lua/persistentvariables.cpp index b565849b..4dca0c6b 100644 --- a/server/src/lua/persistentvariables.cpp +++ b/server/src/lua/persistentvariables.cpp @@ -21,6 +21,7 @@ */ #include "persistentvariables.hpp" +#include #include "enums.hpp" #include "error.hpp" #include "event.hpp" @@ -37,6 +38,43 @@ namespace Lua { +static void checkTableRecursion(lua_State* L, std::vector& indices) +{ + const int index = indices.back(); + assert(lua_istable(L, index)); + + lua_pushnil(L); + while(lua_next(L, index)) + { + if(lua_istable(L, -1)) + { + if(std::find_if(indices.begin(), indices.end(), + [L](int idx) + { + return lua_rawequal(L, idx, -1); + }) != indices.end()) + { + errorTableContainsRecursion(L); + } + + indices.push_back(lua_gettop(L)); + checkTableRecursion(L, indices); + indices.pop_back(); + } + lua_pop(L, 1); + } +} + +static void checkTableRecursion(lua_State* L, int index) +{ + assert(lua_istable(L, index)); + + std::vector indices; + indices.push_back(lua_gettop(L)); + + checkTableRecursion(L, indices); +} + static const char* metaTableName = "pv"; struct PersistentVariablesData @@ -51,11 +89,18 @@ void PersistentVariables::registerType(lua_State* L) lua_setfield(L, -2, "__index"); lua_pushcfunction(L, __newindex); lua_setfield(L, -2, "__newindex"); + lua_pushcfunction(L, __len); + lua_setfield(L, -2, "__len"); lua_pushcfunction(L, __gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); } +bool PersistentVariables::test(lua_State* L, int index) +{ + return luaL_testudata(L, index, metaTableName); +} + void PersistentVariables::push(lua_State* L) { auto* pv = static_cast(lua_newuserdatauv(L, sizeof(PersistentVariablesData), 1)); @@ -65,247 +110,246 @@ void PersistentVariables::push(lua_State* L) pv->registryIndex = luaL_ref(L, LUA_REGISTRYINDEX); } -void PersistentVariables::push(lua_State* L, const nlohmann::json& pv) +void PersistentVariables::push(lua_State* L, const nlohmann::json& value) { - push(L); - - if(!pv.is_object()) + switch(value.type()) { - return; - } + case nlohmann::json::value_t::null: + return lua_pushnil(L); - for(auto item : pv.items()) - { - switch(item.value().type()) + case nlohmann::json::value_t::boolean: + return lua_pushboolean(L, static_cast(value)); + + case nlohmann::json::value_t::number_integer: + case nlohmann::json::value_t::number_unsigned: + return lua_pushinteger(L, value); + + case nlohmann::json::value_t::number_float: + return lua_pushnumber(L, value); + + case nlohmann::json::value_t::string: { - case nlohmann::json::value_t::null: - continue; // no need to set nil value - - case nlohmann::json::value_t::boolean: - lua_pushboolean(L, static_cast(item.value())); - break; - - case nlohmann::json::value_t::number_integer: - case nlohmann::json::value_t::number_unsigned: - lua_pushinteger(L, item.value()); - break; - - case nlohmann::json::value_t::number_float: - lua_pushnumber(L, item.value()); - break; - - case nlohmann::json::value_t::string: + const std::string s = value; + lua_pushlstring(L, s.data(), s.size()); + return; + } + case nlohmann::json::value_t::object: + { + if(value.contains("type")) { - const std::string s = item.value(); - lua_pushlstring(L, s.data(), s.size()); - break; - } - case nlohmann::json::value_t::object: - { - const auto& value = item.value(); - if(value.contains("type")) + const std::string type = value["type"]; + if(type == "object") { - const std::string type = value["type"]; - if(type == "object") + if(value.contains("id")) { - if(value.contains("id")) + const std::string id = value["id"]; + if(auto object = Sandbox::getStateData(L).script().world().getObjectByPath(id)) { - const std::string id = value["id"]; - if(auto object = Sandbox::getStateData(L).script().world().getObjectByPath(id)) + return Object::push(L, object); + } + return lua_pushnil(L); + } + } + else if(type == "vector_property") + { + if(value.contains("object_id") && value.contains("name")) + { + const std::string id = value["object_id"]; + if(auto object = Sandbox::getStateData(L).script().world().getObjectByPath(id)) + { + const std::string name = value["name"]; + if(auto* property = object->getVectorProperty(name); property && property->isScriptReadable()) { - Object::push(L, object); - break; + return VectorProperty::push(L, *property); } - } - } - else if(type == "vector_property") - { - if(value.contains("object_id") && value.contains("name")) - { - const std::string id = value["object_id"]; - if(auto object = Sandbox::getStateData(L).script().world().getObjectByPath(id)) - { - const std::string name = value["name"]; - if(auto* property = object->getVectorProperty(name); property && property->isScriptReadable()) - { - VectorProperty::push(L, *property); - break; - } - } - } - } - else if(type == "method") - { - if(value.contains("object_id") && value.contains("name")) - { - const std::string id = value["object_id"]; - if(auto object = Sandbox::getStateData(L).script().world().getObjectByPath(id)) - { - const std::string name = value["name"]; - if(auto* method = object->getMethod(name); method && method->isScriptCallable()) - { - Method::push(L, *method); - break; - } - } - } - } - else if(type == "event") - { - if(value.contains("object_id") && value.contains("name")) - { - const std::string id = value["object_id"]; - if(auto object = Sandbox::getStateData(L).script().world().getObjectByPath(id)) - { - const std::string name = value["name"]; - if(auto* event = object->getEvent(name); event && event->isScriptable()) - { - Event::push(L, *event); - break; - } - } - } - } - else if(startsWith(type, "enum.")) - { - if(value.contains("value")) - { - pushEnum(L, type.substr(5).c_str(), value["value"]); - break; - } - } - else if(startsWith(type, "set.")) - { - if(value.contains("value")) - { - pushSet(L, type.substr(4).c_str(), value["value"]); - break; + return lua_pushnil(L); } } } - continue; + else if(type == "method") + { + if(value.contains("object_id") && value.contains("name")) + { + const std::string id = value["object_id"]; + if(auto object = Sandbox::getStateData(L).script().world().getObjectByPath(id)) + { + const std::string name = value["name"]; + if(auto* method = object->getMethod(name); method && method->isScriptCallable()) + { + return Method::push(L, *method); + } + return lua_pushnil(L); + } + } + } + else if(type == "event") + { + if(value.contains("object_id") && value.contains("name")) + { + const std::string id = value["object_id"]; + if(auto object = Sandbox::getStateData(L).script().world().getObjectByPath(id)) + { + const std::string name = value["name"]; + if(auto* event = object->getEvent(name); event && event->isScriptable()) + { + return Event::push(L, *event); + } + return lua_pushnil(L); + } + } + } + else if(type == "pv") + { + push(L); + if(value.contains("items")) + { + for(auto item : value["items"]) + { + if(item.is_object()) /*[[likely]]*/ + { + push(L, item["key"]); + push(L, item["value"]); + lua_settable(L, -3); + } + } + } + return; + } + else if(startsWith(type, "enum.")) + { + if(value.contains("value")) + { + return pushEnum(L, type.substr(5).c_str(), value["value"]); + } + } + else if(startsWith(type, "set.")) + { + if(value.contains("value")) + { + return pushSet(L, type.substr(4).c_str(), value["value"]); + } + } } - case nlohmann::json::value_t::array: - case nlohmann::json::value_t::binary: - case nlohmann::json::value_t::discarded: - assert(false); - continue; + break; } - lua_setfield(L, -2, item.key().c_str()); + case nlohmann::json::value_t::array: + assert(false); + case nlohmann::json::value_t::binary: + case nlohmann::json::value_t::discarded: + break; } + assert(false); + errorInternal(L); } nlohmann::json PersistentVariables::toJSON(lua_State* L, int index) { - auto result = nlohmann::json::object(); - - auto& pv = *static_cast(luaL_checkudata(L, index, metaTableName)); - lua_rawgeti(L, LUA_REGISTRYINDEX, pv.registryIndex); - assert(lua_istable(L, -1)); - - lua_pushnil(L); - while(lua_next(L, -2)) + switch(lua_type(L, index)) { - const char* key = lua_tostring(L, -2); - switch(lua_type(L, -1)) - { - case LUA_TNIL: /*[[unlikely]]*/ - result.emplace(key, nullptr); - break; + case LUA_TNIL: /*[[unlikely]]*/ + return nullptr; - case LUA_TBOOLEAN: - result.emplace(key, lua_toboolean(L, -1) != 0); - break; + case LUA_TBOOLEAN: + return (lua_toboolean(L, index) != 0); - case LUA_TNUMBER: - if(lua_isinteger(L, -1)) + case LUA_TNUMBER: + if(lua_isinteger(L, index)) + { + return lua_tointeger(L, index); + } + return lua_tonumber(L, index); + + case LUA_TSTRING: + return lua_tostring(L, index); + + case LUA_TUSERDATA: + if(auto object = Lua::test<::Object>(L, index)) + { + auto value = nlohmann::json::object(); + value.emplace("type", "object"); + value.emplace("id", object->getObjectId()); + return value; + } + else if(auto* vectorProperty = VectorProperty::test(L, index)) + { + auto value = nlohmann::json::object(); + value.emplace("type", "vector_property"); + value.emplace("object_id", vectorProperty->object().getObjectId()); + value.emplace("name", vectorProperty->name()); + return value; + } + else if(auto* method = Method::test(L, index)) + { + auto value = nlohmann::json::object(); + value.emplace("type", "method"); + value.emplace("object_id", method->object().getObjectId()); + value.emplace("name", method->name()); + return value; + } + else if(auto* event = Event::test(L, index)) + { + auto value = nlohmann::json::object(); + value.emplace("type", "event"); + value.emplace("object_id", event->object().getObjectId()); + value.emplace("name", event->name()); + return value; + } + else if(test(L, index)) + { + auto items = nlohmann::json::array(); + + auto& pv = *static_cast(luaL_checkudata(L, index, metaTableName)); + lua_rawgeti(L, LUA_REGISTRYINDEX, pv.registryIndex); + assert(lua_istable(L, -1)); + + lua_pushnil(L); + while(lua_next(L, -2)) { - result.emplace(key, lua_tointeger(L, -1)); + auto item = nlohmann::json::object(); + item["key"] = toJSON(L, -2); + item["value"] = toJSON(L, -1); + items.push_back(item); + lua_pop(L, 1); // pop value } - else - { - result.emplace(key, lua_tonumber(L, -1)); - } - break; + lua_pop(L, 1); // pop table - case LUA_TSTRING: - result.emplace(key, std::string(lua_tostring(L, -1))); - break; + auto value = nlohmann::json::object(); + value.emplace("type", "pv"); + value.emplace("items", items); + return value; + } + else if(lua_getmetatable(L, index)) + { + lua_getfield(L, -1, "__name"); + std::string_view name = lua_tostring(L, -1); + lua_pop(L, 2); - case LUA_TUSERDATA: - if(auto object = test<::Object>(L, -1)) + if(contains(Enums::metaTableNames, name)) { auto value = nlohmann::json::object(); - value.emplace("type", "object"); - value.emplace("id", object->getObjectId()); - result.emplace(key, std::move(value)); - break; + value.emplace("type", std::string("enum.").append(name)); + value.emplace("value", checkEnum(L, index, name.data())); + return value; } - else if(auto* vectorProperty = VectorProperty::test(L, -1)) + else if(contains(Sets::metaTableNames, name)) { auto value = nlohmann::json::object(); - value.emplace("type", "vector_property"); - value.emplace("object_id", vectorProperty->object().getObjectId()); - value.emplace("name", vectorProperty->name()); - result.emplace(key, std::move(value)); - break; + value.emplace("type", std::string("set.").append(name)); + value.emplace("value", checkSet(L, index, name.data())); + return value; } - else if(auto* method = Method::test(L, -1)) - { - auto value = nlohmann::json::object(); - value.emplace("type", "method"); - value.emplace("object_id", method->object().getObjectId()); - value.emplace("name", method->name()); - result.emplace(key, std::move(value)); - break; - } - else if(auto* event = Event::test(L, -1)) - { - auto value = nlohmann::json::object(); - value.emplace("type", "event"); - value.emplace("object_id", event->object().getObjectId()); - value.emplace("name", event->name()); - result.emplace(key, std::move(value)); - break; - } - else if(lua_getmetatable(L, -1)) - { - lua_getfield(L, -1, "__name"); - std::string_view name = lua_tostring(L, -1); - lua_pop(L, 2); + } + break; - if(contains(Enums::metaTableNames, name)) - { - auto value = nlohmann::json::object(); - value.emplace("type", std::string("enum.").append(name)); - value.emplace("value", checkEnum(L, -1, name.data())); - result.emplace(key, std::move(value)); - break; - } - else if(contains(Sets::metaTableNames, name)) - { - auto value = nlohmann::json::object(); - value.emplace("type", std::string("set.").append(name)); - value.emplace("value", checkSet(L, -1, name.data())); - result.emplace(key, std::move(value)); - break; - } - } - assert(false); - break; - - case LUA_TLIGHTUSERDATA: - case LUA_TTABLE: - case LUA_TFUNCTION: - case LUA_TTHREAD: - default: - assert(false); // unsupported - break; - } - lua_pop(L, 1); // pop value + case LUA_TTABLE: + case LUA_TLIGHTUSERDATA: + case LUA_TFUNCTION: + case LUA_TTHREAD: + default: + break; } - - return result; + assert(false); + errorInternal(L); } int PersistentVariables::__index(lua_State* L) @@ -319,38 +363,26 @@ int PersistentVariables::__index(lua_State* L) int PersistentVariables::__newindex(lua_State* L) { - switch(lua_type(L, 3)) + checkValue(L, 2); + + if(lua_istable(L, 3)) { - case LUA_TNIL: - case LUA_TBOOLEAN: - case LUA_TNUMBER: - case LUA_TSTRING: - break; // supported + checkTableRecursion(L, 3); - case LUA_TUSERDATA: - if(test<::Object>(L, 3) || VectorProperty::test(L, 3) || Method::test(L, 3) || Event::test(L, 3)) - { - break; // supported - } - else if(lua_getmetatable(L, 3)) - { - lua_getfield(L, -1, "__name"); - std::string_view name = lua_tostring(L, -1); - lua_pop(L, 2); - - if(contains(Enums::metaTableNames, name) || contains(Sets::metaTableNames, name)) - { - break; // supported - } - } - errorCantStoreValueAsPersistentVariableUnsupportedType(L); - - case LUA_TLIGHTUSERDATA: - case LUA_TTABLE: - case LUA_TFUNCTION: - case LUA_TTHREAD: - default: - errorCantStoreValueAsPersistentVariableUnsupportedType(L); + push(L); // push pv userdata + lua_insert(L, -2); // swap pv and table + lua_pushnil(L); + while(lua_next(L, -2)) + { + lua_pushvalue(L, -2); // copy key on stack + lua_insert(L, -2); // swap copied key and value + lua_settable(L, -5); // pops copied key and value + } + lua_pop(L, 1); // pop table + } + else if(!test(L, 3)) + { + checkValue(L, 3); } const auto& pv = *static_cast(lua_touserdata(L, 1)); @@ -360,6 +392,16 @@ int PersistentVariables::__newindex(lua_State* L) return 0; } +int PersistentVariables::__len(lua_State* L) +{ + const auto& pv = *static_cast(lua_touserdata(L, 1)); + lua_rawgeti(L, LUA_REGISTRYINDEX, pv.registryIndex); + lua_len(L, -1); + lua_insert(L, -2); // swap length and table + lua_pop(L, 1); // pop table + return 1; +} + int PersistentVariables::__gc(lua_State* L) { auto* pv = static_cast(lua_touserdata(L, 1)); @@ -368,4 +410,42 @@ int PersistentVariables::__gc(lua_State* L) return 0; } +void PersistentVariables::checkValue(lua_State* L, int index) +{ + switch(lua_type(L, index)) + { + case LUA_TNIL: + case LUA_TBOOLEAN: + case LUA_TNUMBER: + case LUA_TSTRING: + return; // supported + + case LUA_TUSERDATA: + if(Lua::test<::Object>(L, index) || VectorProperty::test(L, index) || Method::test(L, index) || Event::test(L, index)) + { + return; // supported + } + else if(lua_getmetatable(L, index)) + { + lua_getfield(L, -1, "__name"); + std::string_view name = lua_tostring(L, -1); + lua_pop(L, 2); + + if(contains(Enums::metaTableNames, name) || contains(Sets::metaTableNames, name)) + { + return; // supported + } + } + break; + + case LUA_TTABLE: + case LUA_TLIGHTUSERDATA: + case LUA_TFUNCTION: + case LUA_TTHREAD: + default: + break; + } + errorCantStoreValueAsPersistentVariableUnsupportedType(L); +} + } diff --git a/server/src/lua/persistentvariables.hpp b/server/src/lua/persistentvariables.hpp index 601d61be..a3eac1c1 100644 --- a/server/src/lua/persistentvariables.hpp +++ b/server/src/lua/persistentvariables.hpp @@ -33,12 +33,16 @@ class PersistentVariables private: static int __index(lua_State* L); static int __newindex(lua_State* L); + static int __len(lua_State* L); static int __gc(lua_State* L); + static void checkValue(lua_State* L, int index); + public: static void registerType(lua_State* L); + static bool test(lua_State* L, int index); static void push(lua_State* L); - static void push(lua_State* L, const nlohmann::json& pv); + static void push(lua_State* L, const nlohmann::json& value); static nlohmann::json toJSON(lua_State* L, int index); }; diff --git a/server/src/lua/sandbox.cpp b/server/src/lua/sandbox.cpp index 43264e7e..0b95e54d 100644 --- a/server/src/lua/sandbox.cpp +++ b/server/src/lua/sandbox.cpp @@ -231,7 +231,14 @@ SandboxPtr Sandbox::create(Script& script) lua_setfield(L, -2, "log"); // add persistent variables: - PersistentVariables::push(L, script.m_persistentVariables); + if(script.m_persistentVariables.empty()) + { + PersistentVariables::push(L); + } + else + { + PersistentVariables::push(L, script.m_persistentVariables); + } lua_setfield(L, -2, "pv"); // add class types: diff --git a/server/src/lua/script.cpp b/server/src/lua/script.cpp index a3647f19..39b310fc 100644 --- a/server/src/lua/script.cpp +++ b/server/src/lua/script.cpp @@ -43,7 +43,6 @@ constexpr std::string_view dotLua = ".lua"; Script::Script(World& world, std::string_view _id) : IdObject(world, _id), m_sandbox{nullptr, nullptr}, - m_persistentVariables{nlohmann::json::object()}, name{this, "name", std::string(_id), PropertyFlags::ReadWrite | PropertyFlags::Store}, disabled{this, "disabled", false, PropertyFlags::ReadWrite | PropertyFlags::NoStore | PropertyFlags::NoScript, [this](bool value) @@ -69,7 +68,7 @@ Script::Script(World& world, std::string_view _id) : , clearPersistentVariables{*this, "clear_persistent_variables", [this]() { - m_persistentVariables = nlohmann::json::object(); + m_persistentVariables = nullptr; Log::log(*this, LogMessage::I9003_CLEARED_PERSISTENT_VARIABLES); updateEnabled(); }} diff --git a/server/test/lua/script/persistentvariables.cpp b/server/test/lua/script/persistentvariables.cpp index f2bbd383..e16d83fc 100644 --- a/server/test/lua/script/persistentvariables.cpp +++ b/server/test/lua/script/persistentvariables.cpp @@ -310,7 +310,7 @@ TEST_CASE("Lua script: pv - unsupported - function", "[lua][lua-script][lua-scri script.reset(); } -TEST_CASE("Lua script: pv - unsupported - table", "[lua][lua-script][lua-script-pv]") +TEST_CASE("Lua script: pv - save/restore - table, empty", "[lua][lua-script][lua-script-pv]") { auto world = World::create(); REQUIRE(world); @@ -321,8 +321,386 @@ TEST_CASE("Lua script: pv - unsupported - table", "[lua][lua-script][lua-script- script->code = "pv.test = {}"; script->start(); INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test ~= nil)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - table, array like", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = {1, 2, 3}"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test[1] == 1)\nassert(pv.test[2] == 2)\nassert(pv.test[3] == 3)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - table, array length", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = {1, 2, 3}"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(#pv.test == 3)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - table, map like", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv.test = {one=1, two=2}"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv.test.one == 1)\nassert(pv.test.two == 2)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - bool as key", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv[true] = false"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv[true] == false)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - float as key", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv[math.pi] = 3"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv[math.pi] == 3)"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - enum key as key", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv[enum.world_event.RUN] = 'y'"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv[enum.world_event.RUN] == 'y')"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - set as key", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv[set.world_state.RUN] = 'test'"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv[set.world_state.RUN] == 'test')"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - object as key", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv[world] = 'world'"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv[world] == 'world')"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - vector property as key", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + auto train = world->trains->create(); + REQUIRE(train); + train->id = "train"; + + // set pv: + script->code = "pv[world.get_object('train').blocks] = 'test'"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv[world.get_object('train').blocks] == 'test')"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); + train.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - method as key", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv[world.stop] = 'test'"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv[world.stop] == 'test')"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - save/restore - event as key", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv[world.on_event] = 'test'"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + // check pv: + script->code = "assert(pv[world.on_event] == 'test')"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Running); + script->stop(); + REQUIRE(script->state.value() == LuaScriptState::Stopped); + + script.reset(); +} + +TEST_CASE("Lua script: pv - unsupported - function as key", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv[function(a, b)\nreturn a+b\nend] = 'test'"; + script->start(); + INFO(script->error.value()); REQUIRE(script->state.value() == LuaScriptState::Error); REQUIRE(endsWith(script->error.value(), "can't store value as persistent variable, unsupported type")); script.reset(); } + +TEST_CASE("Lua script: pv - unsupported - table as key", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = "pv[{}] = 'test'"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Error); + REQUIRE(endsWith(script->error.value(), "can't store value as persistent variable, unsupported type")); + + script.reset(); +} + +TEST_CASE("Lua script: pv - unsupported - table recursion", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = + "t = {}\n" + "t['t'] = t\n" + "pv['t'] = t"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Error); + REQUIRE(endsWith(script->error.value(), "table contains recursion")); + + script.reset(); +} + +TEST_CASE("Lua script: pv - unsupported - table recursion 2", "[lua][lua-script][lua-script-pv]") +{ + auto world = World::create(); + REQUIRE(world); + auto script = world->luaScripts->create(); + REQUIRE(script); + + // set pv: + script->code = + "t = {}\n" + "a = {}\n" + "b = {}\n" + "t['a'] = a\n" + "t['b'] = b\n" + "t['a']['b'] = b\n" + "t['b']['a'] = a\n" + "pv['t'] = t"; + script->start(); + INFO(script->error.value()); + REQUIRE(script->state.value() == LuaScriptState::Error); + REQUIRE(endsWith(script->error.value(), "table contains recursion")); + + script.reset(); +} From 890249b677fb846d0709e43296a07fcc0ed3ef95 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sat, 12 Oct 2024 13:52:55 +0200 Subject: [PATCH 17/39] fix: make sure stack index is abs --- server/src/lua/persistentvariables.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/lua/persistentvariables.cpp b/server/src/lua/persistentvariables.cpp index 4dca0c6b..8a6fa64b 100644 --- a/server/src/lua/persistentvariables.cpp +++ b/server/src/lua/persistentvariables.cpp @@ -70,7 +70,7 @@ static void checkTableRecursion(lua_State* L, int index) assert(lua_istable(L, index)); std::vector indices; - indices.push_back(lua_gettop(L)); + indices.push_back(lua_absindex(L, index)); checkTableRecursion(L, indices); } From 57aa6b123e9ada43fa84c817277006d2ef8dd3b2 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sat, 12 Oct 2024 22:05:26 +0200 Subject: [PATCH 18/39] lua: added clear persistent variables toolbar action to script list --- .../widget/objectlist/objectlistwidget.cpp | 10 +++++ server/src/lua/script.cpp | 2 + server/src/lua/scriptlist.cpp | 40 ++++++++++++++++--- server/src/lua/scriptlist.hpp | 5 ++- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/client/src/widget/objectlist/objectlistwidget.cpp b/client/src/widget/objectlist/objectlistwidget.cpp index 38f20b6c..edb9132d 100644 --- a/client/src/widget/objectlist/objectlistwidget.cpp +++ b/client/src/widget/objectlist/objectlistwidget.cpp @@ -399,6 +399,16 @@ ObjectListWidget::ObjectListWidget(const ObjectPtr& object_, QWidget* parent) : } } + if(auto* method = object()->getMethod("clear_persistent_variables")) + { + QWidget* spacer = new QWidget(this); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + spacer->show(); + m_toolbar->addWidget(spacer); + + m_toolbar->addAction(new MethodAction(Theme::getIcon("clear_persistent_variables"), *method, this)); + } + if(!m_toolbar->actions().empty()) { static_cast(this->layout())->insertWidget(0, m_toolbar); diff --git a/server/src/lua/script.cpp b/server/src/lua/script.cpp index 39b310fc..daa3fd63 100644 --- a/server/src/lua/script.cpp +++ b/server/src/lua/script.cpp @@ -189,6 +189,8 @@ void Script::updateEnabled() Attributes::setEnabled(start, stoppedOrError); Attributes::setEnabled(stop, state == LuaScriptState::Running); Attributes::setEnabled(clearPersistentVariables, stoppedOrError && !m_persistentVariables.empty()); + + m_world.luaScripts->updateEnabled(); } void Script::setState(LuaScriptState value) diff --git a/server/src/lua/scriptlist.cpp b/server/src/lua/scriptlist.cpp index 046b014d..2bfa644d 100644 --- a/server/src/lua/scriptlist.cpp +++ b/server/src/lua/scriptlist.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2019-2023 Reinder Feenstra + * Copyright (C) 2019-2024 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -54,6 +54,17 @@ ScriptList::ScriptList(Object& _parent, std::string_view parentPropertyName) if(!script->disabled) script->stop(); }} + , clearPersistentVariables{*this, "clear_persistent_variables", + [this]() + { + for(const auto& script : m_items) + { + if(Attributes::getEnabled(script->clearPersistentVariables)) + { + script->clearPersistentVariables(); + } + } + }} { status.setValueInternal(std::make_shared(*this, status.name())); @@ -74,6 +85,9 @@ ScriptList::ScriptList(Object& _parent, std::string_view parentPropertyName) Attributes::addEnabled(stopAll, false); m_interfaceItems.add(stopAll); + + Attributes::addEnabled(clearPersistentVariables, false); + m_interfaceItems.add(clearPersistentVariables); } ScriptList::~ScriptList() @@ -100,20 +114,18 @@ void ScriptList::objectAdded(const std::shared_ptr