diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4fdb8663..47a2a21e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,16 +60,6 @@ jobs: build_deb: true defines: "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" - - name: "macos-12" - os: "macos-12" - generator: "Unix Makefiles" - arch: "" - target: traintastic-client - jobs: 3 - build_type: Release - build_deb: false - defines: "" - - name: "macos-13" os: "macos-13" generator: "Unix Makefiles" @@ -90,6 +80,16 @@ jobs: build_deb: false defines: "" + - name: "macos-15" + os: "macos-15" + generator: "Unix Makefiles" + arch: "" + target: traintastic-client + jobs: 3 + build_type: Release + build_deb: false + defines: "" + steps: - uses: FranzDiebold/github-env-vars-action@v2 @@ -261,18 +261,6 @@ jobs: defines: "-DINSTALL_SYSTEMD_SERVICE=ON -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" ccov: false - - name: "macos-12" - os: "macos-12" - generator: "Unix Makefiles" - arch: "" - toolset: "" - target: all - jobs: 3 - build_type: Release - build_deb: false - defines: "" - ccov: false - - name: "macos-13" os: "macos-13" generator: "Unix Makefiles" @@ -297,6 +285,18 @@ jobs: defines: "" ccov: false + - name: "macos-15" + os: "macos-15" + generator: "Unix Makefiles" + arch: "" + toolset: "" + target: all + jobs: 3 + build_type: Release + build_deb: false + defines: "" + ccov: false + steps: - uses: FranzDiebold/github-env-vars-action@v2 diff --git a/.gitmodules b/.gitmodules index bc4e5c1a..c60740e6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "shared/data/lncv"] path = shared/data/lncv url = ../lncv.git +[submodule "server/thirdparty/catch2"] + path = server/thirdparty/catch2 + url = https://github.com/catchorg/Catch2.git 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 ad2073e3..b7eb8258 100644 --- a/client/gfx/dark/dark.qrc +++ b/client/gfx/dark/dark.qrc @@ -93,5 +93,6 @@ board_tile.misc.switch.svg board_tile.misc.label.svg zone.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 07de66db..20e05821 100644 --- a/client/gfx/light/light.qrc +++ b/client/gfx/light/light.qrc @@ -67,5 +67,6 @@ board_tile.misc.switch.svg board_tile.misc.label.svg zone.svg + clear_persistent_variables.svg diff --git a/client/src/board/boardareawidget.cpp b/client/src/board/boardareawidget.cpp index 5cae0025..47391096 100644 --- a/client/src/board/boardareawidget.cpp +++ b/client/src/board/boardareawidget.cpp @@ -32,6 +32,7 @@ #include "getboardcolorscheme.hpp" #include "tilepainter.hpp" #include "../network/board.hpp" +#include "../network/callmethod.hpp" #include "../network/object.tpp" #include "../network/object/blockrailtile.hpp" #include "../network/object/nxbuttonrailtile.hpp" @@ -39,6 +40,7 @@ #include "../network/abstractvectorproperty.hpp" #include "../utils/enum.hpp" #include "../utils/rectf.hpp" +#include "../misc/mimedata.hpp" #include "../settings/boardsettings.hpp" QRect rectToViewport(const QRect& r, const int gridSize) @@ -84,6 +86,7 @@ BoardAreaWidget::BoardAreaWidget(BoardWidget& board, QWidget* parent) : { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); setFocusPolicy(Qt::StrongFocus); + setAcceptDrops(true); if(Q_LIKELY(m_boardLeft)) connect(m_boardLeft, &AbstractProperty::valueChanged, this, &BoardAreaWidget::updateMinimumSize); @@ -837,6 +840,65 @@ void BoardAreaWidget::paintEvent(QPaintEvent* event) } } +void BoardAreaWidget::dragEnterEvent(QDragEnterEvent *event) +{ + if(event->mimeData()->hasFormat(AssignTrainMimeData::mimeType)) + { + m_dragMoveTileLocation = TileLocation::invalid; + event->acceptProposedAction(); + } +} + +void BoardAreaWidget::dragMoveEvent(QDragMoveEvent* event) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + const TileLocation l = pointToTileLocation(event->pos()); +#else + const TileLocation l = pointToTileLocation(event->position().toPoint()); +#endif + + if(m_dragMoveTileLocation != l) + { + m_dragMoveTileLocation = l; + if(event->mimeData()->hasFormat(AssignTrainMimeData::mimeType) && + m_board.board().getTileId(l) == TileId::RailBlock) + { + return event->accept(); + } + event->ignore(); + } +} + +void BoardAreaWidget::dropEvent(QDropEvent* event) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + const TileLocation l = pointToTileLocation(event->pos()); +#else + const TileLocation l = pointToTileLocation(event->position().toPoint()); +#endif + + switch(m_board.board().getTileId(l)) + { + case TileId::RailBlock: + if(auto* assignTrain = dynamic_cast(event->mimeData())) + { + if(auto tile = std::dynamic_pointer_cast(m_board.board().getTileObject(l))) + { + if(auto* method = tile->getMethod("assign_train")) + { + callMethod(*method, nullptr, assignTrain->trainId()); + return event->accept(); + } + } + } + break; + + default: + break; + } + event->ignore(); +} + void BoardAreaWidget::settingsChanged() { const auto& s = BoardSettings::instance(); diff --git a/client/src/board/boardareawidget.hpp b/client/src/board/boardareawidget.hpp index ea36d74d..14eb166f 100644 --- a/client/src/board/boardareawidget.hpp +++ b/client/src/board/boardareawidget.hpp @@ -89,6 +89,8 @@ class BoardAreaWidget : public QWidget uint8_t m_mouseMoveTileWidthMax; uint8_t m_mouseMoveTileHeightMax; + TileLocation m_dragMoveTileLocation; + inline int boardLeft() const { return Q_LIKELY(m_boardLeft) ? m_boardLeft->toInt() - boardMargin : 0; } inline int boardTop() const { return Q_LIKELY(m_boardTop) ? m_boardTop->toInt() - boardMargin: 0; } inline int boardRight() const { return Q_LIKELY(m_boardRight) ? m_boardRight->toInt() + boardMargin: 0; } @@ -115,6 +117,9 @@ class BoardAreaWidget : public QWidget void mouseMoveEvent(QMouseEvent* event) final; void wheelEvent(QWheelEvent* event) final; void paintEvent(QPaintEvent* event) final; + void dragEnterEvent(QDragEnterEvent* event) final; + void dragMoveEvent(QDragMoveEvent* event) final; + void dropEvent(QDropEvent* event) final; protected slots: void settingsChanged(); diff --git a/client/src/board/tilepainter.cpp b/client/src/board/tilepainter.cpp index 5ee80fc7..7c13a234 100644 --- a/client/src/board/tilepainter.cpp +++ b/client/src/board/tilepainter.cpp @@ -774,7 +774,7 @@ void TilePainter::drawTriangle(const QRectF& r) {r.right(), r.bottom()}, {r.left(), r.bottom()}}}; - m_painter.drawConvexPolygon(points.data(), points.size()); + m_painter.drawConvexPolygon(points.data(), static_cast(points.size())); } void TilePainter::drawLED(const QRectF& r, const QColor& color, const QColor& borderColor) diff --git a/client/src/mainwindow.cpp b/client/src/mainwindow.cpp index 269de1b0..28a576d5 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]() diff --git a/client/src/mainwindow/mainwindowstatusbar.cpp b/client/src/mainwindow/mainwindowstatusbar.cpp index 62cf22b0..add1065d 100644 --- a/client/src/mainwindow/mainwindowstatusbar.cpp +++ b/client/src/mainwindow/mainwindowstatusbar.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2022-2023 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 @@ -32,6 +32,7 @@ #include "../settings/statusbarsettings.hpp" #include "../widget/status/interfacestatuswidget.hpp" #include "../widget/status/luastatuswidget.hpp" +#include "../widget/status/simulationstatuswidget.hpp" MainWindowStatusBar::MainWindowStatusBar(MainWindow& mainWindow) : QStatusBar(&mainWindow) @@ -148,6 +149,8 @@ void MainWindowStatusBar::updateStatuses() m_statuses->layout()->addWidget(new InterfaceStatusWidget(object, this)); else if(object->classId() == "status.lua") m_statuses->layout()->addWidget(new LuaStatusWidget(object, this)); + else if(object->classId() == "status.simulation") + m_statuses->layout()->addWidget(new SimulationStatusWidget(object, this)); } }); } diff --git a/client/src/misc/mimedata.hpp b/client/src/misc/mimedata.hpp new file mode 100644 index 00000000..e9661b1c --- /dev/null +++ b/client/src/misc/mimedata.hpp @@ -0,0 +1,44 @@ +/** + * client/src/misc/mimedata.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_CLIENT_MISC_MIMEDATA_HPP +#define TRAINTASTIC_CLIENT_MISC_MIMEDATA_HPP + +#include + +class AssignTrainMimeData : public QMimeData +{ +public: + inline static const auto mimeType = QLatin1String("application/vnd.traintastic.assign_train"); + + explicit AssignTrainMimeData(const QString& trainId) + { + setData(mimeType, trainId.toUtf8()); + } + + inline QString trainId() const + { + return QString::fromUtf8(data(mimeType)); + } +}; + +#endif diff --git a/client/src/network/serverlogtablemodel.hpp b/client/src/network/serverlogtablemodel.hpp index 0cb952b9..a7d48570 100644 --- a/client/src/network/serverlogtablemodel.hpp +++ b/client/src/network/serverlogtablemodel.hpp @@ -70,7 +70,7 @@ class ServerLogTableModel final : public QAbstractTableModel ServerLogTableModel(std::shared_ptr connection); ~ServerLogTableModel(); - int columnCount(const QModelIndex& parent = QModelIndex()) const final { Q_UNUSED(parent); return m_columnHeaders.size(); } + int columnCount(const QModelIndex& parent = QModelIndex()) const final { Q_UNUSED(parent); return static_cast(m_columnHeaders.size()); } int rowCount(const QModelIndex& parent = QModelIndex()) const final; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const final; diff --git a/client/src/widget/createwidget.cpp b/client/src/widget/createwidget.cpp index 54d3874f..7f382e67 100644 --- a/client/src/widget/createwidget.cpp +++ b/client/src/widget/createwidget.cpp @@ -24,6 +24,7 @@ #include "list/marklincanlocomotivelistwidget.hpp" #include "objectlist/boardlistwidget.hpp" #include "objectlist/throttleobjectlistwidget.hpp" +#include "objectlist/trainlistwidget.hpp" #include "object/luascripteditwidget.hpp" #include "object/objecteditwidget.hpp" #include "object/itemseditwidget.hpp" @@ -47,7 +48,7 @@ QWidget* createWidgetIfCustom(const ObjectPtr& object, QWidget* parent) if(classId == "command_station_list") return new ObjectListWidget(object, parent); // todo remove - else if(classId == "decoder_list" || classId == "list.train") + else if(classId == "decoder_list") return new ThrottleObjectListWidget(object, parent); // todo remove else if(classId == "controller_list") return new ObjectListWidget(object, parent); // todo remove @@ -61,6 +62,10 @@ QWidget* createWidgetIfCustom(const ObjectPtr& object, QWidget* parent) { return new BoardListWidget(object, parent); } + if(classId == "list.train") + { + return new TrainListWidget(object, parent); + } else if(object->classId().startsWith("list.")) return new ObjectListWidget(object, parent); else if(classId == "lua.script") diff --git a/client/src/widget/inputmonitorwidget.cpp b/client/src/widget/inputmonitorwidget.cpp index 35292d86..1c56be75 100644 --- a/client/src/widget/inputmonitorwidget.cpp +++ b/client/src/widget/inputmonitorwidget.cpp @@ -160,7 +160,7 @@ void InputMonitorWidget::keyReleaseEvent(QKeyEvent* event) uint32_t InputMonitorWidget::pageCount() const { - return static_cast(m_addressMax->toInt64() - m_addressMin->toInt64() + m_leds.size()) / m_leds.size(); + return static_cast(m_addressMax->toInt64() - m_addressMin->toInt64() + m_leds.size()) / static_cast(m_leds.size()); } void InputMonitorWidget::setPage(uint32_t value) @@ -184,7 +184,7 @@ void InputMonitorWidget::setGroupBy(uint32_t value) LEDWidget* InputMonitorWidget::getLED(uint32_t address) { - const uint32_t first = static_cast(m_addressMin->toInt64()) + m_page * m_leds.size(); + const uint32_t first = static_cast(m_addressMin->toInt64()) + m_page * static_cast(m_leds.size()); if(address >= first && (address - first) < m_leds.size()) return m_leds[address - first]; @@ -199,7 +199,7 @@ void InputMonitorWidget::updateLEDs() const uint32_t addressMin = static_cast(m_addressMin->toInt64()); const uint32_t addressMax = static_cast(m_addressMax->toInt64()); - uint32_t address = addressMin + m_page * m_leds.size(); + uint32_t address = addressMin + m_page * static_cast(m_leds.size()); for(auto* led : m_leds) { 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/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/client/src/widget/objectlist/trainlistwidget.cpp b/client/src/widget/objectlist/trainlistwidget.cpp new file mode 100644 index 00000000..29b174a8 --- /dev/null +++ b/client/src/widget/objectlist/trainlistwidget.cpp @@ -0,0 +1,41 @@ +/** + * client/src/widget/objectlist/trainlistwidget.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 "trainlistwidget.hpp" +#include +#include "../tablewidget.hpp" +#include "../../misc/mimedata.hpp" + +TrainListWidget::TrainListWidget(const ObjectPtr& object, QWidget* parent) + : ThrottleObjectListWidget(object, parent) +{ + connect(m_tableWidget, &TableWidget::rowDragged, + [this](int row) + { + if(auto trainId = m_tableWidget->getRowObjectId(row); !trainId.isEmpty()) + { + QDrag* drag = new QDrag(m_tableWidget); + drag->setMimeData(new AssignTrainMimeData(trainId)); + drag->exec(Qt::CopyAction); + } + }); +} diff --git a/server/src/enum/commandstationstatus.hpp b/client/src/widget/objectlist/trainlistwidget.hpp similarity index 66% rename from server/src/enum/commandstationstatus.hpp rename to client/src/widget/objectlist/trainlistwidget.hpp index e91dcf53..21ee9b01 100644 --- a/server/src/enum/commandstationstatus.hpp +++ b/client/src/widget/objectlist/trainlistwidget.hpp @@ -1,9 +1,9 @@ /** - * server/src/enum/commandstationstatus.hpp + * client/src/widget/objectlist/trainlistwidget.hpp * * This file is part of the traintastic source code. * - * Copyright (C) 2019-2020 Reinder Feenstra + * 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 @@ -20,9 +20,15 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef TRAINTASTIC_SERVER_ENUM_COMMANDSTATIONSTATUS_HPP -#define TRAINTASTIC_SERVER_ENUM_COMMANDSTATIONSTATUS_HPP +#ifndef TRAINTASTIC_CLIENT_WIDGET_OBJECTLIST_TRAINLISTWIDGET_HPP +#define TRAINTASTIC_CLIENT_WIDGET_OBJECTLIST_TRAINLISTWIDGET_HPP -#include +#include "throttleobjectlistwidget.hpp" + +class TrainListWidget : public ThrottleObjectListWidget +{ +public: + explicit TrainListWidget(const ObjectPtr& object, QWidget* parent = nullptr); +}; #endif diff --git a/client/src/widget/outputkeyboardwidget.cpp b/client/src/widget/outputkeyboardwidget.cpp index 76a8855e..3a639161 100644 --- a/client/src/widget/outputkeyboardwidget.cpp +++ b/client/src/widget/outputkeyboardwidget.cpp @@ -161,7 +161,7 @@ OutputKeyboardWidget::OutputKeyboardWidget(std::shared_ptr objec connect(led, &LEDWidget::clicked, this, [this, index=i]() { - const uint32_t address = static_cast(m_addressMin->toInt64()) + m_page * m_leds.size() / 2 + index / 2; + const uint32_t address = static_cast(m_addressMin->toInt64()) + m_page * static_cast(m_leds.size()) / 2 + index / 2; const auto value = (index & 0x1) ? OutputPairValue::Second : OutputPairValue::First; callMethod(*m_setOutputValue, nullptr, address, value); }); @@ -251,7 +251,7 @@ uint32_t OutputKeyboardWidget::pageCount() const { leds *= 2; } - return static_cast(leds + m_leds.size() - 1) / m_leds.size(); + return static_cast(leds + m_leds.size() - 1) / static_cast(m_leds.size()); } void OutputKeyboardWidget::setPage(uint32_t value) @@ -277,7 +277,7 @@ LEDWidget* OutputKeyboardWidget::getLED(uint32_t address) { assert(m_object->outputType() == OutputType::Single); - const uint32_t first = static_cast(m_addressMin->toInt64()) + m_page * m_leds.size(); + const uint32_t first = static_cast(m_addressMin->toInt64()) + m_page * static_cast(m_leds.size()); if(address >= first && (address - first) < m_leds.size()) return m_leds[address - first]; @@ -289,7 +289,7 @@ std::pair OutputKeyboardWidget::getLEDs(uint32_t address { assert(m_object->outputType() == OutputType::Pair); - const uint32_t first = static_cast(m_addressMin->toInt64()) + m_page * m_leds.size() / 2; + const uint32_t first = static_cast(m_addressMin->toInt64()) + m_page * static_cast(m_leds.size()) / 2; if(address >= first && (address - first) < m_leds.size()) return {m_leds[(address - first) * 2], m_leds[(address - first) * 2 + 1]}; @@ -309,7 +309,7 @@ void OutputKeyboardWidget::updateLEDs() { case OutputType::Single: { - uint32_t address = addressMin + m_page * m_leds.size(); + uint32_t address = addressMin + m_page * static_cast(m_leds.size()); for(auto* led : m_leds) { const auto& outputState = m_object->getOutputState(address); @@ -329,7 +329,7 @@ void OutputKeyboardWidget::updateLEDs() } case OutputType::Pair: { - uint32_t address = addressMin + m_page * m_leds.size() / 2; + uint32_t address = addressMin + m_page * static_cast(m_leds.size()) / 2; bool second = false; for(auto* led : m_leds) { diff --git a/client/src/widget/outputmapwidget.cpp b/client/src/widget/outputmapwidget.cpp index 88c82e0a..c583cccc 100644 --- a/client/src/widget/outputmapwidget.cpp +++ b/client/src/widget/outputmapwidget.cpp @@ -162,7 +162,7 @@ OutputMapWidget::~OutputMapWidget() void OutputMapWidget::updateItems(const std::vector& items) { - m_table->setRowCount(items.size()); + m_table->setRowCount(static_cast(items.size())); m_itemObjects = items; m_actions.resize(items.size()); for(size_t i = 0; i < items.size(); i++) @@ -197,7 +197,7 @@ void OutputMapWidget::updateItems(const std::vector& items) assert(false); text = "?"; } - m_table->setItem(i, columnKey, new QTableWidgetItem(text)); + m_table->setItem(static_cast(i), columnKey, new QTableWidgetItem(text)); } if(m_hasUseColumn) @@ -211,16 +211,16 @@ void OutputMapWidget::updateItems(const std::vector& items) l->setAlignment(Qt::AlignCenter); l->addWidget(new PropertyCheckBox(*p, w)); w->setLayout(l); - m_table->setCellWidget(i, columnUse, w); + m_table->setCellWidget(static_cast(i), columnUse, w); } } if(auto* p = items[i]->getProperty("visible")) { - m_table->setRowHidden(i, !p->toBool()); + m_table->setRowHidden(static_cast(i), !p->toBool()); connect(p, &Property::valueChangedBool, this, - [this, row=i](bool value) + [this, row=static_cast(i)](bool value) { m_table->setRowHidden(row, !value); }); @@ -228,10 +228,10 @@ void OutputMapWidget::updateItems(const std::vector& items) if(auto* outputActions = dynamic_cast(items[i]->getVectorProperty("output_actions"))) { - updateTableOutputActions(*outputActions, i); + updateTableOutputActions(*outputActions, static_cast(i)); connect(outputActions, &ObjectVectorProperty::valueChanged, this, - [this, row=i]() + [this, row=static_cast(i)]() { updateTableOutputActions(*dynamic_cast(sender()), row); }); @@ -291,7 +291,7 @@ void OutputMapWidget::updateKeyIcons() break; // tileId not supported (yet) } - m_table->item(i, columnKey)->setIcon(QPixmap::fromImage(image)); + m_table->item(static_cast(i), columnKey)->setIcon(QPixmap::fromImage(image)); } } } diff --git a/client/src/widget/propertyluacodeedit.cpp b/client/src/widget/propertyluacodeedit.cpp index 0d00b26b..2dd9ccab 100644 --- a/client/src/widget/propertyluacodeedit.cpp +++ b/client/src/widget/propertyluacodeedit.cpp @@ -189,6 +189,7 @@ PropertyLuaCodeEdit::Highlighter::Highlighter(QTextDocument* parent) : QStringLiteral("\\bworld(?=\\.)"), QStringLiteral("\\bset(?=\\.)"), QStringLiteral("\\benum(?=\\.)"), + QStringLiteral("\\bpv(?=\\.)"), }; for(const auto& regex : globals) m_rules.append(Rule(regex, QColor(0xFF, 0x8C, 0x00))); diff --git a/client/src/widget/status/simulationstatuswidget.cpp b/client/src/widget/status/simulationstatuswidget.cpp new file mode 100644 index 00000000..f7cc3842 --- /dev/null +++ b/client/src/widget/status/simulationstatuswidget.cpp @@ -0,0 +1,66 @@ +/** + * client/src/widget/status/simulationstatuswidget.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 "simulationstatuswidget.hpp" +#include +#include +#include "../../network/object.hpp" +#include "../../network/property.hpp" +#include "../../theme/theme.hpp" + +SimulationStatusWidget::SimulationStatusWidget(const ObjectPtr& object, QWidget* parent) + : QSvgWidget(parent) + , m_object{object} +{ + assert(m_object); + assert(m_object->classId() == "status.simulation"); + + load(Theme::getIconFile("simulation")); + + if(auto* property = m_object->getProperty("label")) + { + connect(property, &Property::valueChanged, this, &SimulationStatusWidget::labelChanged); + } + + labelChanged(); +} + +void SimulationStatusWidget::labelChanged() +{ + QString label; + + if(auto* property = m_object->getProperty("label")) + { + label = Locale::instance->parse(property->toString()); + } + + setToolTip(label); +} + +void SimulationStatusWidget::resizeEvent(QResizeEvent* event) +{ + QSvgWidget::resizeEvent(event); + + // force same width as height: + setMinimumWidth(event->size().height()); + setMaximumWidth(event->size().height()); +} diff --git a/client/src/widget/status/simulationstatuswidget.hpp b/client/src/widget/status/simulationstatuswidget.hpp new file mode 100644 index 00000000..76e24755 --- /dev/null +++ b/client/src/widget/status/simulationstatuswidget.hpp @@ -0,0 +1,44 @@ +/** + * client/src/widget/status/simulationstatuswidget.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_CLIENT_WIDGET_STATUS_SIMULATIONSTATUSWIDGET_HPP +#define TRAINTASTIC_CLIENT_WIDGET_STATUS_SIMULATIONSTATUSWIDGET_HPP + +#include +#include "../../network/objectptr.hpp" + +class SimulationStatusWidget : public QSvgWidget +{ +private: + ObjectPtr m_object; + + void labelChanged(); + void stateChanged(); + +protected: + void resizeEvent(QResizeEvent* event) override; + +public: + explicit SimulationStatusWidget(const ObjectPtr& object, QWidget* parent = nullptr); +}; + +#endif diff --git a/client/src/widget/tablewidget.cpp b/client/src/widget/tablewidget.cpp index 5d85fa76..b67da43b 100644 --- a/client/src/widget/tablewidget.cpp +++ b/client/src/widget/tablewidget.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 @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include "../network/tablemodel.hpp" TableWidget::TableWidget(QWidget* parent) : @@ -113,3 +115,23 @@ void TableWidget::updateRegion() m_model->setRegion(columnMin, columnMax, rowMin, rowMax); } + +void TableWidget::mouseMoveEvent(QMouseEvent* event) +{ + QTableView::mouseMoveEvent(event); + + if(event->button() == Qt::LeftButton) + { + m_dragStartPosition = event->pos(); + } +} + +void TableWidget::mousePressEvent(QMouseEvent* event) +{ + QTableView::mousePressEvent(event); + + if((event->buttons() & Qt::LeftButton) && (event->pos() - m_dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) + { + emit rowDragged(indexAt(m_dragStartPosition).row()); + } +} diff --git a/client/src/widget/tablewidget.hpp b/client/src/widget/tablewidget.hpp index 8adf4da7..d34636c1 100644 --- a/client/src/widget/tablewidget.hpp +++ b/client/src/widget/tablewidget.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2019-2020,2023 Reinder Feenstra + * Copyright (C) 2019-2020,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 @@ -33,6 +33,10 @@ class TableWidget : public QTableView protected: TableModelPtr m_model; int m_selectedRow = -1; + QPoint m_dragStartPosition; + + void mouseMoveEvent(QMouseEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; protected slots: void updateRegion(); @@ -44,6 +48,9 @@ class TableWidget : public QTableView QString getRowObjectId(int row) const; void setTableModel(const TableModelPtr& model); + + signals: + void rowDragged(int row); }; #endif diff --git a/manual/buildluadoc.py b/manual/buildluadoc.py index 0054d993..4603c1fc 100755 --- a/manual/buildluadoc.py +++ b/manual/buildluadoc.py @@ -20,6 +20,7 @@ class LuaDoc: DEFAULT_LANGUAGE = 'en-us' FILENAME_INDEX = 'index.html' FILENAME_GLOBALS = 'globals.html' + FILENAME_PV = 'pv.html' FILENAME_ENUM = 'enum.html' FILENAME_SET = 'set.html' FILENAME_OBJECT = 'object.html' @@ -30,6 +31,7 @@ class LuaDoc: version = None def __init__(self, project_root: str) -> None: + self._project_root = project_root self._globals = LuaDoc._find_globals(project_root) self._enums = LuaDoc._find_enums(project_root) self._sets = LuaDoc._find_sets(project_root) @@ -101,6 +103,16 @@ class LuaDoc: for object in self._objects: if object['lua_name'] == id: return '' + (self._get_term(object['name']) if title == '' else title) + '' + elif id == 'globals': + return '' + (self._get_term('globals:title') if title == '' else title) + '' + elif id == 'enum': + return '' + (self._get_term('enum:title') if title == '' else title) + '' + elif id == 'set': + return '' + (self._get_term('set:title') if title == '' else title) + '' + elif id == 'object': + return '' + (self._get_term('object:title') if title == '' else title) + '' + elif id == 'pv': + return '' + (self._get_term('pv:title') if title == '' else title) + '' return '' + m.group(0) + '' @@ -447,6 +459,7 @@ class LuaDoc: self._build_index(output_dir) self._build_globals(output_dir, nav) + self._build_pv(output_dir, nav) self._build_enums(output_dir, nav) self._build_sets(output_dir, nav) for _, lib in self._libs.items(): @@ -649,6 +662,30 @@ class LuaDoc: html += self._build_items_html(self._globals, 'globals.') LuaDoc._write_file(os.path.join(output_dir, LuaDoc.FILENAME_GLOBALS), self._add_toc(html)) + def _build_pv(self, output_dir: str, nav: list) -> None: + title = self._get_term('pv:title') + html = self._get_header(title, nav + [{'title': title, 'href': LuaDoc.FILENAME_PV}]) + html += '

' + self._get_term('pv:paragraph_1') + '

' + os.linesep + html += '

' + self._get_term('pv:paragraph_2') + '

' + os.linesep + + html += '

' + self._get_term('pv.storing:title') + '

' + os.linesep + html += '

' + self._get_term('pv.storing:paragraph_1') + '

' + os.linesep + html += '
' + highlight_lua(LuaDoc._read_file(os.path.join(self._project_root, 'manual', 'luadoc', 'example', 'pv', 'storingpersistentdata.lua'))) + '
' + + html += '

' + self._get_term('pv.retrieving:title') + '

' + os.linesep + html += '

' + self._get_term('pv.retrieving:paragraph_1') + '

' + os.linesep + html += '
' + highlight_lua(LuaDoc._read_file(os.path.join(self._project_root, 'manual', 'luadoc', 'example', 'pv', 'retrievingpersistentdata.lua'))) + '
' + + html += '

' + self._get_term('pv.deleting:title') + '

' + os.linesep + html += '

' + self._get_term('pv.deleting:paragraph_1') + '

' + os.linesep + html += '
' + highlight_lua(LuaDoc._read_file(os.path.join(self._project_root, 'manual', 'luadoc', 'example', 'pv', 'deletingpersistentdata.lua'))) + '
' + + html += '

' + self._get_term('pv.checking:title') + '

' + os.linesep + html += '

' + self._get_term('pv.checking:paragraph_1') + '

' + os.linesep + html += '
' + highlight_lua(LuaDoc._read_file(os.path.join(self._project_root, 'manual', 'luadoc', 'example', 'pv', 'checkingforpersistentdata.lua'))) + '
' + + LuaDoc._write_file(os.path.join(output_dir, LuaDoc.FILENAME_PV), self._add_toc(html)) + def _build_enums(self, output_dir: str, nav: list) -> None: title = self._get_term('enum:title') nav_enums = nav + [{'title': title, 'href': LuaDoc.FILENAME_ENUM}] @@ -854,6 +891,7 @@ class LuaDoc: def _get_header(self, title: str, nav: list) -> str: menu = '
  • ' + self._get_term('globals:title') + '
  • ' + os.linesep + menu += '
  • ' + self._get_term('pv:title') + '
  • ' + os.linesep for k in sorted(list(self._libs.keys()) + ['enum', 'set']): if k == 'enum': menu += '
  • ' + self._get_term('enum:title') + '
  • ' + os.linesep diff --git a/manual/luadoc/example/pv/checkingforpersistentdata.lua b/manual/luadoc/example/pv/checkingforpersistentdata.lua new file mode 100644 index 00000000..df83b85e --- /dev/null +++ b/manual/luadoc/example/pv/checkingforpersistentdata.lua @@ -0,0 +1,6 @@ +if pv.freight_car_1 == nil then + pv.freight_car_1 = { + cargo = 'none', + destination = 'unset' + } +end diff --git a/manual/luadoc/example/pv/deletingpersistentdata.lua b/manual/luadoc/example/pv/deletingpersistentdata.lua new file mode 100644 index 00000000..5e94ee05 --- /dev/null +++ b/manual/luadoc/example/pv/deletingpersistentdata.lua @@ -0,0 +1,4 @@ +pv.number = nil +pv.title = nil +pv.very_cool = nil +pv.freight_car_1 = nil diff --git a/manual/luadoc/example/pv/retrievingpersistentdata.lua b/manual/luadoc/example/pv/retrievingpersistentdata.lua new file mode 100644 index 00000000..31d1f011 --- /dev/null +++ b/manual/luadoc/example/pv/retrievingpersistentdata.lua @@ -0,0 +1,9 @@ +log.debug(pv.number) +log.debug(pv.title) +log.debug(pv.very_cool) + +log.debug(pv.freight_car_1.cargo) + +for k, v in pairs(pv['freight_car_1']) do + log.debug(k, v) +end diff --git a/manual/luadoc/example/pv/storingpersistentdata.lua b/manual/luadoc/example/pv/storingpersistentdata.lua new file mode 100644 index 00000000..014bf024 --- /dev/null +++ b/manual/luadoc/example/pv/storingpersistentdata.lua @@ -0,0 +1,8 @@ +pv.number = 42 +pv.title = 'Traintastic is awesome!' +pv.very_cool = true + +pv['freight_car_1'] = { + cargo = 'grain', + destination = 'upper yard' +} 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/manual/luadoc/object/train.json b/manual/luadoc/object/train.json index 95fe3e14..ae0be6d5 100644 --- a/manual/luadoc/object/train.json +++ b/manual/luadoc/object/train.json @@ -8,6 +8,7 @@ "mode": {}, "mute": {}, "no_smoke": {}, + "blocks": {}, "zones": {}, "on_block_assigned": { "parameters": [ diff --git a/manual/luadoc/terms/en-us.json b/manual/luadoc/terms/en-us.json index 3020248d..23bbc1cb 100644 --- a/manual/luadoc/terms/en-us.json +++ b/manual/luadoc/terms/en-us.json @@ -2218,5 +2218,53 @@ { "term": "object.zone.trains:description", "definition": "List of {ref:object.trainzonestatus|train zone status} objects of all trains that are entering, entered or leaving the zone." + }, + { + "term": "globals.pv:description", + "definition": "The {ref:pv|persistent variable} table." + }, + { + "term": "pv:title", + "definition": "Persistent variables" + }, + { + "term": "pv:paragraph_1", + "definition": "Persistent variables allow you to store and retrieve data that remains available across multiple executions of the Lua script. This can be particularly useful for maintaining state information that needs to be retained beyond the current script's lifetime." + }, + { + "term": "pv:paragraph_2", + "definition": "The {ref:globals#pv|`pv`} global provides a simple and efficient interface for interacting with persistent data. Any values stored in {ref:globals#pv|`pv`} are saved across script executions and world save and loads. Below is a detailed breakdown of how to use the {ref:globals#pv|`pv`} global." + }, + { + "term": "pv.storing:title", + "definition": "Storing persistent data" + }, + { + "term": "pv.storing:paragraph_1", + "definition": "You can store data in {ref:globals#pv|`pv`} just like you would with a regular Lua table. Supported data types are numbers, strings, booleans, tables, {ref:enum|enums}, {ref:set|sets}, {ref:object|objects} and object methods." + }, + { + "term": "pv.retrieving:title", + "definition": "Retrieving persistent data" + }, + { + "term": "pv.retrieving:paragraph_1", + "definition": "To retrieve a previously stored value, including tables, access the corresponding key in the {ref:globals#pv|`pv`} global:" + }, + { + "term": "pv.deleting:title", + "definition": "Deleting persistent data" + }, + { + "term": "pv.deleting:paragraph_1", + "definition": "To delete a stored persistent value, including tables, simply assign `nil` to the desired key:" + }, + { + "term": "pv.checking:title", + "definition": "Checking for persistent data" + }, + { + "term": "pv.checking:paragraph_1", + "definition": "To determine if a persistent variable has been set, use an `if` statement with `nil` checks. Variables in {ref:globals#pv|`pv`} that haven't been initialized or have been deleted will return `nil`. This pattern is useful for initializing default values or handling cases where the persistent variables are cleared." } ] diff --git a/manual/traintasticmanualbuilder/utils.py b/manual/traintasticmanualbuilder/utils.py index 4db7acf3..11a6f5a7 100644 --- a/manual/traintasticmanualbuilder/utils.py +++ b/manual/traintasticmanualbuilder/utils.py @@ -25,7 +25,7 @@ def highlight_replace(code: str, css_class: str, clickable_links: bool = False) def highlight_lua(code: str) -> str: - code = re.sub(r'\b(math|table|string|class|enum|set|log|world)\b', r'\1', code) # globals + code = re.sub(r'\b(math|table|string|class|enum|set|log|world|pv)\b', r'\1', code) # globals code = re.sub(r'\b([A-Z_][A-Z0-9_]*)\b', r'\1', code) # CONSTANTS code = re.sub(r'\b(and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b', r'\1', code) # keywords code = re.sub(r'\b((|-|\+)[0-9]+(\\.[0-9]*|)((e|E)(|-|\+)[0-9]+|))\b', r'\1', code) # numbers: infloat, decimal diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index c914db1a..2371c176 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -32,6 +32,8 @@ target_include_directories(traintastic-server SYSTEM PRIVATE thirdparty) if(BUILD_TESTING) + add_subdirectory(thirdparty/catch2) + set_target_properties(Catch2 PROPERTIES CXX_STANDARD 17) add_executable(traintastic-server-test test/main.cpp) add_dependencies(traintastic-server-test traintastic-lang) target_compile_definitions(traintastic-server-test PRIVATE -DTRAINTASTIC_TEST) @@ -42,6 +44,7 @@ if(BUILD_TESTING) target_include_directories(traintastic-server-test SYSTEM PRIVATE ../shared/thirdparty thirdparty) + target_link_libraries(traintastic-server-test PRIVATE Catch2::Catch2WithMain) endif() file(GLOB SOURCES @@ -217,6 +220,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: @@ -459,8 +465,7 @@ endif() if(BUILD_TESTING) include(Catch) - target_include_directories(traintastic-server-test PRIVATE thirdparty/catch2) - catch_discover_tests(traintastic-server-test) + catch_discover_tests(traintastic-server-test DISCOVERY_MODE PRE_TEST) endif() ### Doxygen ### diff --git a/server/cmake/Catch.cmake b/server/cmake/Catch.cmake deleted file mode 100644 index a3885162..00000000 --- a/server/cmake/Catch.cmake +++ /dev/null @@ -1,206 +0,0 @@ -# Distributed under the OSI-approved BSD 3-Clause License. See accompanying -# file Copyright.txt or https://cmake.org/licensing for details. - -#[=======================================================================[.rst: -Catch ------ - -This module defines a function to help use the Catch test framework. - -The :command:`catch_discover_tests` discovers tests by asking the compiled test -executable to enumerate its tests. This does not require CMake to be re-run -when tests change. However, it may not work in a cross-compiling environment, -and setting test properties is less convenient. - -This command is intended to replace use of :command:`add_test` to register -tests, and will create a separate CTest test for each Catch test case. Note -that this is in some cases less efficient, as common set-up and tear-down logic -cannot be shared by multiple test cases executing in the same instance. -However, it provides more fine-grained pass/fail information to CTest, which is -usually considered as more beneficial. By default, the CTest test name is the -same as the Catch name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``. - -.. command:: catch_discover_tests - - Automatically add tests with CTest by querying the compiled test executable - for available tests:: - - catch_discover_tests(target - [TEST_SPEC arg1...] - [EXTRA_ARGS arg1...] - [WORKING_DIRECTORY dir] - [TEST_PREFIX prefix] - [TEST_SUFFIX suffix] - [PROPERTIES name1 value1...] - [TEST_LIST var] - [REPORTER reporter] - [OUTPUT_DIR dir] - [OUTPUT_PREFIX prefix} - [OUTPUT_SUFFIX suffix] - ) - - ``catch_discover_tests`` sets up a post-build command on the test executable - that generates the list of tests by parsing the output from running the test - with the ``--list-test-names-only`` argument. This ensures that the full - list of tests is obtained. Since test discovery occurs at build time, it is - not necessary to re-run CMake when the list of tests changes. - However, it requires that :prop_tgt:`CROSSCOMPILING_EMULATOR` is properly set - in order to function in a cross-compiling environment. - - Additionally, setting properties on tests is somewhat less convenient, since - the tests are not available at CMake time. Additional test properties may be - assigned to the set of tests as a whole using the ``PROPERTIES`` option. If - more fine-grained test control is needed, custom content may be provided - through an external CTest script using the :prop_dir:`TEST_INCLUDE_FILES` - directory property. The set of discovered tests is made accessible to such a - script via the ``_TESTS`` variable. - - The options are: - - ``target`` - Specifies the Catch executable, which must be a known CMake executable - target. CMake will substitute the location of the built executable when - running the test. - - ``TEST_SPEC arg1...`` - Specifies test cases, wildcarded test cases, tags and tag expressions to - pass to the Catch executable with the ``--list-test-names-only`` argument. - - ``EXTRA_ARGS arg1...`` - Any extra arguments to pass on the command line to each test case. - - ``WORKING_DIRECTORY dir`` - Specifies the directory in which to run the discovered test cases. If this - option is not provided, the current binary directory is used. - - ``TEST_PREFIX prefix`` - Specifies a ``prefix`` to be prepended to the name of each discovered test - case. This can be useful when the same test executable is being used in - multiple calls to ``catch_discover_tests()`` but with different - ``TEST_SPEC`` or ``EXTRA_ARGS``. - - ``TEST_SUFFIX suffix`` - Similar to ``TEST_PREFIX`` except the ``suffix`` is appended to the name of - every discovered test case. Both ``TEST_PREFIX`` and ``TEST_SUFFIX`` may - be specified. - - ``PROPERTIES name1 value1...`` - Specifies additional properties to be set on all tests discovered by this - invocation of ``catch_discover_tests``. - - ``TEST_LIST var`` - Make the list of tests available in the variable ``var``, rather than the - default ``_TESTS``. This can be useful when the same test - executable is being used in multiple calls to ``catch_discover_tests()``. - Note that this variable is only available in CTest. - - ``REPORTER reporter`` - Use the specified reporter when running the test case. The reporter will - be passed to the Catch executable as ``--reporter reporter``. - - ``OUTPUT_DIR dir`` - If specified, the parameter is passed along as - ``--out dir/`` to Catch executable. The actual file name is the - same as the test name. This should be used instead of - ``EXTRA_ARGS --out foo`` to avoid race conditions writing the result output - when using parallel test execution. - - ``OUTPUT_PREFIX prefix`` - May be used in conjunction with ``OUTPUT_DIR``. - If specified, ``prefix`` is added to each output file name, like so - ``--out dir/prefix``. - - ``OUTPUT_SUFFIX suffix`` - May be used in conjunction with ``OUTPUT_DIR``. - If specified, ``suffix`` is added to each output file name, like so - ``--out dir/suffix``. This can be used to add a file extension to - the output e.g. ".xml". - -#]=======================================================================] - -#------------------------------------------------------------------------------ -function(catch_discover_tests TARGET) - cmake_parse_arguments( - "" - "" - "TEST_PREFIX;TEST_SUFFIX;WORKING_DIRECTORY;TEST_LIST;REPORTER;OUTPUT_DIR;OUTPUT_PREFIX;OUTPUT_SUFFIX" - "TEST_SPEC;EXTRA_ARGS;PROPERTIES" - ${ARGN} - ) - - if(NOT _WORKING_DIRECTORY) - set(_WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") - endif() - if(NOT _TEST_LIST) - set(_TEST_LIST ${TARGET}_TESTS) - endif() - - ## Generate a unique name based on the extra arguments - string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS} ${_REPORTER} ${_OUTPUT_DIR} ${_OUTPUT_PREFIX} ${_OUTPUT_SUFFIX}") - string(SUBSTRING ${args_hash} 0 7 args_hash) - - # Define rule to generate test list for aforementioned test executable - set(ctest_include_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_include-${args_hash}.cmake") - set(ctest_tests_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_tests-${args_hash}.cmake") - get_property(crosscompiling_emulator - TARGET ${TARGET} - PROPERTY CROSSCOMPILING_EMULATOR - ) - add_custom_command( - TARGET ${TARGET} POST_BUILD - BYPRODUCTS "${ctest_tests_file}" - COMMAND "${CMAKE_COMMAND}" - -D "TEST_TARGET=${TARGET}" - -D "TEST_EXECUTABLE=$" - -D "TEST_EXECUTOR=${crosscompiling_emulator}" - -D "TEST_WORKING_DIR=${_WORKING_DIRECTORY}" - -D "TEST_SPEC=${_TEST_SPEC}" - -D "TEST_EXTRA_ARGS=${_EXTRA_ARGS}" - -D "TEST_PROPERTIES=${_PROPERTIES}" - -D "TEST_PREFIX=${_TEST_PREFIX}" - -D "TEST_SUFFIX=${_TEST_SUFFIX}" - -D "TEST_LIST=${_TEST_LIST}" - -D "TEST_REPORTER=${_REPORTER}" - -D "TEST_OUTPUT_DIR=${_OUTPUT_DIR}" - -D "TEST_OUTPUT_PREFIX=${_OUTPUT_PREFIX}" - -D "TEST_OUTPUT_SUFFIX=${_OUTPUT_SUFFIX}" - -D "CTEST_FILE=${ctest_tests_file}" - -P "${_CATCH_DISCOVER_TESTS_SCRIPT}" - VERBATIM - ) - - file(WRITE "${ctest_include_file}" - "if(EXISTS \"${ctest_tests_file}\")\n" - " include(\"${ctest_tests_file}\")\n" - "else()\n" - " add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n" - "endif()\n" - ) - - if(NOT ${CMAKE_VERSION} VERSION_LESS "3.10.0") - # Add discovered tests to directory TEST_INCLUDE_FILES - set_property(DIRECTORY - APPEND PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}" - ) - else() - # Add discovered tests as directory TEST_INCLUDE_FILE if possible - get_property(test_include_file_set DIRECTORY PROPERTY TEST_INCLUDE_FILE SET) - if (NOT ${test_include_file_set}) - set_property(DIRECTORY - PROPERTY TEST_INCLUDE_FILE "${ctest_include_file}" - ) - else() - message(FATAL_ERROR - "Cannot set more than one TEST_INCLUDE_FILE" - ) - endif() - endif() - -endfunction() - -############################################################################### - -set(_CATCH_DISCOVER_TESTS_SCRIPT - ${CMAKE_CURRENT_LIST_DIR}/CatchAddTests.cmake - CACHE INTERNAL "Catch2 full path to CatchAddTests.cmake helper file" -) diff --git a/server/cmake/CatchAddTests.cmake b/server/cmake/CatchAddTests.cmake deleted file mode 100644 index 18286b71..00000000 --- a/server/cmake/CatchAddTests.cmake +++ /dev/null @@ -1,132 +0,0 @@ -# Distributed under the OSI-approved BSD 3-Clause License. See accompanying -# file Copyright.txt or https://cmake.org/licensing for details. - -set(prefix "${TEST_PREFIX}") -set(suffix "${TEST_SUFFIX}") -set(spec ${TEST_SPEC}) -set(extra_args ${TEST_EXTRA_ARGS}) -set(properties ${TEST_PROPERTIES}) -set(reporter ${TEST_REPORTER}) -set(output_dir ${TEST_OUTPUT_DIR}) -set(output_prefix ${TEST_OUTPUT_PREFIX}) -set(output_suffix ${TEST_OUTPUT_SUFFIX}) -set(script) -set(suite) -set(tests) - -function(add_command NAME) - set(_args "") - foreach(_arg ${ARGN}) - if(_arg MATCHES "[^-./:a-zA-Z0-9_]") - set(_args "${_args} [==[${_arg}]==]") # form a bracket_argument - else() - set(_args "${_args} ${_arg}") - endif() - endforeach() - set(script "${script}${NAME}(${_args})\n" PARENT_SCOPE) -endfunction() - -# Run test executable to get list of available tests -if(NOT EXISTS "${TEST_EXECUTABLE}") - message(FATAL_ERROR - "Specified test executable '${TEST_EXECUTABLE}' does not exist" - ) -endif() -execute_process( - COMMAND ${TEST_EXECUTOR} "${TEST_EXECUTABLE}" ${spec} --list-test-names-only - OUTPUT_VARIABLE output - RESULT_VARIABLE result - WORKING_DIRECTORY "${TEST_WORKING_DIR}" -) -# Catch --list-test-names-only reports the number of tests, so 0 is... surprising -if(${result} EQUAL 0) - message(WARNING - "Test executable '${TEST_EXECUTABLE}' contains no tests!\n" - ) -elseif(${result} LESS 0) - message(FATAL_ERROR - "Error running test executable '${TEST_EXECUTABLE}':\n" - " Result: ${result}\n" - " Output: ${output}\n" - ) -endif() - -string(REPLACE "\n" ";" output "${output}") - -# Run test executable to get list of available reporters -execute_process( - COMMAND ${TEST_EXECUTOR} "${TEST_EXECUTABLE}" ${spec} --list-reporters - OUTPUT_VARIABLE reporters_output - RESULT_VARIABLE reporters_result - WORKING_DIRECTORY "${TEST_WORKING_DIR}" -) -if(${reporters_result} EQUAL 0) - message(WARNING - "Test executable '${TEST_EXECUTABLE}' contains no reporters!\n" - ) -elseif(${reporters_result} LESS 0) - message(FATAL_ERROR - "Error running test executable '${TEST_EXECUTABLE}':\n" - " Result: ${reporters_result}\n" - " Output: ${reporters_output}\n" - ) -endif() -string(FIND "${reporters_output}" "${reporter}" reporter_is_valid) -if(reporter AND ${reporter_is_valid} EQUAL -1) - message(FATAL_ERROR - "\"${reporter}\" is not a valid reporter!\n" - ) -endif() - -# Prepare reporter -if(reporter) - set(reporter_arg "--reporter ${reporter}") -endif() - -# Prepare output dir -if(output_dir AND NOT IS_ABSOLUTE ${output_dir}) - set(output_dir "${TEST_WORKING_DIR}/${output_dir}") - if(NOT EXISTS ${output_dir}) - file(MAKE_DIRECTORY ${output_dir}) - endif() -endif() - -# Parse output -foreach(line ${output}) - set(test ${line}) - # Escape characters in test case names that would be parsed by Catch2 - set(test_name ${test}) - foreach(char , [ ]) - string(REPLACE ${char} "\\${char}" test_name ${test_name}) - endforeach(char) - # ...add output dir - if(output_dir) - string(REGEX REPLACE "[^A-Za-z0-9_]" "_" test_name_clean ${test_name}) - set(output_dir_arg "--out ${output_dir}/${output_prefix}${test_name_clean}${output_suffix}") - endif() - - # ...and add to script - add_command(add_test - "${prefix}${test}${suffix}" - ${TEST_EXECUTOR} - "${TEST_EXECUTABLE}" - "${test_name}" - ${extra_args} - "${reporter_arg}" - "${output_dir_arg}" - ) - add_command(set_tests_properties - "${prefix}${test}${suffix}" - PROPERTIES - WORKING_DIRECTORY "${TEST_WORKING_DIR}" - ${properties} - ) - list(APPEND tests "${prefix}${test}${suffix}") -endforeach() - -# Create a list of all discovered tests, which users may use to e.g. set -# properties on the tests -add_command(set ${TEST_LIST} ${tests}) - -# Write CTest script -file(WRITE "${CTEST_FILE}" "${script}") diff --git a/server/src/board/tile/rail/turnout/turnoutrailtile.hpp b/server/src/board/tile/rail/turnout/turnoutrailtile.hpp index 4e82d018..ce244fc7 100644 --- a/server/src/board/tile/rail/turnout/turnoutrailtile.hpp +++ b/server/src/board/tile/rail/turnout/turnoutrailtile.hpp @@ -28,7 +28,7 @@ #include "../../../map/node.hpp" #include "../../../../core/objectproperty.hpp" #include "../../../../core/method.hpp" -#include "../../../../enum/turnoutposition.hpp" +#include #include "../../../../hardware/output/map/turnoutoutputmap.hpp" class BlockPath; diff --git a/server/src/core/attributes.hpp b/server/src/core/attributes.hpp index 018fe590..0493e60a 100644 --- a/server/src/core/attributes.hpp +++ b/server/src/core/attributes.hpp @@ -80,7 +80,7 @@ struct Attributes } template - static inline void addClassList(InterfaceItem& item, const std::array& classList) + static inline void addClassList(InterfaceItem& item, tcb::span classList) { item.addAttribute(AttributeName::ClassList, classList); } 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 diff --git a/server/src/hardware/interface/interfacelist.cpp b/server/src/hardware/interface/interfacelist.cpp index 958148e4..ca3718de 100644 --- a/server/src/hardware/interface/interfacelist.cpp +++ b/server/src/hardware/interface/interfacelist.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2021,2023 Reinder Feenstra + * Copyright (C) 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 @@ -46,7 +46,7 @@ InterfaceList::InterfaceList(Object& _parent, std::string_view parentPropertyNam Attributes::addDisplayName(create, DisplayName::List::create); Attributes::addEnabled(create, editable); - Attributes::addClassList(create, Interfaces::classList); + Attributes::addClassList(create, Interfaces::classList()); m_interfaceItems.add(create); Attributes::addDisplayName(delete_, DisplayName::List::delete_); diff --git a/server/src/hardware/interface/interfaces.cpp b/server/src/hardware/interface/interfaces.cpp index 574a9566..30f1717f 100644 --- a/server/src/hardware/interface/interfaces.cpp +++ b/server/src/hardware/interface/interfaces.cpp @@ -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 @@ -23,6 +23,35 @@ #include "interfaces.hpp" #include "../../utils/ifclassidcreate.hpp" #include "../../world/world.hpp" +#include "../../utils/makearray.hpp" + +#include "dccexinterface.hpp" +#include "ecosinterface.hpp" +#include "hsi88.hpp" +#include "loconetinterface.hpp" +#include "marklincaninterface.hpp" +#include "traintasticdiyinterface.hpp" +#include "withrottleinterface.hpp" +#include "wlanmausinterface.hpp" +#include "xpressnetinterface.hpp" +#include "z21interface.hpp" + +tcb::span Interfaces::classList() +{ + static constexpr auto classes = makeArray( + DCCEXInterface::classId, + ECoSInterface::classId, + HSI88Interface::classId, + LocoNetInterface::classId, + MarklinCANInterface::classId, + TraintasticDIYInterface::classId, + WiThrottleInterface::classId, + WlanMausInterface::classId, + XpressNetInterface::classId, + Z21Interface::classId + ); + return classes; +} std::shared_ptr Interfaces::create(World& world, std::string_view classId, std::string_view id) { diff --git a/server/src/hardware/interface/interfaces.hpp b/server/src/hardware/interface/interfaces.hpp index 6b156634..3ed606ce 100644 --- a/server/src/hardware/interface/interfaces.hpp +++ b/server/src/hardware/interface/interfaces.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 @@ -24,36 +24,12 @@ #define TRAINTASTIC_SERVER_HARDWARE_INTERFACE_INTERFACES_HPP #include "interface.hpp" -#include "../../utils/makearray.hpp" - -#include "dccexinterface.hpp" -#include "ecosinterface.hpp" -#include "hsi88.hpp" -#include "loconetinterface.hpp" -#include "marklincaninterface.hpp" -#include "traintasticdiyinterface.hpp" -#include "withrottleinterface.hpp" -#include "wlanmausinterface.hpp" -#include "xpressnetinterface.hpp" -#include "z21interface.hpp" struct Interfaces { static constexpr std::string_view classIdPrefix = "interface."; - static constexpr auto classList = makeArray( - DCCEXInterface::classId, - ECoSInterface::classId, - HSI88Interface::classId, - LocoNetInterface::classId, - MarklinCANInterface::classId, - TraintasticDIYInterface::classId, - WiThrottleInterface::classId, - WlanMausInterface::classId, - XpressNetInterface::classId, - Z21Interface::classId - ); - + static tcb::span classList(); static std::shared_ptr create(World& world, std::string_view classId, std::string_view id = std::string_view{}); }; diff --git a/server/src/hardware/output/map/turnoutoutputmapitem.hpp b/server/src/hardware/output/map/turnoutoutputmapitem.hpp index f3b1b489..be482065 100644 --- a/server/src/hardware/output/map/turnoutoutputmapitem.hpp +++ b/server/src/hardware/output/map/turnoutoutputmapitem.hpp @@ -24,7 +24,7 @@ #define TRAINTASTIC_SERVER_HARDWARE_OUTPUT_MAP_TURNOUTOUTPUTMAPITEM_HPP #include "outputmapitembase.hpp" -#include "../../../enum/turnoutposition.hpp" +#include class TurnoutOutputMapItem : public OutputMapItemBase { diff --git a/server/src/lua/enums.hpp b/server/src/lua/enums.hpp index 6b91996d..a3e287f3 100644 --- a/server/src/lua/enums.hpp +++ b/server/src/lua/enums.hpp @@ -37,9 +37,9 @@ #include #include #include "../../src/enum/tristate.hpp" -#include "../../src/enum/turnoutposition.hpp" +#include #include "../../src/enum/signalaspect.hpp" -#include "../../src/enum/worldevent.hpp" +#include #include "../../src/enum/worldscale.hpp" #include @@ -81,6 +81,14 @@ struct Enums if constexpr(sizeof...(Ts) != 0) registerValues(L); } + + template + inline static const std::array getMetaTableNames() + { + return std::array{EnumName::value...}; + } + + inline static const auto metaTableNames = getMetaTableNames(); }; } diff --git a/server/src/lua/error.hpp b/server/src/lua/error.hpp index 71ba0ab0..22550aac 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(); } @@ -50,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/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) 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) diff --git a/server/src/lua/persistentvariables.cpp b/server/src/lua/persistentvariables.cpp new file mode 100644 index 00000000..616773ab --- /dev/null +++ b/server/src/lua/persistentvariables.cpp @@ -0,0 +1,463 @@ +/** + * 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 +#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 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_absindex(L, index)); + + checkTableRecursion(L, indices); +} + +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, __pairs); + lua_setfield(L, -2, "__pairs"); + 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)); + 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& value) +{ + switch(value.type()) + { + case nlohmann::json::value_t::null: + return lua_pushnil(L); + + 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: + { + 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 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)) + { + 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()) + { + return VectorProperty::push(L, *property); + } + return lua_pushnil(L); + } + } + } + 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"]); + } + } + } + break; + } + 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) +{ + switch(lua_type(L, index)) + { + case LUA_TNIL: /*[[unlikely]]*/ + return nullptr; + + case LUA_TBOOLEAN: + return (lua_toboolean(L, index) != 0); + + 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)) + { + 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 + } + lua_pop(L, 1); // pop table + + 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); + + if(contains(Enums::metaTableNames, name)) + { + auto value = nlohmann::json::object(); + value.emplace("type", std::string("enum.").append(name)); + value.emplace("value", checkEnum(L, index, name.data())); + return value; + } + else if(contains(Sets::metaTableNames, name)) + { + auto value = nlohmann::json::object(); + value.emplace("type", std::string("set.").append(name)); + value.emplace("value", checkSet(L, index, name.data())); + return value; + } + } + break; + + case LUA_TTABLE: + case LUA_TLIGHTUSERDATA: + case LUA_TFUNCTION: + case LUA_TTHREAD: + default: + break; + } + assert(false); + errorInternal(L); +} + +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) +{ + checkValue(L, 2); + + if(lua_istable(L, 3)) + { + checkTableRecursion(L, 3); + + 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)); + 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::__pairs(lua_State* L) +{ + lua_getglobal(L, "next"); + assert(lua_isfunction(L, -1)); + const auto& pv = *static_cast(lua_touserdata(L, 1)); + lua_rawgeti(L, LUA_REGISTRYINDEX, pv.registryIndex); + assert(lua_istable(L, -1)); + return 2; +} + +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)); + luaL_unref(L, LUA_REGISTRYINDEX, pv->registryIndex); + pv->~PersistentVariablesData(); + 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 new file mode 100644 index 00000000..32eded05 --- /dev/null +++ b/server/src/lua/persistentvariables.hpp @@ -0,0 +1,52 @@ +/** + * 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 __pairs(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& value); + 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..0b95e54d 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,17 @@ SandboxPtr Sandbox::create(Script& script) Log::push(L); lua_setfield(L, -2, "log"); + // add persistent variables: + if(script.m_persistentVariables.empty()) + { + PersistentVariables::push(L); + } + else + { + PersistentVariables::push(L, script.m_persistentVariables); + } + lua_setfield(L, -2, "pv"); + // add class types: lua_newtable(L); Class::registerValues(L); @@ -268,6 +283,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..3e676004 100644 --- a/server/src/lua/script.cpp +++ b/server/src/lua/script.cpp @@ -25,8 +25,8 @@ #include "scriptlisttablemodel.hpp" #include "push.hpp" #include "../world/world.hpp" -#include "../enum/worldevent.hpp" -#include "../set/worldstate.hpp" +#include +#include #include "../core/attributes.hpp" #include "../core/method.tpp" #include "../core/objectproperty.tpp" @@ -65,6 +65,13 @@ Script::Script(World& world, std::string_view _id) : if(state == LuaScriptState::Running) stopSandbox(); }} + , clearPersistentVariables{*this, "clear_persistent_variables", + [this]() + { + m_persistentVariables = nullptr; + Log::log(*this, LogMessage::I9003_CLEARED_PERSISTENT_VARIABLES); + updateEnabled(); + }} { Attributes::addDisplayName(name, DisplayName::Object::name); Attributes::addEnabled(name, false); @@ -80,6 +87,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(); } @@ -92,6 +101,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 +117,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() @@ -156,14 +179,18 @@ 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()); + + m_world.luaScripts->updateEnabled(); } void Script::setState(LuaScriptState value) diff --git a/server/src/lua/script.hpp b/server/src/lua/script.hpp index aca5c83a..35ffdebf 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; @@ -65,6 +68,7 @@ class Script : public IdObject Property error; ::Method start; ::Method stop; + ::Method clearPersistentVariables; }; } 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