diff --git a/.clang-tidy b/.clang-tidy index f33ac2e3..f9bb9ef8 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,11 +1,22 @@ --- Checks: > -*, + misc-*, + -misc-no-recursion, + modernize-*, + -modernize-avoid-bind, + -modernize-avoid-c-arrays, + -modernize-use-trailing-return-type, readability-*, -readability-braces-around-statements, -readability-implicit-bool-conversion, -readability-identifier-length, - -readability-magic-numbers + -readability-magic-numbers, + -readability-suspicious-call-argument + +WarningsAsErrors: > + readability-*, + -readability-function-cognitive-complexity CheckOptions: - {key: readability-identifier-naming.NamespaceCase, value: CamelCase} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 25d3af46..36b199ce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,16 +20,6 @@ jobs: build_deb: false defines: "" - - name: "ubuntu_22.04" - os: ubuntu-22.04 - generator: "Unix Makefiles" - arch: "" - target: traintastic-client - jobs: 4 - build_type: Release - build_deb: true - defines: "" - - name: "ubuntu_24.04" os: ubuntu-24.04 generator: "Unix Makefiles" @@ -60,16 +50,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" @@ -80,16 +60,6 @@ jobs: build_deb: false defines: "" - - name: "macos-14" - os: "macos-14" - generator: "Unix Makefiles" - arch: "" - target: traintastic-client - jobs: 3 - build_type: Release - build_deb: false - defines: "" - steps: - uses: FranzDiebold/github-env-vars-action@v2 @@ -112,8 +82,16 @@ jobs: uses: jurplel/install-qt-action@v4 with: cache: true + modules: qtwebsockets version: 6.5.* + - name: Set CMAKE_OSX_ARCHITECTURES + if: startswith(matrix.config.os, 'macos') + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('CMAKE_OSX_ARCHITECTURES', 'x86_64'); + # Ubuntu only: - name: apt update if: startswith(matrix.config.os, 'ubuntu') @@ -122,7 +100,7 @@ jobs: # Ubuntu only: - name: Install packages if: startswith(matrix.config.os, 'ubuntu') - run: sudo apt install qtbase5-dev qtbase5-dev-tools libqt5svg5-dev + run: sudo apt install qtbase5-dev qtbase5-dev-tools libqt5svg5-dev libqt5websockets5-dev # All: - name: Create Build Environment @@ -157,22 +135,24 @@ jobs: # Windows only: - name: Upload artifact if: startswith(matrix.config.os, 'windows') - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: traintastic-client + name: traintastic-client-windows path: ${{github.workspace}}/client/build/${{matrix.config.build_type}} # Linux only: - name: Upload debian package artifact if: matrix.config.build_deb - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: traintastic-client-deb + name: traintastic-client-deb-${{matrix.config.name}} path: ${{github.workspace}}/client/build/*.deb build-server: name: server ${{matrix.config.name}} runs-on: ${{matrix.config.os}} + env: + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" strategy: fail-fast: false matrix: @@ -186,33 +166,9 @@ jobs: jobs: 4 build_type: Release build_deb: false - defines: "-DENABLE_CLANG_TIDY=ON" - ccov: false - - - name: "windows_x64_mingw" - os: windows-2022 - generator: "MinGW Makefiles" - arch: "" - toolset: "" - target: all - jobs: 4 - build_type: Release - build_deb: false defines: "" ccov: false - - name: "ubuntu_22.04" - os: ubuntu-22.04 - generator: "Unix Makefiles" - arch: "" - toolset: "" - target: all - jobs: 4 - build_type: Release - build_deb: true - defines: "-DENABLE_CLANG_TIDY=ON -DINSTALL_SYSTEMD_SERVICE=ON" - ccov: false - - name: "ubuntu_24.04" os: ubuntu-24.04 generator: "Unix Makefiles" @@ -222,11 +178,11 @@ jobs: jobs: 4 build_type: Release build_deb: true - defines: "-DENABLE_CLANG_TIDY=ON -DINSTALL_SYSTEMD_SERVICE=ON" + defines: "-DINSTALL_SYSTEMD_SERVICE=ON" ccov: false - - name: "ubuntu_latest (debug+ccov)" - os: ubuntu-latest + - name: "ubuntu_24.04 (debug+ccov)" + os: ubuntu-24.04 generator: "Unix Makefiles" arch: "" toolset: "" @@ -234,7 +190,7 @@ jobs: jobs: 4 build_type: Debug build_deb: false - defines: "-DENABLE_CLANG_TIDY=ON -DCODE_COVERAGE=ON" + defines: "-DCODE_COVERAGE=ON" ccov: true - name: "raspberrypios_arm7" @@ -261,18 +217,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" @@ -285,18 +229,6 @@ jobs: defines: "" ccov: false - - name: "macos-14" - os: "macos-14" - 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 @@ -306,6 +238,27 @@ jobs: with: submodules: recursive + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Set VCPKG_ROOT + if: startswith(matrix.config.os, 'windows') || startswith(matrix.config.os, 'macos') + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('VCPKG_ROOT', process.env.VCPKG_INSTALLATION_ROOT || ''); + + - name: Set CMAKE_OSX_ARCHITECTURES + if: startswith(matrix.config.os, 'macos') + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('CMAKE_OSX_ARCHITECTURES', 'x86_64'); + # Ubuntu only: - name: apt update if: startswith(matrix.config.os, 'ubuntu') @@ -314,12 +267,12 @@ jobs: # Ubuntu only: - name: Install packages if: startswith(matrix.config.os, 'ubuntu') - run: sudo apt install libboost-program-options-dev liblua5.3-dev lcov libarchive-dev clang-tidy libsystemd-dev + run: sudo apt install libboost-program-options-dev liblua5.4-dev lcov libarchive-dev clang-tidy libsystemd-dev # MacOS only: - name: Install brew packages if: startswith(matrix.config.os, 'macos') - run: brew install libarchive lua@5.3 + run: brew install libarchive lua@5.4 # All: - name: Create Build Environment @@ -366,17 +319,17 @@ jobs: # Windows only: - name: Upload artifact if: matrix.config.name == 'windows_x64_clang' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: traintastic-server + name: traintastic-server-windows path: ${{github.workspace}}/server/build/Release/traintastic-server.exe # Linux only: - name: Upload debian package artifact if: matrix.config.build_deb - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: traintastic-server-deb + name: traintastic-server-deb-${{matrix.config.name}} path: ${{github.workspace}}/server/build/*.deb # Code coverage: @@ -395,7 +348,7 @@ jobs: - name: "Code coverage: upload artifact" if: matrix.config.ccov - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: traintastic-server-test-code-coverage path: ${{github.workspace}}/server/build/ccov/* @@ -408,10 +361,6 @@ jobs: fail-fast: false matrix: config: - - name: "ubuntu_22.04" - os: ubuntu-22.04 - defines: "" - - name: "ubuntu_24.04" os: ubuntu-24.04 defines: "" @@ -429,7 +378,7 @@ jobs: submodules: recursive - name: "Download artifact: lang" - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: traintastic-lang path: ${{github.workspace}}/shared/translations @@ -447,9 +396,9 @@ jobs: run: cpack - name: Upload debian package artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: traintastic-data-deb + name: traintastic-data-deb-${{matrix.config.name}} path: ${{github.workspace}}/shared/build/*.deb build-lang: @@ -469,7 +418,7 @@ jobs: run: python3 json2lang.py - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: traintastic-lang path: ${{github.workspace}}/shared/translations/*.lang @@ -494,7 +443,7 @@ jobs: run: ./builddoc.py html-single-page --output-dir build - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: traintastic-manual path: ${{github.workspace}}/manual/build/* @@ -516,7 +465,7 @@ jobs: run: ./buildluadoc.py - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: traintastic-manual-lua path: ${{github.workspace}}/manual/build.luadoc/* @@ -535,31 +484,31 @@ jobs: submodules: recursive - name: Download artifacts 1/2 - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: - name: traintastic-client + name: traintastic-client-windows path: ${{github.workspace}}/client/build/Release - name: Download artifacts 2/2 - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: - name: traintastic-server + name: traintastic-server-windows path: ${{github.workspace}}/server/build - name: "Download artifact: lang" - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: traintastic-lang path: ${{github.workspace}}/shared/translations - name: "Download artifact: manual" - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: traintastic-manual path: ${{github.workspace}}/manual/build - name: "Download artifact: manual-lua" - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: traintastic-manual-lua path: ${{github.workspace}}/manual/build.luadoc @@ -570,7 +519,7 @@ jobs: "C:/Program Files (x86)/Inno Setup 6/ISCC.exe" %GITHUB_WORKSPACE%/package/innosetup/traintastic.iss - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: package-innosetup path: ${{github.workspace}}/package/innosetup/output @@ -585,43 +534,46 @@ jobs: - uses: FranzDiebold/github-env-vars-action@v2 - name: Download artifacts 1/6 - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: package-innosetup path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}} - name: Download artifacts 2/6 - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: - name: traintastic-client-deb + pattern: traintastic-client-deb-* + merge-multiple: true path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}} - name: Download artifacts 3/6 - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: - name: traintastic-server-deb + pattern: traintastic-server-deb-* + merge-multiple: true path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}} - name: Download artifacts 4/6 - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: traintastic-server-test-code-coverage path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}}/ccov - name: Download artifacts 5/6 - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: traintastic-manual path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}}/manual - name: Download artifacts 6/6 - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: - name: traintastic-data-deb + pattern: traintastic-data-deb-* + merge-multiple: true path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}} - name: "Download artifact: manual-lua" - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: traintastic-manual-lua path: ${{github.workspace}}/dist/${{env.CI_REF_NAME_SLUG}}/${{github.run_number}}/manual-lua 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/CMakeLists.txt b/client/CMakeLists.txt index a3a64222..1c0caefa 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -90,17 +90,17 @@ file(GLOB SOURCES "thirdparty/QtWaitingSpinner/*.hpp" "thirdparty/QtWaitingSpinner/*.cpp") -find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Widgets Network Svg Xml) +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Widgets Network Svg Xml WebSockets) if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets Network Svg Xml) + find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets Network Svg Xml WebSockets) else() - find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets Network Svg Xml SvgWidgets) + find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets Network Svg Xml SvgWidgets WebSockets) endif() message(STATUS "Found Qt ${QT_VERSION}") target_sources(traintastic-client PRIVATE ${SOURCES}) -target_link_libraries(traintastic-client PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Svg Qt${QT_VERSION_MAJOR}::Xml) +target_link_libraries(traintastic-client PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Svg Qt${QT_VERSION_MAJOR}::Xml Qt${QT_VERSION_MAJOR}::WebSockets) if(QT_VERSION_MAJOR GREATER 5) target_link_libraries(traintastic-client PRIVATE Qt${QT_VERSION_MAJOR}::SvgWidgets) endif() @@ -130,6 +130,11 @@ install(TARGETS traintastic-client RUNTIME DESTINATION "/opt/traintastic/bin/" ) +if(LINUX) + install(FILES ${CMAKE_SOURCE_DIR}/debian/traintastic-client.desktop DESTINATION share/applications) + install(FILES ${CMAKE_SOURCE_DIR}/debian/traintastic_256.png DESTINATION "/opt/traintastic/icons/") +endif() + ### BUILD TYPE ### if (CMAKE_BUILD_TYPE EQUAL "Release") diff --git a/client/data/wizard/add_interface.json b/client/data/wizard/add_interface.json index 17133b43..58213ef1 100644 --- a/client/data/wizard/add_interface.json +++ b/client/data/wizard/add_interface.json @@ -333,11 +333,13 @@ "protocol_dr5000_usb": { "title": "$wizard.add_interface.protocol:title$", "text": "$wizard.add_interface.protocol:text$", + "bottom_text": "$wizard.add_interface.protocol_dr5000_usb:bottom_text$", "type": "radio", "options": [ { - "name": "LocoNet", + "name": "LocoNet ($wizard.add_interface:recommended$)", "next": "serial_port", + "checked": true, "actions": { "create_interface": { "class_id": "interface.loconet", diff --git a/client/debian/traintastic-client.desktop b/client/debian/traintastic-client.desktop new file mode 100644 index 00000000..78953c8b --- /dev/null +++ b/client/debian/traintastic-client.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Name=Traintastic client +Comment=Model railroad control and automation software +Exec=/opt/traintastic/bin/traintastic-client +Icon=/opt/traintastic/icons/traintastic_256.png +Terminal=false +Categories=Science;Engineering; diff --git a/client/debian/traintastic_256.png b/client/debian/traintastic_256.png new file mode 100644 index 00000000..f439dd76 Binary files /dev/null and b/client/debian/traintastic_256.png differ 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..ccbd7c4c 100644 --- a/client/gfx/dark/dark.qrc +++ b/client/gfx/dark/dark.qrc @@ -92,5 +92,7 @@ board_tile.rail.nx_button.svg board_tile.misc.switch.svg board_tile.misc.label.svg + clear_persistent_variables.svg + swap.svg diff --git a/client/gfx/dark/swap.svg b/client/gfx/dark/swap.svg new file mode 100644 index 00000000..0221211b --- /dev/null +++ b/client/gfx/dark/swap.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + 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/board/boardareawidget.cpp b/client/src/board/boardareawidget.cpp index 5cae0025..53d310b4 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) @@ -64,14 +66,14 @@ constexpr QRect updateTileRect(const int x, const int y, const int w, const int } -BoardAreaWidget::BoardAreaWidget(BoardWidget& board, QWidget* parent) : +BoardAreaWidget::BoardAreaWidget(std::shared_ptr board, QWidget* parent) : QWidget(parent), m_colorScheme{&BoardColorScheme::dark}, - m_board{board}, - m_boardLeft{board.board().getProperty("left")}, - m_boardTop{board.board().getProperty("top")}, - m_boardRight{board.board().getProperty("right")}, - m_boardBottom{board.board().getProperty("bottom")}, + m_board{std::move(board)}, + m_boardLeft{m_board->getProperty("left")}, + m_boardTop{m_board->getProperty("top")}, + m_boardRight{m_board->getProperty("right")}, + m_boardBottom{m_board->getProperty("bottom")}, m_grid{Grid::Dot}, m_zoomLevel{0}, m_mouseLeftButtonPressed{false}, @@ -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); @@ -96,7 +99,7 @@ BoardAreaWidget::BoardAreaWidget(BoardWidget& board, QWidget* parent) : connect(&BoardSettings::instance(), &SettingsBase::changed, this, &BoardAreaWidget::settingsChanged); - for(const auto& [l, object] : m_board.board().tileObjects()) + for(const auto& [l, object] : m_board->tileObjects()) tileObjectAdded(l.x, l.y, object); settingsChanged(); @@ -112,7 +115,7 @@ void BoardAreaWidget::tileObjectAdded(int16_t x, int16_t y, const ObjectPtr& obj { try { - const TileData& tileData = m_board.board().tileData().at(l); + const TileData& tileData = m_board->tileData().at(l); update(updateTileRect(l.x - boardLeft(), l.y - boardTop(), tileData.width(), tileData.height(), getTileSize())); } catch(...) @@ -127,7 +130,7 @@ void BoardAreaWidget::tileObjectAdded(int16_t x, int16_t y, const ObjectPtr& obj connect(property, &BaseProperty::valueChanged, this, handler); }; - switch(m_board.board().getTileId(l)) + switch(m_board->getTileId(l)) { case TileId::RailTurnoutLeft45: case TileId::RailTurnoutLeft90: @@ -192,6 +195,7 @@ void BoardAreaWidget::tileObjectAdded(int16_t x, int16_t y, const ObjectPtr& obj case TileId::RailTunnel: case TileId::RailOneWay: case TileId::RailLink: + case TileId::HiddenRailCrossOver: case TileId::ReservedForFutureExpension: break; @@ -307,7 +311,7 @@ void BoardAreaWidget::setMouseMoveTileSizeMax(uint8_t width, uint8_t height) TurnoutPosition BoardAreaWidget::getTurnoutPosition(const TileLocation& l) const { - if(ObjectPtr object = m_board.board().getTileObject(l)) + if(ObjectPtr object = m_board->getTileObject(l)) if(const auto* p = object->getProperty("position")) return p->toEnum(); return TurnoutPosition::Unknown; @@ -315,7 +319,7 @@ TurnoutPosition BoardAreaWidget::getTurnoutPosition(const TileLocation& l) const SensorState BoardAreaWidget::getSensorState(const TileLocation& l) const { - if(ObjectPtr object = m_board.board().getTileObject(l)) + if(ObjectPtr object = m_board->getTileObject(l)) if(const auto* p = object->getProperty("state")) return p->toEnum(); return SensorState::Unknown; @@ -323,7 +327,7 @@ SensorState BoardAreaWidget::getSensorState(const TileLocation& l) const DirectionControlState BoardAreaWidget::getDirectionControlState(const TileLocation& l) const { - if(ObjectPtr object = m_board.board().getTileObject(l)) + if(ObjectPtr object = m_board->getTileObject(l)) if(const auto* p = object->getProperty("state")) return p->toEnum(); return DirectionControlState::Both; @@ -331,7 +335,7 @@ DirectionControlState BoardAreaWidget::getDirectionControlState(const TileLocati SignalAspect BoardAreaWidget::getSignalAspect(const TileLocation& l) const { - if(ObjectPtr object = m_board.board().getTileObject(l)) + if(ObjectPtr object = m_board->getTileObject(l)) if(const auto* p = object->getProperty("aspect")) return p->toEnum(); return SignalAspect::Unknown; @@ -339,7 +343,7 @@ SignalAspect BoardAreaWidget::getSignalAspect(const TileLocation& l) const Color BoardAreaWidget::getColor(const TileLocation& l) const { - if(ObjectPtr object = m_board.board().getTileObject(l)) + if(ObjectPtr object = m_board->getTileObject(l)) if(const auto* p = object->getProperty("color")) return p->toEnum(); return Color::None; @@ -347,7 +351,7 @@ Color BoardAreaWidget::getColor(const TileLocation& l) const DecouplerState BoardAreaWidget::getDecouplerState(const TileLocation& l) const { - if(ObjectPtr object = m_board.board().getTileObject(l)) + if(ObjectPtr object = m_board->getTileObject(l)) if(const auto* p = object->getProperty("state")) return p->toEnum(); return DecouplerState::Deactivated; @@ -355,7 +359,7 @@ DecouplerState BoardAreaWidget::getDecouplerState(const TileLocation& l) const bool BoardAreaWidget::getNXButtonEnabled(const TileLocation& l) const { - if(auto object = std::dynamic_pointer_cast(m_board.board().getTileObject(l))) + if(auto object = std::dynamic_pointer_cast(m_board->getTileObject(l))) { return object->getPropertyValueBool("enabled", false); } @@ -364,7 +368,7 @@ bool BoardAreaWidget::getNXButtonEnabled(const TileLocation& l) const bool BoardAreaWidget::getNXButtonPressed(const TileLocation& l) const { - if(auto object = std::dynamic_pointer_cast(m_board.board().getTileObject(l))) + if(auto object = std::dynamic_pointer_cast(m_board->getTileObject(l))) { return object->isPressed(); } @@ -379,11 +383,11 @@ TileLocation BoardAreaWidget::pointToTileLocation(const QPoint& p) QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const { - const auto tileId = m_board.board().getTileId(l); + const auto tileId = m_board->getTileId(l); if(isRailTurnout(tileId)) { - if(auto turnout = m_board.board().getTileObject(l)) + if(auto turnout = m_board->getTileObject(l)) { QString text = "" + turnout->getPropertyValueString("name") + ""; if(auto* position = turnout->getProperty("position")) /*[[likely]]*/ @@ -395,7 +399,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const } else if(isRailSignal(tileId)) { - if(auto signal = m_board.board().getTileObject(l)) + if(auto signal = m_board->getTileObject(l)) { QString text = "" + signal->getPropertyValueString("name") + ""; if(auto* aspect = signal->getProperty("aspect")) /*[[likely]]*/ @@ -407,7 +411,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const } else if(tileId == TileId::RailSensor) { - if(auto sensor = m_board.board().getTileObject(l)) + if(auto sensor = m_board->getTileObject(l)) { QString text = "" + sensor->getPropertyValueString("name") + ""; if(auto* state = sensor->getProperty("state")) /*[[likely]]*/ @@ -419,7 +423,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const } else if(tileId == TileId::RailBlock) { - if(auto block = m_board.board().getTileObject(l)) + if(auto block = m_board->getTileObject(l)) { QString text = "" + block->getPropertyValueString("name") + ""; if(auto* state = block->getProperty("state")) /*[[likely]]*/ @@ -431,7 +435,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const } else if(tileId == TileId::RailDirectionControl) { - if(auto directionControl = m_board.board().getTileObject(l)) + if(auto directionControl = m_board->getTileObject(l)) { QString text = "" + directionControl->getPropertyValueString("name") + ""; if(auto* state = directionControl->getProperty("state")) /*[[likely]]*/ @@ -443,7 +447,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const } else if(tileId == TileId::RailSensor) { - if(auto sensor = m_board.board().getTileObject(l)) + if(auto sensor = m_board->getTileObject(l)) { QString text = "" + sensor->getPropertyValueString("name") + ""; if(auto* state = sensor->getProperty("state")) /*[[likely]]*/ @@ -455,7 +459,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const } else if(tileId == TileId::Switch) { - if(auto switch_ = m_board.board().getTileObject(l)) + if(auto switch_ = m_board->getTileObject(l)) { QString text = "" + switch_->getPropertyValueString("name") + ""; if(auto* value = switch_->getProperty("value")) /*[[likely]]*/ @@ -480,7 +484,7 @@ QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const else if(tileId == TileId::PushButton || tileId == TileId::RailNXButton) { - if(auto tile = m_board.board().getTileObject(l)) + if(auto tile = m_board->getTileObject(l)) { return "" + tile->getPropertyValueString("name") + ""; } @@ -667,7 +671,7 @@ void BoardAreaWidget::paintEvent(QPaintEvent* event) painter.save(); - for(auto it : m_board.board().tileData()) + for(auto it : m_board->tileData()) if(it.first.x + it.second.width() - 1 >= tiles.left() && it.first.x <= tiles.right() && it.first.y + it.second.height() - 1 >= tiles.top() && it.first.y <= tiles.bottom()) { @@ -727,7 +731,7 @@ void BoardAreaWidget::paintEvent(QPaintEvent* event) break; case TileId::RailBlock: - tilePainter.drawBlock(id, r, a, state & 0x01, state & 0x02, m_board.board().getTileObject(it.first)); + tilePainter.drawBlock(id, r, a, state & 0x01, state & 0x02, m_board->getTileObject(it.first)); break; case TileId::RailDirectionControl: @@ -748,7 +752,7 @@ void BoardAreaWidget::paintEvent(QPaintEvent* event) case TileId::Label: { - if(auto label = m_board.board().getTileObject(it.first)) /*[[likely]]*/ + if(auto label = m_board->getTileObject(it.first)) /*[[likely]]*/ { tilePainter.drawLabel(r, a, label->getPropertyValueString("text"), @@ -763,7 +767,7 @@ void BoardAreaWidget::paintEvent(QPaintEvent* event) break; } case TileId::Switch: - if(auto sw = m_board.board().getTileObject(it.first)) /*[[likely]]*/ + if(auto sw = m_board->getTileObject(it.first)) /*[[likely]]*/ { tilePainter.drawSwitch(r, sw->getPropertyValueBool("value", false), @@ -837,6 +841,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->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->getTileId(l)) + { + case TileId::RailBlock: + if(auto* assignTrain = dynamic_cast(event->mimeData())) + { + if(auto tile = std::dynamic_pointer_cast(m_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..39d2d8ab 100644 --- a/client/src/board/boardareawidget.hpp +++ b/client/src/board/boardareawidget.hpp @@ -38,7 +38,7 @@ #include "../network/abstractproperty.hpp" #include "../network/objectptr.hpp" -class BoardWidget; +class Board; class BoardAreaWidget : public QWidget { @@ -66,7 +66,7 @@ class BoardAreaWidget : public QWidget protected: static constexpr int boardMargin = 1; // tile - BoardWidget& m_board; + std::shared_ptr m_board; AbstractProperty* m_boardLeft; AbstractProperty* m_boardTop; AbstractProperty* m_boardRight; @@ -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(); @@ -124,7 +129,7 @@ class BoardAreaWidget : public QWidget static constexpr int zoomLevelMin = -2; static constexpr int zoomLevelMax = 15; - BoardAreaWidget(BoardWidget& board, QWidget* parent = nullptr); + BoardAreaWidget(std::shared_ptr board, QWidget* parent = nullptr); Grid grid() const { return m_grid; } void nextGrid(); diff --git a/client/src/board/boardwidget.cpp b/client/src/board/boardwidget.cpp index d24ea47a..2c18cd7c 100644 --- a/client/src/board/boardwidget.cpp +++ b/client/src/board/boardwidget.cpp @@ -34,7 +34,6 @@ #include #include #include -#include #include #include "getboardcolorscheme.hpp" #include "tilepainter.hpp" @@ -84,7 +83,7 @@ BoardWidget::BoardWidget(std::shared_ptr object, QWidget* parent) : QWidget(parent), m_object{std::move(object)}, m_nxManagerRequestId{Connection::invalidRequestId}, - m_boardArea{new BoardAreaWidget(*this, this)}, + m_boardArea{new BoardAreaWidget(m_object, this)}, m_statusBar{new QStatusBar(this)}, m_statusBarMessage{new QLabel(this)}, m_statusBarCoords{new QLabel(this)}, @@ -92,6 +91,7 @@ BoardWidget::BoardWidget(std::shared_ptr object, QWidget* parent) : m_editActions{new QActionGroup(this)} , m_tileMoveStarted{false} , m_tileResizeStarted{false} + , m_nxButtonTimerId(0) { setWindowIcon(Theme::getIconForClassId(object->classId)); setFocusPolicy(Qt::StrongFocus); @@ -447,6 +447,8 @@ BoardWidget::BoardWidget(std::shared_ptr object, QWidget* parent) : BoardWidget::~BoardWidget() { + stopTimerAndReleaseButtons(); + if(m_nxManagerRequestId != Connection::invalidRequestId) { m_object->connection()->cancelRequest(m_nxManagerRequestId); @@ -459,6 +461,10 @@ void BoardWidget::worldEditChanged(bool value) m_editActionNone->activate(QAction::Trigger); m_toolbarEdit->setVisible(value); m_statusBar->setVisible(value); + + // Stop timers in edit mode + if(value) + stopTimerAndReleaseButtons(); } void BoardWidget::gridChanged(BoardAreaWidget::Grid value) @@ -650,7 +656,8 @@ void BoardWidget::tileClicked(int16_t x, int16_t y) if(nxButton->isPressed()) { - releaseNXButton(nxButton); + stopTimerAndReleaseButtons(); + return; } else { @@ -669,31 +676,13 @@ void BoardWidget::tileClicked(int16_t x, int16_t y) m_nxButtonPressed.reset(); - QTimer::singleShot(nxButtonReleaseDelay, this, - [this, weak1=std::weak_ptr(firstButton), weak2=std::weak_ptr(nxButton)]() - { - if(auto btn = weak1.lock()) - { - releaseNXButton(btn); - } - if(auto btn = weak2.lock()) - { - releaseNXButton(btn); - } - }); + startReleaseTimer(firstButton, nxButton); } else { m_nxButtonPressed = nxButton; - QTimer::singleShot(nxButtonHoldTime, this, - [this, weak=std::weak_ptr(nxButton)]() - { - if(auto btn = weak.lock()) - { - releaseNXButton(btn); - } - }); + startHoldTimer(nxButton); } } } @@ -778,6 +767,53 @@ void BoardWidget::rightClicked() rotateTile(true); } +void BoardWidget::startHoldTimer(const std::shared_ptr& nxButton) +{ + stopTimerAndReleaseButtons(); + + m_releaseButton1 = nxButton; + + assert(m_nxButtonTimerId == 0); + m_nxButtonTimerId = startTimer(nxButtonHoldTime); +} + +void BoardWidget::startReleaseTimer(const std::shared_ptr &firstButton, + const std::shared_ptr &nxButton) +{ + // Do not release first button yet + m_releaseButton1.reset(); + + stopTimerAndReleaseButtons(); + + m_releaseButton1 = firstButton; + m_releaseButton2 = nxButton; + + assert(m_nxButtonTimerId == 0); + m_nxButtonTimerId = startTimer(nxButtonReleaseDelay); +} + +void BoardWidget::stopTimerAndReleaseButtons() +{ + if(m_nxButtonTimerId) + { + // Instantly release buttons + if(auto btn = m_releaseButton1.lock()) + { + releaseNXButton(btn); + } + m_releaseButton1.reset(); + + if(auto btn = m_releaseButton2.lock()) + { + releaseNXButton(btn); + } + m_releaseButton2.reset(); + + killTimer(m_nxButtonTimerId); + m_nxButtonTimerId = 0; + } +} + void BoardWidget::actionSelected(const Board::TileInfo* info) { m_boardArea->setMouseMoveAction(BoardAreaWidget::MouseMoveAction::None); @@ -825,6 +861,17 @@ void BoardWidget::keyPressEvent(QKeyEvent* event) } } +void BoardWidget::timerEvent(QTimerEvent *e) +{ + if(e->timerId() == m_nxButtonTimerId) + { + stopTimerAndReleaseButtons(); + return; + } + + QWidget::timerEvent(e); +} + void BoardWidget::rotateTile(bool ccw) { if(m_tileRotates != 0) diff --git a/client/src/board/boardwidget.hpp b/client/src/board/boardwidget.hpp index ff1c2eec..73e0b973 100644 --- a/client/src/board/boardwidget.hpp +++ b/client/src/board/boardwidget.hpp @@ -78,11 +78,25 @@ class BoardWidget : public QWidget TileRotate m_tileRotateLast = TileRotate::Deg0; //!< Last used tile rotate for add/move std::weak_ptr m_nxButtonPressed; + int m_nxButtonTimerId; + std::weak_ptr m_releaseButton1; + std::weak_ptr m_releaseButton2; + + void startHoldTimer(const std::shared_ptr &nxButton); + void startReleaseTimer(const std::shared_ptr &firstButton, + const std::shared_ptr &nxButton); + void actionSelected(const Board::TileInfo* tile); + void keyPressEvent(QKeyEvent* event) override; + void timerEvent(QTimerEvent *e) override; + void rotateTile(bool ccw = false); void releaseNXButton(const std::shared_ptr& nxButton); + protected slots: + void stopTimerAndReleaseButtons(); + protected slots: void worldEditChanged(bool value); void gridChanged(BoardAreaWidget::Grid value); diff --git a/client/src/board/tilemenu.cpp b/client/src/board/tilemenu.cpp index a38702b9..089b5788 100644 --- a/client/src/board/tilemenu.cpp +++ b/client/src/board/tilemenu.cpp @@ -39,7 +39,7 @@ std::unique_ptr TileMenu::getBlockRailTileMenu(const ObjectPtr& tile, QWi menu->addAction(new MethodAction(*assignTrain, [parent, assignTrain]() { - std::make_unique(*assignTrain, parent)->exec(); + std::make_unique(*assignTrain, false, parent)->exec(); })); if(auto* removeTrain = tile->getMethod("remove_train")) { diff --git a/client/src/board/tilepainter.cpp b/client/src/board/tilepainter.cpp index 5ee80fc7..d78b45e9 100644 --- a/client/src/board/tilepainter.cpp +++ b/client/src/board/tilepainter.cpp @@ -177,6 +177,7 @@ void TilePainter::draw(TileId id, const QRectF& r, TileRotate rotate, bool isRes break; case TileId::None: + case TileId::HiddenRailCrossOver: case TileId::ReservedForFutureExpension: break; } @@ -774,7 +775,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/dialog/connectdialog.cpp b/client/src/dialog/connectdialog.cpp index e52f288e..bf3d15f8 100644 --- a/client/src/dialog/connectdialog.cpp +++ b/client/src/dialog/connectdialog.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 @@ -150,8 +150,10 @@ void ConnectDialog::socketReadyRead() QString name = QString::fromUtf8(message.read()); QUrl url; + url.setScheme("ws"); url.setHost(host.toString()); url.setPort(port); + url.setPath("/client"); auto it = m_servers.find(url); if(it == m_servers.end()) @@ -230,6 +232,12 @@ void ConnectDialog::serverTextChanged(const QString& text) { QString url{text}; m_url = QUrl::fromUserInput(url.remove(QRegularExpression("\\s*\\(.*\\)$"))); + m_url.setScheme("ws"); + m_url.setPath("/client"); + if(m_url.port() == -1) + { + m_url.setPort(Connection::defaultPort); + } m_connect->setEnabled(m_url.isValid()); } diff --git a/client/src/dialog/objectselectlistdialog.cpp b/client/src/dialog/objectselectlistdialog.cpp index 3c6ac841..619b2c62 100644 --- a/client/src/dialog/objectselectlistdialog.cpp +++ b/client/src/dialog/objectselectlistdialog.cpp @@ -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 @@ -34,19 +34,20 @@ #include "../widget/alertwidget.hpp" #include -ObjectSelectListDialog::ObjectSelectListDialog(Method& method, QWidget* parent) : - ObjectSelectListDialog(static_cast(method), parent) +ObjectSelectListDialog::ObjectSelectListDialog(Method& method, bool multiSelect, QWidget* parent) : + ObjectSelectListDialog(static_cast(method), multiSelect, parent) { } ObjectSelectListDialog::ObjectSelectListDialog(ObjectProperty& property, QWidget* parent) : - ObjectSelectListDialog(static_cast(property), parent) + ObjectSelectListDialog(static_cast(property), false, parent) { } -ObjectSelectListDialog::ObjectSelectListDialog(InterfaceItem& item, QWidget* parent) : +ObjectSelectListDialog::ObjectSelectListDialog(InterfaceItem& item, bool multiSelect, QWidget* parent) : QDialog(parent, Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint), m_item{item}, + m_multiSelect{multiSelect}, m_buttons{new QDialogButtonBox(this)}, m_tableWidget{new TableWidget()} { @@ -58,7 +59,14 @@ ObjectSelectListDialog::ObjectSelectListDialog(InterfaceItem& item, QWidget* par connect(m_buttons->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, [this]() { - acceptRow(m_tableWidget->selectionModel()->selectedIndexes().first().row()); + if(m_multiSelect) + { + acceptRows(m_tableWidget->selectionModel()->selectedIndexes()); + } + else + { + acceptRow(m_tableWidget->selectionModel()->selectedIndexes().first().row()); + } }); m_buttons->button(QDialogButtonBox::Cancel)->setText(Locale::tr("qtapp.object_select_list_dialog:cancel")); connect(m_buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &ObjectSelectListDialog::reject); @@ -91,7 +99,8 @@ ObjectSelectListDialog::ObjectSelectListDialog(InterfaceItem& item, QWidget* par connect(m_tableWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this](const QItemSelection&, const QItemSelection&) { - m_buttons->button(QDialogButtonBox::Ok)->setEnabled(m_tableWidget->selectionModel()->selectedRows().count() == 1); + const auto selectionCount = m_tableWidget->selectionModel()->selectedRows().count(); + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(m_multiSelect ? selectionCount > 0 : selectionCount == 1); }); connect(m_tableWidget, &TableWidget::doubleClicked, this, [this](const QModelIndex& index) @@ -133,3 +142,16 @@ void ObjectSelectListDialog::acceptRow(int row) accept(); } + +void ObjectSelectListDialog::acceptRows(const QModelIndexList& indexes) +{ + if(auto* m = dynamic_cast(&m_item)) + { + for(const auto& index : indexes) + { + callMethod(*m, nullptr, m_tableWidget->getRowObjectId(index.row())); + } + } + + accept(); +} diff --git a/client/src/dialog/objectselectlistdialog.hpp b/client/src/dialog/objectselectlistdialog.hpp index f11649c2..e178a88d 100644 --- a/client/src/dialog/objectselectlistdialog.hpp +++ b/client/src/dialog/objectselectlistdialog.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2019-2020 Reinder Feenstra + * Copyright (C) 2019-2020,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,7 @@ #define TRAINTASTIC_CLIENT_DIALOG_OBJECTSELECTLISTDIALOG_HPP #include +#include #include "../network/objectptr.hpp" class QDialogButtonBox; @@ -39,17 +40,19 @@ class ObjectSelectListDialog : public QDialog protected: InterfaceItem& m_item; + const bool m_multiSelect; ObjectPtr m_object; int m_requestId; QDialogButtonBox* m_buttons; TableWidget* m_tableWidget; - ObjectSelectListDialog(InterfaceItem& item, QWidget* parent); + ObjectSelectListDialog(InterfaceItem& item, bool multiSelect, QWidget* parent); void acceptRow(int row); + void acceptRows(const QModelIndexList& indexes); public: - ObjectSelectListDialog(Method& method, QWidget* parent); + ObjectSelectListDialog(Method& method, bool multiSelect, QWidget* parent); ObjectSelectListDialog(ObjectProperty& property, QWidget* parent); }; diff --git a/client/src/main.cpp b/client/src/main.cpp index 3e530ecc..f98045ed 100644 --- a/client/src/main.cpp +++ b/client/src/main.cpp @@ -26,6 +26,9 @@ #endif #include #include +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + #include +#endif #include #include "mainwindow.hpp" #include "settings/generalsettings.hpp" @@ -124,9 +127,15 @@ int main(int argc, char* argv[]) if(logMissingStrings) const_cast(Locale::instance.get())->enableMissingLogging(); - // Auto select icon set based on background color lightness: - const qreal backgroundLightness = QApplication::style()->standardPalette().window().color().lightnessF(); - Theme::setIconSet(backgroundLightness < 0.5 ? Theme::IconSet::Dark : Theme::IconSet::Light); + // Detect light/dark: +#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) + const QPalette defaultPalette; + const auto text = defaultPalette.color(QPalette::WindowText); + const auto window = defaultPalette.color(QPalette::Window); + Theme::setDark(text.lightness() > window.lightness()); +#else + Theme::setDark(QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark); +#endif MainWindow mw; if(options.fullscreen) diff --git a/client/src/mainwindow.cpp b/client/src/mainwindow.cpp index eaba884b..b5449590 100644 --- a/client/src/mainwindow.cpp +++ b/client/src/mainwindow.cpp @@ -122,6 +122,7 @@ MainWindow::MainWindow(QWidget* parent) : QAction* boardsAction; QAction* trainsAction; + m_mdiArea->setBackground(palette().window().color().darker()); m_mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); @@ -171,7 +172,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]() @@ -510,7 +510,7 @@ MainWindow::MainWindow(QWidget* parent) : })->setShortcut(QKeySequence::HelpContents); auto* subMenu = menu->addMenu(Locale::tr("qtapp.mainmenu:wizards")); subMenu->addAction(Locale::tr("wizard.introduction:title"), this, &MainWindow::showIntroductionWizard); - subMenu->addAction(Locale::tr("wizard.add_interface.welcome:title"), this, &MainWindow::showAddInterfaceWizard); + m_actionAddInterfaceWizard = subMenu->addAction(Locale::tr("wizard.add_interface.welcome:title"), this, &MainWindow::showAddInterfaceWizard); //menu->addSeparator(); //menu->addAction(Locale::tr("qtapp.mainmenu:about_qt") + "...", qApp, &QApplication::aboutQt); menu->addAction(Locale::tr("qtapp.mainmenu:about") + "...", this, &MainWindow::showAbout); @@ -667,7 +667,8 @@ void MainWindow::changeEvent(QEvent* event) void MainWindow::worldChanged() { - m_newWorldWizard.reset(); + m_wizard.newWorld.reset(); + m_wizard.addInterface.reset(); if(m_world) m_mdiArea->closeAllSubWindows(); @@ -738,13 +739,13 @@ void MainWindow::worldChanged() if(m_newWorldRequested && m_world) { m_newWorldRequested = false; - m_newWorldWizard = std::make_unique(m_world, this); - connect(m_newWorldWizard.get(), &NewWorldWizard::finished, + m_wizard.newWorld = std::make_unique(m_world, this); + connect(m_wizard.newWorld.get(), &NewWorldWizard::finished, [this]() { - m_newWorldWizard.release()->deleteLater(); + m_wizard.newWorld.release()->deleteLater(); }); - m_newWorldWizard->open(); + m_wizard.newWorld->open(); } } @@ -761,13 +762,21 @@ void MainWindow::loadWorld() if(!m_connection) return; - std::unique_ptr d = std::make_unique(m_connection, this); - if(d->exec() == QDialog::Accepted) - { - Method* method = m_connection->traintastic()->getMethod("load_world"); - if(Q_LIKELY(method)) - method->call(d->uuid()); - } + m_loadWorldDialog = std::make_unique(m_connection, this); + connect(m_loadWorldDialog.get(), &WorldListDialog::finished, + [this](int result) + { + if(result == QDialog::Accepted) + { + if(Method* method = m_connection->traintastic()->getMethod("load_world")) /*[[likely]]*/ + { + method->call(m_loadWorldDialog->uuid()); + } + } + m_loadWorldDialog.release()->deleteLater(); + }); + m_loadWorldDialog->setModal(true); + m_loadWorldDialog->show(); } void MainWindow::toggleFullScreen() @@ -929,17 +938,18 @@ IntroductionWizard* MainWindow::showIntroductionWizard() return introductionWizard; } -AddInterfaceWizard* MainWindow::showAddInterfaceWizard() +void MainWindow::showAddInterfaceWizard() { - if(!m_world) /*[[unlikely]]*/ + if(m_world && !m_wizard.addInterface) /*[[likely]]*/ { - return nullptr; + m_wizard.addInterface = std::make_unique(m_world, this); + connect(m_wizard.addInterface.get(), &AddInterfaceWizard::finished, + [this]() + { + m_wizard.addInterface.release()->deleteLater(); + }); + m_wizard.addInterface->open(); } - - auto* addInterfaceWizard = new AddInterfaceWizard(m_world, this); - addInterfaceWizard->setAttribute(Qt::WA_DeleteOnClose); - addInterfaceWizard->open(); - return addInterfaceWizard; } NewBoardWizard* MainWindow::showNewBoardWizard(const ObjectPtr& board) @@ -970,6 +980,10 @@ void MainWindow::connectionStateChanged() if(m_connection && m_connection->state() == Connection::State::Disconnected) { m_connection.reset(); + if(m_loadWorldDialog) + { + m_loadWorldDialog->reject(); + } if(m_serverLog) { delete m_serverLog; @@ -989,7 +1003,6 @@ void MainWindow::updateActions() const bool connected = m_connection && m_connection->state() == Connection::State::Connected; const bool haveWorld = connected && m_connection->world(); - m_actionConnectToServer->setEnabled(!m_connection); m_actionConnectToServer->setVisible(!connected); m_actionDisconnectFromServer->setVisible(connected); @@ -1013,6 +1026,7 @@ void MainWindow::updateActions() m_actionServerShutdown->setEnabled(m && m->getAttributeBool(AttributeName::Enabled, false)); } m_menuProgramming->setEnabled(haveWorld); + m_actionAddInterfaceWizard->setEnabled(haveWorld); setMenuEnabled(m_menuWorld, haveWorld); m_worldOnlineOfflineToolButton->setEnabled(haveWorld); diff --git a/client/src/mainwindow.hpp b/client/src/mainwindow.hpp index 34a5a814..b3bda9d6 100644 --- a/client/src/mainwindow.hpp +++ b/client/src/mainwindow.hpp @@ -46,6 +46,7 @@ class NewWorldWizard; class IntroductionWizard; class AddInterfaceWizard; class NewBoardWizard; +class WorldListDialog; class MainWindow final : public QMainWindow { @@ -55,7 +56,12 @@ class MainWindow final : public QMainWindow std::shared_ptr m_connection; ObjectPtr m_world; bool m_newWorldRequested = false; - std::unique_ptr m_newWorldWizard; + std::unique_ptr m_loadWorldDialog; + struct + { + std::unique_ptr addInterface; + std::unique_ptr newWorld; + } m_wizard; int m_clockRequest; ObjectPtr m_clock; QSplitter* m_splitter; @@ -99,6 +105,7 @@ class MainWindow final : public QMainWindow QAction* m_actionServerShutdown; QAction* m_actionServerLog; QMenu* m_menuProgramming; + QAction* m_actionAddInterfaceWizard; // Main toolbar: QToolBar* m_toolbar; QToolButton* m_worldOnlineOfflineToolButton; @@ -137,7 +144,7 @@ class MainWindow final : public QMainWindow const ObjectPtr& worldClock() const { return m_clock; } IntroductionWizard* showIntroductionWizard(); - AddInterfaceWizard* showAddInterfaceWizard(); + void showAddInterfaceWizard(); NewBoardWizard* showNewBoardWizard(const ObjectPtr& board); void showLuaScriptsList(); 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/connection.cpp b/client/src/network/connection.cpp index b52bb87a..8ea5392a 100644 --- a/client/src/network/connection.cpp +++ b/client/src/network/connection.cpp @@ -21,7 +21,7 @@ */ #include "connection.hpp" -#include +#include #include #include #include @@ -130,23 +130,31 @@ inline static QStringList readObjectIdArray(const Message& message, const int le Connection::Connection() : QObject(), - m_socket{new QTcpSocket(this)}, + m_socket{new QWebSocket()}, m_state{State::Disconnected}, m_worldProperty{nullptr}, m_worldRequestId{invalidRequestId} , m_serverLogTableModel{nullptr} { - connect(m_socket, &QTcpSocket::connected, this, &Connection::socketConnected); - connect(m_socket, &QTcpSocket::disconnected, this, &Connection::socketDisconnected); -#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) - connect(m_socket, static_cast(&QTcpSocket::error), this, &Connection::socketError); + connect(m_socket, &QWebSocket::connected, this, &Connection::socketConnected); + connect(m_socket, &QWebSocket::disconnected, this, &Connection::socketDisconnected); +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + connect(m_socket, &QWebSocket::errorOccurred, this, &Connection::socketError); #else - connect(m_socket, &QTcpSocket::errorOccurred, this, &Connection::socketError); + connect(m_socket, static_cast(&QWebSocket::error), this, &Connection::socketError); #endif - m_socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); - - connect(m_socket, &QTcpSocket::readyRead, this, &Connection::socketReadyRead); + connect(m_socket, &QWebSocket::binaryMessageReceived, + [this](const QByteArray& data) + { + const Message::Header& header = *reinterpret_cast(data.data()); + auto message = std::make_shared(header); + if(header.dataSize != 0) + { + std::memcpy(message->data(), data.data() + sizeof(header), message->dataSize()); + } + processMessage(message); + }); } bool Connection::isDisconnected() const @@ -172,12 +180,12 @@ void Connection::connectToHost(const QUrl& url, const QString& username, const Q else m_password = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256); setState(State::Connecting); - m_socket->connectToHost(url.host(), static_cast(url.port(defaultPort))); + m_socket->open(url); } void Connection::disconnectFromHost() { - m_socket->disconnectFromHost(); + m_socket->close(); } void Connection::cancelRequest(int requestId) @@ -443,7 +451,8 @@ int Connection::getTileData(Board& object) void Connection::send(std::unique_ptr& message) { Q_ASSERT(!message->isRequest()); - m_socket->write(static_cast(**message), message->size()); + QByteArray bytes(static_cast(**message), message->size()); // Deep copy :( + m_socket->sendBinaryMessage(bytes); // sendBinaryMessage only supports QByteArray } void Connection::send(std::unique_ptr& message, std::function&)> callback) @@ -451,7 +460,8 @@ void Connection::send(std::unique_ptr& message, std::functionisRequest()); Q_ASSERT(!m_requestCallback.contains(message->requestId())); m_requestCallback[message->requestId()] = callback; - m_socket->write(static_cast(**message), message->size()); + QByteArray bytes(static_cast(**message), message->size()); // Deep copy :( + m_socket->sendBinaryMessage(bytes); // sendBinaryMessage only supports QByteArray } ObjectPtr Connection::readObject(const Message& message) @@ -1050,14 +1060,14 @@ void Connection::socketConnected() else { setState(State::ErrorNewSessionFailed); - m_socket->disconnectFromHost(); + m_socket->close(); } }); } else { setState(State::ErrorAuthenticationFailed); - m_socket->disconnectFromHost(); + m_socket->close(); } }); } @@ -1071,32 +1081,3 @@ void Connection::socketError(QAbstractSocket::SocketError) { setState(State::SocketError); } - -void Connection::socketReadyRead() -{ - while(m_socket->bytesAvailable() != 0) - { - if(!m_readBuffer.message) // read header - { - m_readBuffer.offset += m_socket->read(reinterpret_cast(&m_readBuffer.header) + m_readBuffer.offset, sizeof(m_readBuffer.header) - m_readBuffer.offset); - if(m_readBuffer.offset == sizeof(m_readBuffer.header)) - { - if(m_readBuffer.header.dataSize != 0) - m_readBuffer.message = std::make_shared(m_readBuffer.header); - else - processMessage(std::make_shared(m_readBuffer.header)); - m_readBuffer.offset = 0; - } - } - else // read data - { - m_readBuffer.offset += m_socket->read(reinterpret_cast(m_readBuffer.message->data()) + m_readBuffer.offset, m_readBuffer.message->dataSize() - m_readBuffer.offset); - if(m_readBuffer.offset == m_readBuffer.message->dataSize()) - { - processMessage(m_readBuffer.message); - m_readBuffer.message.reset(); - m_readBuffer.offset = 0; - } - } - } -} diff --git a/client/src/network/connection.hpp b/client/src/network/connection.hpp index 58501a95..be417df8 100644 --- a/client/src/network/connection.hpp +++ b/client/src/network/connection.hpp @@ -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 @@ -35,7 +35,7 @@ #include "objectptr.hpp" #include "tablemodelptr.hpp" -class QTcpSocket; +class QWebSocket; class ServerLogTableModel; class Property; class ObjectProperty; @@ -73,7 +73,7 @@ class Connection : public QObject, public std::enable_shared_from_this 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/theme/theme.cpp b/client/src/theme/theme.cpp index 962c619e..03ccf9ae 100644 --- a/client/src/theme/theme.cpp +++ b/client/src/theme/theme.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 @@ -23,6 +23,7 @@ #include "theme.hpp" #include #include +#include const QString iconPathDefault = QStringLiteral(":/"); const QString iconPathDark = QStringLiteral(":/dark/"); @@ -31,8 +32,20 @@ const QString iconExtension = QStringLiteral(".svg"); const std::array iconPathsDark = {&iconPathDark, &iconPathDefault, &iconPathLight}; const std::array iconPathsLight = {&iconPathLight, &iconPathDefault, &iconPathDark}; +bool Theme::s_isDark = false; Theme::IconSet Theme::s_iconSet = Theme::IconSet::Light; +bool Theme::isDark() +{ + return s_isDark; +} + +void Theme::setDark(bool value) +{ + s_isDark = value; + setIconSet(value ? Theme::IconSet::Dark : Theme::IconSet::Light); +} + void Theme::setIconSet(IconSet value) { s_iconSet = value; diff --git a/client/src/theme/theme.hpp b/client/src/theme/theme.hpp index 33753fb7..ac432064 100644 --- a/client/src/theme/theme.hpp +++ b/client/src/theme/theme.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2021 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,11 +39,15 @@ class Theme private: Theme() = delete; + static bool s_isDark; static IconSet s_iconSet; static const std::array& getIconPaths(); public: + static bool isDark(); + static void setDark(bool value); + static void setIconSet(IconSet value); static QString getIconFile(const QString& id); diff --git a/client/src/utils/enum.cpp b/client/src/utils/enum.cpp index 1c4d477a..afde06ba 100644 --- a/client/src/utils/enum.cpp +++ b/client/src/utils/enum.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -105,6 +106,7 @@ QString translateEnum(const QString& enumName, qint64 value) TRANSLATE_ENUM(DecouplerState) TRANSLATE_ENUM(Direction) TRANSLATE_ENUM(DirectionControlState) + TRANSLATE_ENUM(ExternalOutputChangeAction) TRANSLATE_ENUM(LengthUnit) TRANSLATE_ENUM(LocoNetF9F28) TRANSLATE_ENUM(LocoNetFastClock) diff --git a/client/src/widget/createwidget.cpp b/client/src/widget/createwidget.cpp index 54d3874f..f6877fe4 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" @@ -34,6 +35,7 @@ #include "propertydoublespinbox.hpp" #include "propertyspinbox.hpp" #include "propertylineedit.hpp" +#include "propertypairoutputaction.hpp" #include "../board/boardwidget.hpp" #include "../network/object.hpp" #include "../network/inputmonitor.hpp" @@ -47,7 +49,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 +63,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") @@ -117,6 +123,10 @@ QWidget* createWidget(Property& property, QWidget* parent) break; // TODO case ValueType::Enum: + if(property.enumName() == "pair_output_action") + { + return new PropertyPairOutputAction(property, parent); + } return new PropertyComboBox(property, parent); case ValueType::Integer: 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/methodicon.cpp b/client/src/widget/methodicon.cpp new file mode 100644 index 00000000..8401028b --- /dev/null +++ b/client/src/widget/methodicon.cpp @@ -0,0 +1,82 @@ +/** + * client/src/widget/methodicon.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 "methodicon.hpp" +#include +#include +#include "../network/method.hpp" + +MethodIcon::MethodIcon(Method& method, QIcon icon, QWidget* parent) : + QLabel(parent), + m_method{method} +{ + setPixmap(icon.pixmap(32, 32)); + setCursor(Qt::PointingHandCursor); + setEnabled(m_method.getAttributeBool(AttributeName::Enabled, true)); + setVisible(m_method.getAttributeBool(AttributeName::Visible, true)); + setToolTip(m_method.displayName()); + connect(&m_method, &Method::attributeChanged, this, + [this](AttributeName name, const QVariant& value) + { + switch(name) + { + case AttributeName::Enabled: + setEnabled(value.toBool()); + break; + + case AttributeName::Visible: + setVisible(value.toBool()); + break; + + case AttributeName::DisplayName: + setToolTip(m_method.displayName()); + break; + + default: + break; + } + }); +} + +void MethodIcon::mousePressEvent(QMouseEvent* event) +{ + if(event->button() == Qt::LeftButton) + { + m_mouseLeftButtonPressed = true; + } +} + +void MethodIcon::mouseReleaseEvent(QMouseEvent* event) +{ + if(m_mouseLeftButtonPressed && event->button() == Qt::LeftButton) + { + m_mouseLeftButtonPressed = false; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + if(rect().contains(event->localPos().toPoint())) // test if mouse release in widget +#else + if(rect().contains(event->position().toPoint())) // test if mouse release in widget +#endif + { + m_method.call(); + } + } +} diff --git a/client/src/widget/methodicon.hpp b/client/src/widget/methodicon.hpp new file mode 100644 index 00000000..92a9a742 --- /dev/null +++ b/client/src/widget/methodicon.hpp @@ -0,0 +1,43 @@ +/** + * client/src/widget/methodicon.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_METHODICON_HPP +#define TRAINTASTIC_CLIENT_WIDGET_METHODICON_HPP + +#include + +class Method; + +class MethodIcon : public QLabel +{ + protected: + Method& m_method; + bool m_mouseLeftButtonPressed; + + void mousePressEvent(QMouseEvent* event) final; + void mouseReleaseEvent(QMouseEvent* event) final; + + public: + MethodIcon(Method& item, QIcon icon, QWidget* parent = nullptr); +}; + +#endif 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/object/objecteditwidget.cpp b/client/src/widget/object/objecteditwidget.cpp index 3ee6f951..e6ee36cd 100644 --- a/client/src/widget/object/objecteditwidget.cpp +++ b/client/src/widget/object/objecteditwidget.cpp @@ -40,6 +40,7 @@ #include "../propertydirectioncontrol.hpp" #include "../propertyvaluelabel.hpp" #include "../methodpushbutton.hpp" +#include "../unitpropertycombobox.hpp" #include "../unitpropertyedit.hpp" #include "../createwidget.hpp" #include "../../theme/theme.hpp" @@ -107,7 +108,16 @@ void ObjectEditWidget::buildForm() { Property* property = static_cast(baseProperty); if(UnitProperty* unitProperty = dynamic_cast(property)) - w = new UnitPropertyEdit(*unitProperty, this); + { + if(unitProperty->hasAttribute(AttributeName::Values)) + { + w = new UnitPropertyComboBox(*unitProperty, this); + } + else + { + w = new UnitPropertyEdit(*unitProperty, this); + } + } else if(!property->isWritable()) w = new PropertyValueLabel(*property, this); else if(property->type() == ValueType::Boolean) diff --git a/client/src/widget/objectlist/objectlistwidget.cpp b/client/src/widget/objectlist/objectlistwidget.cpp index 54f5b64b..edb9132d 100644 --- a/client/src/widget/objectlist/objectlistwidget.cpp +++ b/client/src/widget/objectlist/objectlistwidget.cpp @@ -146,10 +146,11 @@ ObjectListWidget::ObjectListWidget(const ObjectPtr& object_, QWidget* parent) : method->argumentTypes().size() == 1 && method->argumentTypes()[0] == ValueType::Object && method->resultType() == ValueType::Invalid) { + const bool multiSelect = object_->classId() == "list.train_vehicle"; m_actionAdd = m_toolbar->addAction(Theme::getIcon("add"), method->displayName(), - [this, method]() + [this, method, multiSelect]() { - std::make_unique(*method, this)->exec(); + std::make_unique(*method, multiSelect, this)->exec(); }); m_actionAdd->setEnabled(method->getAttributeBool(AttributeName::Enabled, true)); connect(method, &Method::attributeChanged, this, @@ -398,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/turnoutposition.hpp b/client/src/widget/objectlist/trainlistwidget.hpp similarity index 66% rename from server/src/enum/turnoutposition.hpp rename to client/src/widget/objectlist/trainlistwidget.hpp index 72e2b2da..21ee9b01 100644 --- a/server/src/enum/turnoutposition.hpp +++ b/client/src/widget/objectlist/trainlistwidget.hpp @@ -1,9 +1,9 @@ /** - * server/src/enum/turnoutposition.hpp + * client/src/widget/objectlist/trainlistwidget.hpp * * This file is part of the traintastic source code. * - * Copyright (C) 2020-2021 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_TURNOUTPOSITION_HPP -#define TRAINTASTIC_SERVER_ENUM_TURNOUTPOSITION_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 4e607040..2e715512 100644 --- a/client/src/widget/outputmapwidget.cpp +++ b/client/src/widget/outputmapwidget.cpp @@ -27,14 +27,20 @@ #include #include #include +#include #include +#include "createwidget.hpp" #include "interfaceitemnamelabel.hpp" #include "propertycheckbox.hpp" #include "propertycombobox.hpp" +#include "propertypairoutputaction.hpp" #include "propertyspinbox.hpp" #include "objectpropertycombobox.hpp" #include "propertyaddresses.hpp" #include "outputmapoutputactionwidget.hpp" +#include "methodicon.hpp" +#include "../board/tilepainter.hpp" +#include "../board/getboardcolorscheme.hpp" #include "../dialog/objectselectlistdialog.hpp" #include "../network/callmethod.hpp" #include "../network/method.hpp" @@ -46,9 +52,12 @@ #include "../theme/theme.hpp" #include "../misc/methodaction.hpp" -constexpr int columnCountNonOutput = 2; -constexpr int columnUse = 0; -constexpr int columnKey = 1; +constexpr int columnKey = 0; + +static bool hasUseColumn(const QString& classId) +{ + return classId == "output_map.signal"; +} static void setComboBoxMinimumWidth(QComboBox* comboBox) { @@ -58,10 +67,14 @@ static void setComboBoxMinimumWidth(QComboBox* comboBox) OutputMapWidget::OutputMapWidget(ObjectPtr object, QWidget* parent) : QWidget(parent) , m_object{std::move(object)} + , m_hasUseColumn{hasUseColumn(m_object->classId())} + , m_columnCountNonOutput{m_hasUseColumn ? 2 : 1} , m_addresses{m_object->getVectorProperty("addresses")} , m_ecosObject{dynamic_cast(m_object->getProperty("ecos_object"))} , m_items{m_object->getObjectVectorProperty("items")} , m_table{new QTableWidget(this)} + , m_getParentRequestId{Connection::invalidRequestId} + , m_getItemsRequestId{Connection::invalidRequestId} { QVBoxLayout* l = new QVBoxLayout(); @@ -92,16 +105,49 @@ OutputMapWidget::OutputMapWidget(ObjectPtr object, QWidget* parent) } l->addLayout(form); - m_table->setColumnCount(columnCountNonOutput); + const int listViewIconSize = m_table->style()->pixelMetric(QStyle::PM_ListViewIconSize); + m_table->setIconSize({listViewIconSize, listViewIconSize}); + m_table->setColumnCount(m_columnCountNonOutput); m_table->setRowCount(0); - m_table->setHorizontalHeaderLabels({Locale::tr("output_map:use"), Locale::tr(m_object->classId() + ":key")}); + QStringList labels; + labels.append(Locale::tr(m_object->classId() + ":key")); + if(m_hasUseColumn) + { + labels.append(Locale::tr("output_map:use")); + } + m_table->setHorizontalHeaderLabels(labels); m_table->verticalHeader()->setVisible(false); m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); l->addWidget(m_table); + if(auto* swapOutputs = m_object->getMethod("swap_outputs")) + { + m_swapOutputs = new MethodIcon(*swapOutputs, Theme::getIcon("swap"), m_table); + if(!swapOutputs->getAttributeBool(AttributeName::Visible, true)) + { + m_swapOutputs->hide(); + } + m_table->installEventFilter(this); + m_swapOutputs->installEventFilter(this); + } + setLayout(l); + if(auto* parentObject = m_object->getObjectProperty("parent")) + { + m_getParentRequestId = parentObject->getObject( + [this](const ObjectPtr& obj, std::optional ec) + { + if(obj && !ec) + { + m_getParentRequestId = Connection::invalidRequestId; + m_parentObject = obj; + updateKeyIcons(); + } + }); + } + if(m_items) /*[[likely]]*/ { m_getItemsRequestId = m_items->getObjects( @@ -112,26 +158,30 @@ OutputMapWidget::OutputMapWidget(ObjectPtr object, QWidget* parent) updateItems(objects); } }); + + connect(&BoardSettings::instance(), &BoardSettings::changed, this, &OutputMapWidget::updateKeyIcons); + } +} + +OutputMapWidget::~OutputMapWidget() +{ + if(m_getParentRequestId != Connection::invalidRequestId) + { + m_object->connection()->cancelRequest(m_getParentRequestId); + } + if(m_getItemsRequestId != Connection::invalidRequestId) + { + m_object->connection()->cancelRequest(m_getItemsRequestId); } } 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++) { - if(auto* p = dynamic_cast(items[i]->getProperty("use"))) - { - QWidget* w = new QWidget(m_table); - QHBoxLayout* l = new QHBoxLayout(); - l->setAlignment(Qt::AlignCenter); - l->addWidget(new PropertyCheckBox(*p, w)); - w->setLayout(l); - m_table->setCellWidget(i, columnUse, w); - } - if(auto* p = items[i]->getProperty("key")) { QString text; @@ -162,34 +212,116 @@ 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) + { + const int columnUse = columnKey + 1; + + if(auto* p = dynamic_cast(items[i]->getProperty("use"))) + { + QWidget* w = new QWidget(m_table); + QHBoxLayout* l = new QHBoxLayout(); + l->setAlignment(Qt::AlignCenter); + l->addWidget(new PropertyCheckBox(*p, w)); + w->setLayout(l); + m_table->setCellWidget(static_cast(i), columnUse, w); + } + } + + if(auto* p = items[i]->getProperty("visible")) + { + m_table->setRowHidden(static_cast(i), !p->toBool()); + + connect(p, &Property::valueChangedBool, this, + [this, row=static_cast(i)](bool value) + { + m_table->setRowHidden(row, !value); + }); } 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); }); } } + updateKeyIcons(); updateTableOutputColumns(); } +void OutputMapWidget::updateKeyIcons() +{ + if(!m_parentObject) + { + return; + } + + if(auto tileIdProperty = m_parentObject->getProperty("tile_id")) + { + const bool darkBackground = m_table->palette().window().color().lightnessF() < 0.5; + const auto tileId = tileIdProperty->toEnum(); + + const int iconSize = m_table->iconSize().height(); + QImage image(iconSize, iconSize, QImage::Format_ARGB32); + QPainter painter{&image}; + painter.setRenderHint(QPainter::Antialiasing, true); + TilePainter tilePainter{painter, iconSize, *getBoardColorScheme(darkBackground ? BoardSettings::ColorScheme::Dark : BoardSettings::ColorScheme::Light)}; + + for(size_t i = 0; i < m_itemObjects.size(); i++) + { + if(auto* key = m_itemObjects[i]->getProperty("key")) + { + image.fill(Qt::transparent); + + if(isRailTurnout(tileId)) + { + tilePainter.drawTurnout(tileId, image.rect(), TileRotate::Deg0, TurnoutPosition::Unknown, static_cast(key->toInt())); + } + else if(isRailSignal(tileId)) + { + tilePainter.drawSignal(tileId, image.rect(), TileRotate::Deg0, false, static_cast(key->toInt())); + } + else if(tileId == TileId::RailDirectionControl) + { + tilePainter.drawDirectionControl(tileId, image.rect(), TileRotate::Deg0, false, static_cast(key->toInt())); + } + else if(tileId == TileId::RailDecoupler) + { + tilePainter.drawRailDecoupler(image.rect(), TileRotate::Deg90, false, static_cast(key->toInt())); + } + else if(tileId == TileId::Switch) + { + tilePainter.drawSwitch(image.rect(), key->toBool()); + } + else + { + break; // tileId not supported (yet) + } + + m_table->item(static_cast(i), columnKey)->setIcon(QPixmap::fromImage(image)); + } + } + } +} + void OutputMapWidget::updateTableOutputColumns() { if(m_addresses && m_addresses->getAttributeBool(AttributeName::Visible, true)) { const auto size = m_addresses->size(); - m_table->setColumnCount(columnCountNonOutput + size); + m_table->setColumnCount(m_columnCountNonOutput + size); for(int i = 0; i < size; i++) { - const int column = columnCountNonOutput + i; + const int column = m_columnCountNonOutput + i; const int address = m_addresses->getInt(i); auto* item = new QTableWidgetItem(QString("#%1").arg(address)); item->setToolTip(Locale::tr("output_map:address_x").arg(address)); @@ -198,15 +330,26 @@ void OutputMapWidget::updateTableOutputColumns() } else if(m_ecosObject && m_ecosObject->getAttributeBool(AttributeName::Visible, true)) { - m_table->setColumnCount(columnCountNonOutput + 1); - m_table->setHorizontalHeaderItem(columnCountNonOutput, new QTableWidgetItem(Locale::tr("output.ecos_object:state"))); + m_table->setColumnCount(m_columnCountNonOutput + 1); + m_table->setHorizontalHeaderItem(m_columnCountNonOutput, new QTableWidgetItem(Locale::tr("output.ecos_object:state"))); } else { - m_table->setColumnCount(columnCountNonOutput); + m_table->setColumnCount(m_columnCountNonOutput); } } +bool OutputMapWidget::eventFilter(QObject* object, QEvent* event) +{ + if(m_swapOutputs && ((object == m_table && event->type() == QEvent::Resize) || (object == m_swapOutputs && event->type() == QEvent::Show))) + { + auto pnt = m_swapOutputs->rect().bottomRight(); + pnt = m_table->rect().bottomRight() - pnt - pnt / 4; + m_swapOutputs->move(pnt.x(), pnt.y()); + } + return QWidget::eventFilter(object, event); +} + void OutputMapWidget::updateTableOutputActions(ObjectVectorProperty& property, int row) { if(!property.empty()) @@ -214,21 +357,21 @@ void OutputMapWidget::updateTableOutputActions(ObjectVectorProperty& property, i m_dummy = property.getObjects( [this, row](const std::vector& objects, std::optional /*ec*/) { - const int columnCount = static_cast(columnCountNonOutput + objects.size()); + const int columnCount = static_cast(m_columnCountNonOutput + objects.size()); if(columnCount > m_table->columnCount()) { m_table->setColumnCount(columnCount); } auto& rowActions = m_actions[row]; - int column = columnCountNonOutput; + int column = m_columnCountNonOutput; for(auto& object : objects) { if(column >= static_cast(rowActions.size()) || object.get() != rowActions[column].get()) { if(auto* action = dynamic_cast(object->getProperty("action"))) { - m_table->setCellWidget(row, column, new PropertyComboBox(*action, this)); + m_table->setCellWidget(row, column, createWidget(*action, this)); } else if(auto* aspect = dynamic_cast(object->getProperty("aspect"))) { diff --git a/client/src/widget/outputmapwidget.hpp b/client/src/widget/outputmapwidget.hpp index 7d31e5f2..563100e2 100644 --- a/client/src/widget/outputmapwidget.hpp +++ b/client/src/widget/outputmapwidget.hpp @@ -29,6 +29,7 @@ class QTableWidget; class Method; class MethodAction; +class MethodIcon; class AbstractProperty; class AbstractVectorProperty; class ObjectVectorProperty; @@ -40,21 +41,30 @@ class OutputMapWidget : public QWidget protected: ObjectPtr m_object; + ObjectPtr m_parentObject; + const bool m_hasUseColumn; + const int m_columnCountNonOutput; AbstractVectorProperty* m_addresses; Property* m_ecosObject; ObjectVectorProperty* m_items; QTableWidget* m_table; + MethodIcon* m_swapOutputs = nullptr; std::vector m_itemObjects; std::vector> m_actions; + int m_getParentRequestId; int m_getItemsRequestId; int m_dummy; void updateTableOutputActions(ObjectVectorProperty& property, int row); void updateItems(const std::vector& items); + void updateKeyIcons(); void updateTableOutputColumns(); + bool eventFilter(QObject* object, QEvent* event) override; + public: explicit OutputMapWidget(ObjectPtr object, QWidget* parent = nullptr); + ~OutputMapWidget() override; }; #endif 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/propertypairoutputaction.cpp b/client/src/widget/propertypairoutputaction.cpp new file mode 100644 index 00000000..376c1ed1 --- /dev/null +++ b/client/src/widget/propertypairoutputaction.cpp @@ -0,0 +1,168 @@ +/** + * client/src/widget/propertypairoutputaction.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 "propertypairoutputaction.hpp" +#include +#include +#include "../network/property.hpp" + +PropertyPairOutputAction::PropertyPairOutputAction(Property& property, QWidget* parent) + : QWidget(parent) + , m_property{property} +{ + assert(m_property.enumName() == "pair_output_action"); + + setFocusPolicy(Qt::StrongFocus); + + connect(&m_property, &Property::valueChanged, this, + [this]() + { + update(rect()); + }); +} + +PairOutputAction PropertyPairOutputAction::value() const +{ + return m_property.toEnum(); +} + +void PropertyPairOutputAction::setValue(PairOutputAction newValue) +{ + m_property.setValueEnum(newValue); +} + +void PropertyPairOutputAction::toggleValue() +{ + switch(value()) + { + case PairOutputAction::None: + setValue(PairOutputAction::First); + break; + + case PairOutputAction::First: + setValue(PairOutputAction::Second); + break; + + case PairOutputAction::Second: + default: + setValue(PairOutputAction::None); + break; + } +} + +void PropertyPairOutputAction::toggleValue(PairOutputAction action) +{ + setValue(action == value() ? PairOutputAction::None : action); +} + +void PropertyPairOutputAction::keyPressEvent(QKeyEvent* event) +{ + switch(event->key()) + { + case Qt::Key_Enter: + case Qt::Key_Space: + toggleValue(); + return; + + case Qt::Key_1: + case Qt::Key_R: + toggleValue(PairOutputAction::First); + return; + + case Qt::Key_2: + case Qt::Key_G: + toggleValue(PairOutputAction::Second); + return; + } + QWidget::keyPressEvent(event); +} + +void PropertyPairOutputAction::mousePressEvent(QMouseEvent* event) +{ + if(event->button() == Qt::LeftButton) + { + m_mouseLeftClickPos = event->pos(); + } +} + +void PropertyPairOutputAction::mouseReleaseEvent(QMouseEvent* event) +{ + if(event->button() == Qt::LeftButton && m_mouseLeftClickPos) + { + const auto [first, second] = outputRects(); + + if(first.contains(*m_mouseLeftClickPos) && first.contains(event->pos())) + { + toggleValue(PairOutputAction::First); + } + else if(second.contains(*m_mouseLeftClickPos) && second.contains(event->pos())) + { + toggleValue(PairOutputAction::Second); + } + + m_mouseLeftClickPos.reset(); + } +} + +void PropertyPairOutputAction::paintEvent(QPaintEvent* /*event*/) +{ + constexpr int thinkness = 2; + const QColor firstOnColor(Qt::red); + const QColor secondOnColor(Qt::green); + const auto textOnColor = palette().color(QPalette::Active, QPalette::WindowText); + const auto offColor = palette().color(QPalette::Disabled, QPalette::WindowText); + const auto [left, right] = outputRects(); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + + painter.save(); + + QPen p(painter.pen()); + p.setWidth(thinkness); + + p.setColor(value() == PairOutputAction::First ? firstOnColor : offColor); + painter.setPen(p); + painter.drawEllipse(left.adjusted(thinkness, thinkness, -thinkness, -thinkness)); + + p.setColor(value() == PairOutputAction::Second ? secondOnColor : offColor); + painter.setPen(p); + painter.drawEllipse(right.adjusted(thinkness, thinkness, -thinkness, -thinkness)); + + painter.restore(); + + painter.setPen(value() == PairOutputAction::First ? textOnColor : offColor); + painter.drawText(left, Qt::AlignCenter, "R"); + + painter.setPen(value() == PairOutputAction::Second ? textOnColor : offColor); + painter.drawText(right, Qt::AlignCenter, "G"); +} + +std::pair PropertyPairOutputAction::outputRects() const +{ + constexpr int margin = 1; + const int height = rect().height(); + const int hCenter = rect().width() / 2; + return { + {hCenter - margin - height, 0, height, height}, + {hCenter + margin, 0, height, height}}; +} diff --git a/client/src/widget/propertypairoutputaction.hpp b/client/src/widget/propertypairoutputaction.hpp new file mode 100644 index 00000000..6e00d983 --- /dev/null +++ b/client/src/widget/propertypairoutputaction.hpp @@ -0,0 +1,55 @@ +/** + * client/src/widget/propertypairoutputaction.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_PROPERTYPAIROUTPUTACTION_HPP +#define TRAINTASTIC_CLIENT_WIDGET_PROPERTYPAIROUTPUTACTION_HPP + +#include +#include +#include + +class Property; + +class PropertyPairOutputAction : public QWidget +{ +private: + Property& m_property; + std::optional m_mouseLeftClickPos = std::nullopt; + + std::pair outputRects() const; + +protected: + void keyPressEvent(QKeyEvent* event) final; + void mousePressEvent(QMouseEvent* event) final; + void mouseReleaseEvent(QMouseEvent* event) final; + void paintEvent(QPaintEvent* event) final; + +public: + PropertyPairOutputAction(Property& property, QWidget* parent = nullptr); + + PairOutputAction value() const; + void setValue(PairOutputAction action); + void toggleValue(); + void toggleValue(PairOutputAction output); +}; + +#endif diff --git a/client/src/widget/propertyspinbox.cpp b/client/src/widget/propertyspinbox.cpp index 6b2d8c0f..b8989f55 100644 --- a/client/src/widget/propertyspinbox.cpp +++ b/client/src/widget/propertyspinbox.cpp @@ -35,7 +35,11 @@ PropertySpinBox::PropertySpinBox(Property& property, QWidget* parent) : Q_ASSERT(m_property.type() == ValueType::Integer); setEnabled(m_property.getAttributeBool(AttributeName::Enabled, true)); setVisible(m_property.getAttributeBool(AttributeName::Visible, true)); - setRange(std::numeric_limits::min(), std::numeric_limits::max()); + updateRange(); + if(auto unit = m_property.getAttributeString(AttributeName::Unit, ""); !unit.isEmpty()) + { + setSuffix(unit.prepend(" ")); + } setValue(m_property.toInt()); connect(&m_property, &AbstractProperty::valueChangedInt, this, [this](int value) @@ -58,6 +62,22 @@ PropertySpinBox::PropertySpinBox(Property& property, QWidget* parent) : setVisible(value.toBool()); break; + case AttributeName::Min: + case AttributeName::Max: + updateRange(); + break; + + case AttributeName::Unit: + if(auto unit = value.toString(); !unit.isEmpty()) + { + setSuffix(unit.prepend(" ")); + } + else + { + setSuffix(""); + } + break; + default: break; } @@ -105,3 +125,10 @@ void PropertySpinBox::focusOutEvent(QFocusEvent* event) QSpinBox::focusOutEvent(event); setValue(m_property.toInt()); } + +void PropertySpinBox::updateRange() +{ + setRange( + m_property.getAttributeInt(AttributeName::Min, std::numeric_limits::min()), + m_property.getAttributeInt(AttributeName::Max, std::numeric_limits::max())); +} diff --git a/client/src/widget/propertyspinbox.hpp b/client/src/widget/propertyspinbox.hpp index e888b367..88f4ba8c 100644 --- a/client/src/widget/propertyspinbox.hpp +++ b/client/src/widget/propertyspinbox.hpp @@ -36,6 +36,8 @@ class PropertySpinBox : public QSpinBox void cancelRequest(); void showError(const QString& error); + void updateRange(); + protected: void focusOutEvent(QFocusEvent* event) override; 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/client/src/widget/unitpropertycombobox.cpp b/client/src/widget/unitpropertycombobox.cpp new file mode 100644 index 00000000..b34cbed5 --- /dev/null +++ b/client/src/widget/unitpropertycombobox.cpp @@ -0,0 +1,193 @@ +/** + * client/src/widget/unitpropertycombobox.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 "unitpropertycombobox.hpp" +#include +#include +#include +#include "../network/unitproperty.hpp" +#include "../utils/internalupdateholder.hpp" +#include "../utils/enum.hpp" + +UnitPropertyComboBox::UnitPropertyComboBox(UnitProperty& property, QWidget* parent) + : QWidget(parent) + , m_property{property} + , m_valueComboBox{new QComboBox(this)} + , m_unitComboBox{new QComboBox(this)} +{ + setEnabled(m_property.getAttributeBool(AttributeName::Enabled, true)); + setVisible(m_property.getAttributeBool(AttributeName::Visible, true)); + + connect(&m_property, &UnitProperty::attributeChanged, this, + [this](AttributeName name, const QVariant& value) + { + switch(name) + { + case AttributeName::Enabled: + setEnabled(value.toBool()); + break; + + case AttributeName::Visible: + setVisible(value.toBool()); + break; + + case AttributeName::AliasKeys: + case AttributeName::AliasValues: + case AttributeName::Values: + updateValues(); + break; + + default: + break; + } + }); + + connect(&m_property, &Property::valueChangedDouble, this, + [this](double value) + { + InternalUpdateHolder hold(m_internalUpdate); + if(int index = m_valueComboBox->findData(value); index != -1) // predefined value + { + m_valueComboBox->setCurrentIndex(index); + } + else // custom value + { + m_valueComboBox->setCurrentText(QString::number(value)); + } + }); + + m_valueComboBox->setEditable(true); // FIXME + m_valueComboBox->setInsertPolicy(QComboBox::NoInsert); + + if(m_valueComboBox->isEditable()) + { + connect(m_valueComboBox, &QComboBox::currentTextChanged, + [this](const QString& value) + { + if(m_internalUpdate == 0) + { + const QVariant v = m_valueComboBox->currentData(); + if(v.isValid()) // predefined value + { + m_property.setValueDouble(v.value()); + } + else // custom value + { + m_property.setValueString(value); + } + } + }); + } + else + { + connect(m_valueComboBox, static_cast(&QComboBox::currentIndexChanged), + [this](int) + { + if(m_internalUpdate == 0) + { + const QVariant v = m_valueComboBox->currentData(); + m_property.setValueDouble(v.value()); + } + }); + } + + QHBoxLayout* l = new QHBoxLayout(); + l->setContentsMargins(0, 0, 0, 0); + l->addWidget(m_valueComboBox, 1); + + for(qint64 value : enumValues(m_property.unitName())) + { + m_unitComboBox->addItem(translateEnum(m_property.unitName(), value), value); + if(m_property.unitValue() == value) + m_unitComboBox->setCurrentIndex(m_unitComboBox->count() - 1); + } + l->addWidget(m_unitComboBox); + connect(m_unitComboBox, static_cast(&QComboBox::currentIndexChanged), this, + [this](int) + { + if(QVariant v = m_unitComboBox->currentData(); v.canConvert()) + if(qint64 value = v.toLongLong(); value != m_property.unitValue()) + m_property.setUnitValue(value); + }); + + setLayout(l); + updateValues(); +} + +void UnitPropertyComboBox::updateValues() +{ + QVariant values = m_property.getAttribute(AttributeName::Values, QVariant()); + if(Q_LIKELY(values.isValid())) + { + InternalUpdateHolder hold(m_internalUpdate); + + m_valueComboBox->clear(); + + if(Q_LIKELY(values.userType() == QMetaType::QVariantList)) + { + bool currentIndexSet = false; + + switch(m_property.type()) + { + case ValueType::Float: + { + const QVariantList aliasKeys = m_property.getAttribute(AttributeName::AliasKeys, QVariant()).toList(); + const QVariantList aliasValues = m_property.getAttribute(AttributeName::AliasValues, QVariant()).toList(); + + for(QVariant& v : values.toList()) + { + const auto value = v.toDouble(); + if(int index = aliasKeys.indexOf(v); index != -1) + { + m_valueComboBox->addItem(Locale::instance->parse(aliasValues[index].toString()), value); + } + else + { + m_valueComboBox->addItem(QString::number(value), value); + } + + if(qFuzzyCompare(m_property.toDouble(), value) || m_property.toDouble() == value) + { + m_valueComboBox->setCurrentIndex(m_valueComboBox->count() - 1); + currentIndexSet = true; + } + } + break; + } + case ValueType::Invalid: + case ValueType::Boolean: + case ValueType::Integer: + case ValueType::Enum: + case ValueType::String: + case ValueType::Object: + case ValueType::Set: + assert(false); + break; + } + + if(m_valueComboBox->isEditable() && !currentIndexSet) + { + m_valueComboBox->setCurrentText(m_property.toString()); + } + } + } +} diff --git a/client/src/widget/unitpropertycombobox.hpp b/client/src/widget/unitpropertycombobox.hpp new file mode 100644 index 00000000..08c96f23 --- /dev/null +++ b/client/src/widget/unitpropertycombobox.hpp @@ -0,0 +1,47 @@ +/** + * client/src/widget/unitpropertycombobox.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_UNITPROPERTYCOMBOBOX_HPP +#define TRAINTASTIC_CLIENT_WIDGET_UNITPROPERTYCOMBOBOX_HPP + +#include + +class UnitProperty; +class QComboBox; + +class UnitPropertyComboBox : public QWidget +{ + Q_OBJECT + + private: + UnitProperty& m_property; + size_t m_internalUpdate = 0; + QComboBox* m_valueComboBox; + QComboBox* m_unitComboBox; + + void updateValues(); + + public: + explicit UnitPropertyComboBox(UnitProperty& property, QWidget* parent = nullptr); +}; + +#endif diff --git a/client/src/wizard/jsonwizard.cpp b/client/src/wizard/jsonwizard.cpp index eb788955..a898e5cb 100644 --- a/client/src/wizard/jsonwizard.cpp +++ b/client/src/wizard/jsonwizard.cpp @@ -225,13 +225,17 @@ class RadioPageJSON : public RadioPage, public PageJSON void initializePage() override { - setTitleAndText(*static_cast(wizard()), this, m_pageData); + auto* jsonWizard = static_cast(wizard()); + + setTitleAndText(*jsonWizard, this, m_pageData); for(const auto& option : m_pageData["options"].toArray()) { auto item = option.toObject(); - addItem(static_cast(wizard())->translateAndReplaceVariables(item["name"].toString()), item["disabled"].toBool()); + addItem(jsonWizard->translateAndReplaceVariables(item["name"].toString()), item["checked"].toBool(), item["disabled"].toBool()); } + + setBottomText(jsonWizard->translateAndReplaceVariables(m_pageData["bottom_text"].toString())); } void cleanupPage() override @@ -469,4 +473,4 @@ Properties JSONWizard::toProperties(const QJsonObject& object) } } return properties; -} \ No newline at end of file +} diff --git a/client/src/wizard/page/radiopage.cpp b/client/src/wizard/page/radiopage.cpp index 3d364615..88e9d0c0 100644 --- a/client/src/wizard/page/radiopage.cpp +++ b/client/src/wizard/page/radiopage.cpp @@ -24,11 +24,19 @@ #include #include #include +#include RadioPage::RadioPage(QWidget* parent) : TextPage(parent) , m_group{new QButtonGroup(this)} + , m_bottomText{new QLabel(this)} { + m_bottomText->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + m_bottomText->setWordWrap(true); + setBottomText({}); + + static_cast(layout())->addStretch(); + layout()->addWidget(m_bottomText); } int RadioPage::currentIndex() const @@ -36,23 +44,25 @@ int RadioPage::currentIndex() const return m_group->id(m_group->checkedButton()); } -void RadioPage::addItem(const QString& label, bool disabled) +void RadioPage::addItem(const QString& label, bool checked, bool disabled) { auto* button = new QRadioButton(label); + button->setChecked(checked && !disabled); button->setDisabled(disabled); m_group->addButton(button, m_group->buttons().size()); - layout()->addWidget(button); + static_cast(layout())->insertWidget(layout()->count() - 2, button); } void RadioPage::clear() { - while(layout()->count() > 1) // remove all but first (=text label) + for(auto* button : m_group->buttons()) { - auto* item = layout()->itemAt(layout()->count() - 1); - if(item->widget()) - { - delete item->widget(); - } - layout()->removeItem(item); + delete button; } } + +void RadioPage::setBottomText(const QString& text) +{ + m_bottomText->setText(text); + m_bottomText->setVisible(!text.isEmpty()); +} diff --git a/client/src/wizard/page/radiopage.hpp b/client/src/wizard/page/radiopage.hpp index 13cc175d..eb92f7f4 100644 --- a/client/src/wizard/page/radiopage.hpp +++ b/client/src/wizard/page/radiopage.hpp @@ -31,14 +31,17 @@ class RadioPage : public TextPage { protected: QButtonGroup* m_group; + QLabel* m_bottomText; public: explicit RadioPage(QWidget* parent = nullptr); int currentIndex() const; - void addItem(const QString& label, bool disabled = false); + void addItem(const QString& label, bool checked = false, bool disabled = false); void clear(); + + void setBottomText(const QString& text); }; #endif diff --git a/manual/buildluadoc.py b/manual/buildluadoc.py index 309dab46..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) @@ -52,6 +54,7 @@ class LuaDoc: definition = self._terms[term] definition = re.sub(r'`(.+?)`', r'\1', definition) + definition = re.sub(r'\[([^\]]+)]\(([^\)]+)\)', r'\1', definition) definition = re.sub(r'{ref:([a-z0-9_\.]+?)(|#[a-z0-9_]+)(|\|.+?)}', self._ref_link, definition) return definition @@ -100,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) + '' @@ -347,7 +360,7 @@ class LuaDoc: hpp = LuaDoc._read_file(filename_hpp) cpp = LuaDoc._read_file(filename_cpp) if os.path.exists(filename_cpp) else hpp for cpp_type, cpp_template_type, cpp_item_name in re.findall(r'(Property|VectorProperty|ObjectProperty|ObjectVectorProperty|Method|Event)<(.*?)>\s+([A-Za-z0-9_]+);', hpp): - m = re.search(cpp_item_name + r'({|\()\s*[\*]?this\s*,\s*"([a-z0-9_]+)"[^}]*(PropertyFlags::ScriptReadOnly|PropertyFlags::ScriptReadWrite|MethodFlags::ScriptCallable|EventFlags::Scriptable)[^}]*}', cpp) + m = re.search(cpp_item_name + r'({|\()\s*[\*]?this\s*,\s*"([a-z0-9_]+)".*?(PropertyFlags::ScriptReadOnly|PropertyFlags::ScriptReadWrite|MethodFlags::ScriptCallable|EventFlags::Scriptable)[^}]*}', cpp) if m is None: continue @@ -446,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(): @@ -648,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}] @@ -708,7 +746,6 @@ class LuaDoc: category['title'] = self._get_term('object.category.' + key + ':title') category['items'] = [] for object in category['objects']: - print(object) for item in items: if item['id'] == object: category['items'].append(item) @@ -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/blockrailtile.json b/manual/luadoc/object/blockrailtile.json index c0413441..523fc76c 100644 --- a/manual/luadoc/object/blockrailtile.json +++ b/manual/luadoc/object/blockrailtile.json @@ -2,6 +2,7 @@ "name": { "since": "0.3" }, + "trains": {}, "on_train_assigned": { "parameters": [ { @@ -64,4 +65,4 @@ ], "since": "0.3" } -} \ No newline at end of file +} diff --git a/manual/luadoc/object/railvehicle.json b/manual/luadoc/object/railvehicle.json index c4454edd..0214173e 100644 --- a/manual/luadoc/object/railvehicle.json +++ b/manual/luadoc/object/railvehicle.json @@ -1,3 +1,4 @@ { - "active_train": {} -} \ No newline at end of file + "active_train": {}, + "trains": {} +} 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/manual/luadoc/object/turnoutsliprailtile.json b/manual/luadoc/object/turnoutsliprailtile.json new file mode 100644 index 00000000..a0b9173d --- /dev/null +++ b/manual/luadoc/object/turnoutsliprailtile.json @@ -0,0 +1,3 @@ +{ + "dual_motor": {} +} diff --git a/manual/luadoc/terms/en-us.json b/manual/luadoc/terms/en-us.json index bbfb2c53..8c2774c5 100644 --- a/manual/luadoc/terms/en-us.json +++ b/manual/luadoc/terms/en-us.json @@ -2006,5 +2006,77 @@ { "term": "object.train.on_block_reserved.parameter.direction:description", "definition": "Train direction from the block perspective, a {ref:enum.block_train_direction} value." + }, + { + "term": "object.turnoutsliprailtile.dual_motor:description", + "definition": "`true` if the slip turnout has two motors/coils, `false` if it has one motor/coil." + }, + { + "term": "object.turnoutrailtile.name:description", + "definition": "Turnout name." + }, + { + "term": "object.turnoutrailtile.position:description", + "definition": "Current turnout position, a {ref:enum.turnout_position} value. To change the turnout position call [`set_position`](#set_position)." + }, + { + "term": "object.turnoutrailtile.set_position:description", + "definition": "Change turnout position." + }, + { + "term": "object.turnoutrailtile.set_position.parameter.position:description", + "definition": "Requested turnout position, a {ref:enum.turnout_position} value." + }, + { + "term": "object.turnoutrailtile.set_position:return_values", + "definition": "`true` if the position is changed, `false` if position value is invalid or turnout is locked e.g. due to a reserved path." + }, + { + "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/traintasticmanual/css/traintasticmanual.css b/manual/traintasticmanual/css/traintasticmanual.css index bae21a8d..789974bc 100644 --- a/manual/traintasticmanual/css/traintasticmanual.css +++ b/manual/traintasticmanual/css/traintasticmanual.css @@ -320,7 +320,8 @@ ul.index-az-nav li a /** Lua **********************************************************************/ -pre[lang="lua"] +pre[lang="lua"], +pre[lang="bash"] { border: solid 1px darkgray; background-color: #f8f8f8; @@ -423,7 +424,8 @@ pre[lang="lua"] code span.function border-left-color: blue; } - pre[lang="lua"] + pre[lang="lua"], + pre[lang="bash"] { border-color: gray; background-color: #222; diff --git a/manual/traintasticmanual/en-us/gettingstarted/start-client.md b/manual/traintasticmanual/en-us/gettingstarted/start-client.md index 7a49dce9..fd5670fd 100644 --- a/manual/traintasticmanual/en-us/gettingstarted/start-client.md +++ b/manual/traintasticmanual/en-us/gettingstarted/start-client.md @@ -4,7 +4,7 @@ When Traintastic server is running the Traintastic client can be started. **Windows:** The Traintastic client can be started using the desktop icon (if installed) or by selecting *Traintastic* -> *Traintastic client* from the Windows start menu. -**Linux:** TODO +**Linux:** To start Traintastic client, open your desktop environment's application launcher, search for "Traintastic Client," and select it from the list. ## Connect to the server diff --git a/manual/traintasticmanual/en-us/gettingstarted/start-server.md b/manual/traintasticmanual/en-us/gettingstarted/start-server.md index 82bc6519..bf60644b 100644 --- a/manual/traintasticmanual/en-us/gettingstarted/start-server.md +++ b/manual/traintasticmanual/en-us/gettingstarted/start-server.md @@ -2,13 +2,36 @@ When running Traintastic, the server should be started first. -**Windows:** The Traintastic server can be started using the desktop icon (if installed) or by selecting *Traintastic* -> *Traintastic server* from the Windows start menu. +## Windows +The Traintastic server can be started using the desktop icon (if installed) or by selecting *Traintastic* -> *Traintastic server* from the Windows start menu. Traintastic server runs as background process, a Traintastic icon will appear in the system tray next to the clock. A Windows notification is displayed when it is running in the background. Traintastic server can be quit by clicking on the Traintastic icon and selecting *Quit* from the popup menu. ![](../../gfx/en-us/start/start-traintastic-server-windows.png "Traintastic server tray icon and notification") -**Linux:** TODO +## Linux +When installing Traintastic server using a Debian package it is installed as systemd service. -When Traintastic server is running proceed to [start Traintastic client](start-client.md). +To start the Traintastic server using systemd, open a terminal and run the command: +```bash +sudo systemctl start traintastic-server.service +``` + +To stop the service, use the command: +```bash +sudo systemctl stop traintastic-server.service +``` + +Ensure you have the necessary permissions (typically root) to manage systemd services. + +### Auto start on system boot +To enable Traintastic server to start automatically at boot, run the following command in a terminal: +```bash +sudo systemctl enable traintastic-server.service +``` + +To disable automatic start at boot, run the following command in a terminal: +```bash +sudo systemctl disable traintastic-server.service +``` 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/package/innosetup/traintastic.iss b/package/innosetup/traintastic.iss index 23082a2f..60a05102 100644 --- a/package/innosetup/traintastic.iss +++ b/package/innosetup/traintastic.iss @@ -54,7 +54,7 @@ Name: "firewall_wlanmaus"; Description: "{cm:firewall_allow_wlanmaus_z21}"; Grou [Files] ; Server Source: "..\..\server\build\{#ServerExeName}"; DestDir: "{app}\server"; Flags: ignoreversion; Check: InstallServer -Source: "..\..\server\thirdparty\lua5.3\bin\win64\lua53.dll"; DestDir: "{app}\server"; Flags: ignoreversion; Check: InstallServer +Source: "..\..\server\thirdparty\lua5.4\bin\win64\lua54.dll"; DestDir: "{app}\server"; Flags: ignoreversion; Check: InstallServer Source: "..\..\server\thirdparty\libarchive\bin\archive.dll"; DestDir: "{app}\server"; Flags: ignoreversion; Check: InstallServer Source: "..\..\server\thirdparty\zlib\bin\zlib1.dll"; DestDir: "{app}\server"; Flags: ignoreversion; Check: InstallServer ; Client @@ -107,7 +107,7 @@ Root: HKLM; Subkey: "{#CompanySubKey}"; Flags: uninsdeletekeyifempty Root: HKLM; Subkey: "{#AppSubKey}"; Flags: uninsdeletekey [INI] -Filename: {commonappdata}\traintastic\traintastic-client.ini; Section: general_; Key: language; String: {code:GetTraintasticClientLanguage}; Flags: uninsdeleteentry uninsdeletesectionifempty; +Filename: {commonappdata}\traintastic\traintastic-client.ini; Section: general_; Key: language; String: {code:GetTraintasticLanguage}; Flags: uninsdeleteentry uninsdeletesectionifempty; [Code] const @@ -179,6 +179,7 @@ begin ClientAndServerRadioButton.Checked := (Components = 'ClientAndServer'); ClientAndServerRadioButton.Font.Style := [fsBold]; ClientAndServerRadioButton.Height := ScaleY(23); + ClientAndServerRadioButton.Width := ComponentsPage.SurfaceWidth; ClientAndServerRadioButton.Parent := ComponentsPage.Surface; ClientAndServerRadioButton.OnClick := @ComponentRadioButtonClick; @@ -195,6 +196,7 @@ begin ClientOnlyRadioButton.Font.Style := [fsBold]; ClientOnlyRadioButton.Top := Lbl.Top + Lbl.Height + ScaleY(10); ClientOnlyRadioButton.Height := ScaleY(23); + ClientOnlyRadioButton.Width := ComponentsPage.SurfaceWidth; ClientOnlyRadioButton.Parent := ComponentsPage.Surface; ClientOnlyRadioButton.OnClick := @ComponentRadioButtonClick; @@ -206,7 +208,7 @@ begin Lbl.Parent := ComponentsPage.Surface; end; -function GetTraintasticClientLanguage(Param: String) : String; +function GetTraintasticLanguage(Param: String) : String; begin case ActiveLanguage of 'nl': Result := 'nl-nl'; @@ -219,6 +221,19 @@ begin end; end; +procedure CurStepChanged(CurStep: TSetupStep); +var + ServerSettingsFile: String; +begin + if CurStep = ssPostInstall then begin + // Server: only write language if there is no setting file yet: + ServerSettingsFile := ExpandConstant('{localappdata}\traintastic\server\settings.json'); + if not FileExists(ServerSettingsFile) then begin + SaveStringToFile(ServerSettingsFile, '{"language":"' + GetTraintasticLanguage('') + '"}', False); + end; + end +end; + function VC2019RedistNeedsInstall: Boolean; var Version: String; diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 0126a335..4536b3ff 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9) +cmake_minimum_required(VERSION 3.15) include(../shared/traintastic.cmake) project(traintastic-server VERSION ${TRAINTASTIC_VERSION} DESCRIPTION "Traintastic server") include(GNUInstallDirs) @@ -32,16 +32,25 @@ target_include_directories(traintastic-server SYSTEM PRIVATE thirdparty) if(BUILD_TESTING) + add_subdirectory(thirdparty/catch2) + set_target_properties(Catch2 PROPERTIES + CXX_STANDARD 20 + CXX_CLANG_TIDY "" + ) add_executable(traintastic-server-test test/main.cpp) add_dependencies(traintastic-server-test traintastic-lang) target_compile_definitions(traintastic-server-test PRIVATE -DTRAINTASTIC_TEST) - set_target_properties(traintastic-server-test PROPERTIES CXX_STANDARD 20) + set_target_properties(traintastic-server-test PROPERTIES + CXX_STANDARD 20 + CXX_CLANG_TIDY "" + ) target_include_directories(traintastic-server-test PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ../shared/src) target_include_directories(traintastic-server-test SYSTEM PRIVATE ../shared/thirdparty thirdparty) + target_link_libraries(traintastic-server-test PRIVATE Catch2::Catch2WithMain) endif() file(GLOB SOURCES @@ -55,6 +64,7 @@ file(GLOB SOURCES "src/board/nx/*.cpp" "src/board/tile/*.hpp" "src/board/tile/*.cpp" + "src/board/tile/hidden/*.cpp" "src/board/tile/misc/*.hpp" "src/board/tile/misc/*.cpp" "src/board/tile/rail/*.hpp" @@ -184,6 +194,30 @@ file(GLOB TEST_SOURCES set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DENABLE_LOG_DEBUG") +### VCPKG +if(DEFINED ENV{VCPKG_ROOT}) + message(STATUS "Using VCPKG (VCPKG_ROOT=$ENV{VCPKG_ROOT})") + if(WIN32) + set(VCPKG_TARGET_TRIPLET "x64-windows-static-md") + endif() + include($ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake) +endif() + +### RESOURCES ### + +include(cmake/add-resource.cmake) + +add_resource(resource-shared + BASE_DIR ../ + FILES + shared/gfx/appicon.ico +) + +add_dependencies(traintastic-server resource-shared) +if(BUILD_TESTING) + add_dependencies(traintastic-server-test resource-shared) +endif() + ### OPTIONS ### if(NO_LOCALHOST_ONLY_SETTING) @@ -215,6 +249,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: @@ -272,47 +309,12 @@ if(WIN32 AND NOT MSVC) endif() # boost -if(LINUX) - find_package(Boost 1.71 REQUIRED COMPONENTS program_options) - - target_include_directories(traintastic-server SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}) - target_link_libraries(traintastic-server PRIVATE ${Boost_LIBRARIES}) - if(BUILD_TESTING) - target_include_directories(traintastic-server-test SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}) - target_link_libraries(traintastic-server-test PRIVATE ${Boost_LIBRARIES}) - endif() -else() - add_definitions( - -DBOOST_ALL_NO_LIB - -DBOOST_ERROR_CODE_HEADER_ONLY - -DBOOST_CHRONO_HEADER_ONLY - -DBOOST_ASIO_HEADER_ONLY - -DBOOST_SYSTEM_NO_DEPRECATED) - - if(NOT MSVC) - set_source_files_properties( - thirdparty/boost/libs/program_options/src/cmdline.cpp - thirdparty/boost/libs/program_options/src/config_file.cpp - thirdparty/boost/libs/program_options/src/convert.cpp - thirdparty/boost/libs/program_options/src/options_description.cpp - thirdparty/boost/libs/program_options/src/parsers.cpp - thirdparty/boost/libs/program_options/src/positional_options.cpp - thirdparty/boost/libs/program_options/src/split.cpp - thirdparty/boost/libs/program_options/src/utf8_codecvt_facet.cpp - thirdparty/boost/libs/program_options/src/value_semantic.cpp - thirdparty/boost/libs/program_options/src/variables_map.cpp - thirdparty/boost/libs/program_options/src/winmain.cpp - PROPERTIES - COMPILE_FLAGS -Wno-shadow) - endif() - - target_include_directories(traintastic-server SYSTEM PRIVATE thirdparty/boost) - if(BUILD_TESTING) - target_include_directories(traintastic-server-test SYSTEM PRIVATE thirdparty/boost) - endif() - - file(GLOB SOURCES_BOOST "thirdparty/boost/libs/program_options/src/*.cpp") - list(APPEND SOURCES ${SOURCES_BOOST}) +find_package(Boost 1.81 REQUIRED COMPONENTS program_options) +target_include_directories(traintastic-server SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}) +target_link_libraries(traintastic-server PRIVATE ${Boost_LIBRARIES}) +if(BUILD_TESTING) + target_include_directories(traintastic-server-test SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}) + target_link_libraries(traintastic-server-test PRIVATE ${Boost_LIBRARIES}) endif() # zlib @@ -384,41 +386,41 @@ if(BUILD_TESTING) target_link_libraries(traintastic-server-test PRIVATE ${LibArchive_LIBRARIES}) endif() -# liblua5.3 +# liblua5.4 if(WIN32) add_definitions(-DLUA_BUILD_AS_DLL) - set(LUA_INCLUDE_DIR "thirdparty/lua5.3/include") + set(LUA_INCLUDE_DIR "thirdparty/lua5.4/include") if(MSVC) - set(LUA_LIBRARIES lua53) + set(LUA_LIBRARIES lua54) add_custom_command(TARGET traintastic-server PRE_LINK - COMMAND lib "/def:${PROJECT_SOURCE_DIR}/thirdparty/lua5.3/bin/win64/lua53.def" /out:lua53.lib /machine:x64) + COMMAND lib "/def:${PROJECT_SOURCE_DIR}/thirdparty/lua5.4/bin/win64/lua54.def" /out:lua54.lib /machine:x64) add_custom_command(TARGET traintastic-server-test PRE_LINK - COMMAND lib "/def:${PROJECT_SOURCE_DIR}/thirdparty/lua5.3/bin/win64/lua53.def" /out:lua53.lib /machine:x64) + COMMAND lib "/def:${PROJECT_SOURCE_DIR}/thirdparty/lua5.4/bin/win64/lua54.def" /out:lua54.lib /machine:x64) else() # MinGW can directly link .dll without import lib - set(LUA_LIBRARIES "${PROJECT_SOURCE_DIR}/thirdparty/lua5.3/bin/win64/lua53.dll") + set(LUA_LIBRARIES "${PROJECT_SOURCE_DIR}/thirdparty/lua5.4/bin/win64/lua54.dll") endif() - # copy lua53.dll to build directory, to be able to run the tests: + # copy lua54.dll to build directory, to be able to run the tests: add_custom_command(TARGET traintastic-server-test POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy "${PROJECT_SOURCE_DIR}/thirdparty/lua5.3/bin/win64/lua53.dll" .) + COMMAND ${CMAKE_COMMAND} -E copy "${PROJECT_SOURCE_DIR}/thirdparty/lua5.4/bin/win64/lua54.dll" .) elseif(APPLE) find_path(LUA_INCLUDE_DIR NAMES lua.h PATHS - "/usr/local/opt/lua@5.3/include/lua" # x86_64 - "/opt/homebrew/opt/lua@5.3/include/lua" # arm64 + "/usr/local/opt/lua@5.4/include/lua" # x86_64 + "/opt/homebrew/opt/lua@5.4/include/lua" # arm64 ) find_library(LUA_LIBRARIES - NAMES lua5.3 liblua5.3 + NAMES lua5.4 liblua5.4 PATHS - "/usr/local/opt/lua@5.3/lib" # x86_64 - "/opt/homebrew/opt/lua@5.3/lib" # arm64 + "/usr/local/opt/lua@5.4/lib" # x86_64 + "/opt/homebrew/opt/lua@5.4/lib" # arm64 ) else() - find_package(Lua 5.3 REQUIRED) + find_package(Lua 5.4 REQUIRED) endif() target_include_directories(traintastic-server PRIVATE ${LUA_INCLUDE_DIR}) target_link_libraries(traintastic-server PRIVATE ${LUA_LIBRARIES}) @@ -457,8 +459,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/FindLua.cmake b/server/FindLua.cmake index dc85e8e2..782f648d 100644 --- a/server/FindLua.cmake +++ b/server/FindLua.cmake @@ -17,6 +17,7 @@ # Copyright 2007-2009 Kitware, Inc. # Modified to support Lua 5.2 by LuaDist 2012 # Modified to support Lua 5.3 by Reinder Feenstra 2019 +# Modified to support Lua 5.4 by Reinder Feenstra 2024 # # Distributed under the OSI-approved BSD License (the "License"); # see accompanying file Copyright.txt for details. @@ -41,7 +42,7 @@ SET(_POSSIBLE_LUA_LIBRARY lua) IF(Lua_FIND_VERSION_MAJOR AND Lua_FIND_VERSION_MINOR) SET(_POSSIBLE_SUFFIXES "${Lua_FIND_VERSION_MAJOR}${Lua_FIND_VERSION_MINOR}" "${Lua_FIND_VERSION_MAJOR}.${Lua_FIND_VERSION_MINOR}" "-${Lua_FIND_VERSION_MAJOR}.${Lua_FIND_VERSION_MINOR}") ELSE(Lua_FIND_VERSION_MAJOR AND Lua_FIND_VERSION_MINOR) - SET(_POSSIBLE_SUFFIXES "53" "5.3" "-5.3" "52" "5.2" "-5.2" "51" "5.1" "-5.1") + SET(_POSSIBLE_SUFFIXES "54" "5.4" "-5.4" "53" "5.3" "-5.3" "52" "5.2" "-5.2" "51" "5.1" "-5.1") ENDIF(Lua_FIND_VERSION_MAJOR AND Lua_FIND_VERSION_MINOR) # Set up possible search names and locations 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/cmake/add-resource.cmake b/server/cmake/add-resource.cmake new file mode 100644 index 00000000..2b8a294f --- /dev/null +++ b/server/cmake/add-resource.cmake @@ -0,0 +1,40 @@ +# +# This file is part of the traintastic source code. +# See . +# +# 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. +# + +function(add_resource TARGET_NAME) + cmake_parse_arguments(PARSE_ARG "" "BASE_DIR" "FILES" ${ARGN}) + if(PARSE_ARG_BASE_DIR) + set(PARSE_ARG_BASE_DIR "${CMAKE_SOURCE_DIR}/${PARSE_ARG_BASE_DIR}") + else() + set(PARSE_ARG_BASE_DIR "${CMAKE_SOURCE_DIR}") + endif() + foreach(INPUT_FILE ${PARSE_ARG_FILES}) + set(OUTPUT_FILE ${CMAKE_BINARY_DIR}/resource/${INPUT_FILE}.hpp) + add_custom_command( + OUTPUT ${OUTPUT_FILE} + COMMAND Python3::Interpreter ${CMAKE_SOURCE_DIR}/cmake/generateresourceheader.py ${PARSE_ARG_BASE_DIR} ${INPUT_FILE} ${OUTPUT_FILE} + DEPENDS ${CMAKE_SOURCE_DIR}/cmake/generateresourceheader.py ${PARSE_ARG_BASE_DIR}/${INPUT_FILE} + COMMENT "Generating resource header resource/${INPUT_FILE}.hpp" + ) + list(APPEND OUTPUT_HEADERS ${OUTPUT_FILE}) + endforeach() + add_custom_target(${TARGET_NAME} ALL DEPENDS ${OUTPUT_HEADERS}) +endfunction() diff --git a/server/cmake/code-coverage.cmake b/server/cmake/code-coverage.cmake index 601e58a1..334b8d68 100644 --- a/server/cmake/code-coverage.cmake +++ b/server/cmake/code-coverage.cmake @@ -380,7 +380,7 @@ function(target_code_coverage TARGET_NAME) COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --zerocounters COMMAND $ ${target_code_coverage_ARGS} COMMAND - ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --base-directory + ${LCOV_PATH} --ignore-errors mismatch --directory ${CMAKE_BINARY_DIR} --base-directory ${CMAKE_SOURCE_DIR} --capture ${EXTERNAL_OPTION} --output-file ${COVERAGE_INFO} COMMAND ${EXCLUDE_COMMAND} diff --git a/server/cmake/generateresourceheader.py b/server/cmake/generateresourceheader.py new file mode 100644 index 00000000..ccd2e8e9 --- /dev/null +++ b/server/cmake/generateresourceheader.py @@ -0,0 +1,86 @@ +# +# This file is part of the traintastic source code. +# See . +# +# 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. +# + +import sys +import os +import re +import textwrap + +if len(sys.argv) != 4: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + +input_file = os.path.join(sys.argv[1], sys.argv[2]) +input_file_ext = os.path.splitext(input_file)[1] + +namespaces = ['Resource'] + os.path.dirname(sys.argv[2]).replace('../', '').split('/') +variable = re.sub(r'[\.]+','_', os.path.basename(sys.argv[2]).lower()) +guard = '_'.join(namespaces).upper() + '_' + re.sub(r'[\.]+','_', os.path.basename(sys.argv[3]).upper()) + +is_binary = input_file_ext not in ['html', 'css', 'js'] + +with open(input_file, 'rb') as f: + contents = f.read() + +os.makedirs(os.path.dirname(sys.argv[3]), exist_ok=True) + +if is_binary: + size = len(contents) + contents = ', '.join(['std::byte{' + str(by) + '}' for by in contents]) + + contents = '\n '.join(textwrap.wrap(contents, width=120)) + + with open(sys.argv[3], 'w') as f: + f.write(f'''// Auto-generated, do not edit, it will be overwritten + +#ifndef {guard} +#define {guard} + +#include + +namespace {'::'.join(namespaces)} +{{ + +constexpr std::array {variable}{{{{ + {contents} +}}}}; + +}} +#endif +''') + +else: # text + with open(sys.argv[3], 'w') as f: + f.write(f'''// Auto-generated, do not edit, it will be overwritten + +#ifndef {guard} +#define {guard} + +#include + +namespace {'::'.join(namespaces)} +{{ + +constexpr std::string_view {variable} = R"({contents})"; + +}} +#endif +''') diff --git a/server/src/board/board.cpp b/server/src/board/board.cpp index d4391ea1..b59432d5 100644 --- a/server/src/board/board.cpp +++ b/server/src/board/board.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020-2023 Reinder Feenstra + * Copyright (C) 2020-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 @@ -25,6 +25,7 @@ #include "boardlisttablemodel.hpp" #include "map/link.hpp" #include "tile/tiles.hpp" +#include "tile/hidden/hiddencrossoverrailtile.hpp" #include "../core/method.tpp" #include "../core/objectproperty.tpp" #include "../world/world.hpp" @@ -33,6 +34,8 @@ #include "../utils/displayname.hpp" #include +#include "../log/log.hpp" + CREATE_IMPL(Board) Board::Board(World& world, std::string_view _id) : @@ -53,7 +56,7 @@ Board::Board(World& world, std::string_view _id) : { const TileRotate tileRotate = it->second->rotate; - if(it->second->tileId() == TileId::RailStraight && tileClassId == StraightRailTile::classId) // merge to bridge + if(it->second->tileId == TileId::RailStraight && tileClassId == StraightRailTile::classId) // merge to bridge { if((tileRotate == rotate + TileRotate::Deg90 || tileRotate == rotate - TileRotate::Deg90) && deleteTile(x, y)) { @@ -73,7 +76,7 @@ Board::Board(World& world, std::string_view _id) : else return false; } - else if(it->second->tileId() == TileId::RailStraight && // replace straight by a straight with something extra + else if(it->second->tileId == TileId::RailStraight && // replace straight by a straight with something extra Tiles::canUpgradeStraightRail(tileClassId) && (tileRotate == rotate || (tileRotate + TileRotate::Deg180) == rotate) && deleteTile(x, y)) @@ -385,6 +388,43 @@ void Board::modified() Connector connector{startConnector.opposite()}; while(auto nextTile = getTile(connector.location)) { + if(isIntercardinal(connector.direction)) // check for crossover + { + auto prevTile = getTile(TileLocation{nextTile->x, nextTile->y} + connector.direction); + assert(prevTile); + auto otherTile1 = getTile({prevTile->x, nextTile->y}); + auto otherTile2 = getTile({nextTile->x, prevTile->y}); + + if(otherTile1 && otherTile2) + { + const auto perpendicular = + (connector.direction == Connector::Direction::NorthEast) || (connector.direction == Connector::Direction::SouthWest) + ? ~rotate90cw(connector.direction) : rotate90cw(connector.direction); + + auto otherConnector1 = otherTile1->getConnector(perpendicular); + auto otherConnector2 = otherTile2->getConnector(~perpendicular); + + if(otherConnector1 && otherConnector2) // crossover found! + { + const TileLocation topLeft{std::min(prevTile->x, nextTile->x), std::min(prevTile->y, nextTile->y)}; + auto it = m_railCrossOver.find(topLeft); + if(it == m_railCrossOver.end()) + { + it = m_railCrossOver.emplace(topLeft, std::make_shared(world())).first; + it->second->x.setValueInternal(topLeft.x); + it->second->y.setValueInternal(topLeft.y); + } + auto& crossOver = it->second; + auto crossOverConnector = crossOver->getConnector(connector.direction); + assert(crossOverConnector); + + auto link = std::make_shared(std::move(tiles)); + link->connect(*startTile->node(), startConnector, *crossOver->node(), *crossOverConnector); + return; + } + } + } + if(nextTile->node()) { auto link = std::make_shared(std::move(tiles)); @@ -429,6 +469,30 @@ void Board::modified() } } } + + // remove unconnected crossovers: + auto it = m_railCrossOver.begin(); + while(it != m_railCrossOver.end()) + { + bool remove = false; + assert(it->second->node()); + for(const auto& link : (*it->second->node()).get().links()) + { + if(!link) + { + remove = true; + break; + } + } + if(remove) + { + it = m_railCrossOver.erase(it); + } + else + { + it++; + } + } } // notify board changed: diff --git a/server/src/board/board.hpp b/server/src/board/board.hpp index 3263875c..bb468f45 100644 --- a/server/src/board/board.hpp +++ b/server/src/board/board.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020-2023 Reinder Feenstra + * Copyright (C) 2020-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 @@ -31,6 +31,7 @@ class Tile; struct TileData; +class HiddenCrossOverRailTile; class Board : public IdObject { @@ -41,6 +42,7 @@ class Board : public IdObject private: bool m_modified = false; + std::unordered_map, TileLocationHash> m_railCrossOver; void modified(); void removeTile(int16_t x, int16_t y); @@ -101,6 +103,13 @@ class Board : public IdObject return {}; } + +#ifdef TRAINTASTIC_TEST + const auto& railCrossOver() const + { + return m_railCrossOver; + } +#endif }; #endif diff --git a/server/src/board/map/abstractsignalpath.cpp b/server/src/board/map/abstractsignalpath.cpp index 36487a09..5274b839 100644 --- a/server/src/board/map/abstractsignalpath.cpp +++ b/server/src/board/map/abstractsignalpath.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 @@ -84,7 +84,7 @@ bool AbstractSignalPath::requireReservation() const return (m_signal.requireReservation == AutoYesNo::Yes || (m_signal.requireReservation == AutoYesNo::Auto && m_requireReservation)); } -const AbstractSignalPath::BlockItem* AbstractSignalPath::nextBlock(const Item* item) const +const AbstractSignalPath::BlockItem* AbstractSignalPath::nextBlock(const Item* item) { while(item) { @@ -97,7 +97,7 @@ const AbstractSignalPath::BlockItem* AbstractSignalPath::nextBlock(const Item* i return nullptr; } -std::tuple AbstractSignalPath::nextBlockOrSignal(const Item* item) const +std::tuple AbstractSignalPath::nextBlockOrSignal(const Item* item) { while(item) { @@ -238,7 +238,7 @@ std::unique_ptr AbstractSignalPath::findBlocks(c if(nextNode.getLink(0).get() == &link) return findBlocks(nextNode, *nextLink, blocksAhead); } - else if(isRailBridge(tile->tileId()) || isRailCross(tile->tileId())) + else if(isRailBridge(tile->tileId) || isRailCross(tile->tileId) || tile->tileId == TileId::HiddenRailCrossOver) { // 2 1 2 2 3 // | \| |/ @@ -257,7 +257,7 @@ std::unique_ptr AbstractSignalPath::findBlocks(c if(auto linkNode = linkTile->link->node()) return findBlocks(linkNode->get(), linkNode->get().getLink(0), blocksAhead); } - else if(tile->tileId() != TileId::RailBufferStop) + else if(tile->tileId != TileId::RailBufferStop) { if(const auto& nextLink = otherLink(nextNode, link)) { diff --git a/server/src/board/map/abstractsignalpath.hpp b/server/src/board/map/abstractsignalpath.hpp index 7b6b3398..01a6199c 100644 --- a/server/src/board/map/abstractsignalpath.hpp +++ b/server/src/board/map/abstractsignalpath.hpp @@ -181,13 +181,13 @@ class AbstractSignalPath : public Path return m_root.get(); } - const BlockItem* nextBlock(const Item* item) const; + static const BlockItem* nextBlock(const Item* item); inline const BlockItem* nextBlock() const { return nextBlock(root()); } - std::tuple nextBlockOrSignal(const Item* item) const; + static std::tuple nextBlockOrSignal(const Item* item); inline std::tuple nextBlockOrSignal() const { return nextBlockOrSignal(root()); diff --git a/server/src/board/map/blockpath.cpp b/server/src/board/map/blockpath.cpp index 7e176081..7c106207 100644 --- a/server/src/board/map/blockpath.cpp +++ b/server/src/board/map/blockpath.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 @@ -25,6 +25,7 @@ #include #include "node.hpp" #include "link.hpp" +#include "../tile/hidden/hiddencrossoverrailtile.hpp" #include "../tile/rail/blockrailtile.hpp" #include "../tile/rail/bridgerailtile.hpp" #include "../tile/rail/crossrailtile.hpp" @@ -33,6 +34,8 @@ #include "../tile/rail/turnout/turnoutrailtile.hpp" #include "../tile/rail/linkrailtile.hpp" #include "../tile/rail/nxbuttonrailtile.hpp" +#include "../../train/trainblockstatus.hpp" +#include "../../core/eventloop.hpp" #include "../../core/objectproperty.tpp" #include "../../enum/bridgepath.hpp" @@ -101,11 +104,11 @@ std::vector> BlockPath::find(BlockRailTile& startBloc const auto& nextNode = current.link->getNext(*current.node); auto& tile = nextNode.tile(); - switch(tile.tileId()) + switch(tile.tileId.value()) { case TileId::RailBlock: { - if(current.node->tile().tileId() == TileId::RailNXButton) + if(current.node->tile().tileId == TileId::RailNXButton) { current.path->m_nxButtonTo = current.node->tile().shared_ptr(); } @@ -282,6 +285,31 @@ std::vector> BlockPath::find(BlockRailTile& startBloc current.link = otherLink(nextNode, *current.link).get(); break; + case TileId::HiddenRailCrossOver: + { + // 1 2 + // X + // 0 3 + auto crossOver = tile.shared_ptr(); + if(contains(current.path->m_crossOvers, crossOver)) + { + todo.pop(); // drop it, can't pass crossover twice + break; + } + + for(size_t i = 0; i < 4; i++) + { + if(nextNode.getLink(i).get() == current.link) + { + current.node = &nextNode; + current.link = nextNode.getLink((i + 2) % 4).get(); // opposite + current.path->m_crossOvers.emplace_back(crossOver, i % 2 == 0 ? CrossState::AC : CrossState::BD); + break; + } + } + break; + } + default: // passive or non rail tiles assert(false); // this should never happen todo.pop(); // drop it in case it does, however that is a bug! @@ -296,9 +324,34 @@ BlockPath::BlockPath(BlockRailTile& block, BlockSide side) : m_fromBlock{block} , m_fromSide{side} , m_toSide{static_cast(-1)} + , m_delayReleaseTimer{EventLoop::ioContext} + , m_isReserved(false) + , m_delayedReleaseScheduled(false) { } +BlockPath::BlockPath(const BlockPath &other) + : Path(other) + , std::enable_shared_from_this() // NOLINT(readability-redundant-member-init) -Wextra requires this + , m_fromBlock(other.m_fromBlock) + , m_fromSide(other.m_fromSide) + , m_toBlock(other.m_toBlock) + , m_toSide(other.m_toSide) + , m_tiles(other.m_tiles) + , m_turnouts(other.m_turnouts) + , m_directionControls(other.m_directionControls) + , m_crossings(other.m_crossings) + , m_bridges(other.m_bridges) + , m_signals(other.m_signals) + , m_nxButtonFrom(other.m_nxButtonFrom) + , m_nxButtonTo(other.m_nxButtonTo) + , m_delayReleaseTimer{EventLoop::ioContext} + , m_isReserved(false) + , m_delayedReleaseScheduled(false) +{ + +} + bool BlockPath::operator ==(const BlockPath& other) const noexcept { return @@ -331,7 +384,7 @@ bool BlockPath::isReady() const } } - for(const auto& [directionControlWeak, state] : m_directionControls) + for(const auto& [directionControlWeak, state] : m_directionControls) // NOLINT(readability-use-anyofallof) { auto directionControl = directionControlWeak.lock(); if(!directionControl) /*[[unlikely]]*/ @@ -387,7 +440,7 @@ bool BlockPath::reserve(const std::shared_ptr& train, bool dryRun) { if(auto turnout = turnoutWeak.lock()) { - if(!turnout->reserve(position, dryRun)) + if(!turnout->reserve(shared_from_this(), position, dryRun)) { assert(dryRun); return false; @@ -434,6 +487,23 @@ bool BlockPath::reserve(const std::shared_ptr& train, bool dryRun) } } + for(const auto& [crossOverWeak, state] : m_crossOvers) + { + if(auto crossOver = crossOverWeak.lock()) + { + if(!crossOver->reserve(state, dryRun)) + { + assert(dryRun); + return false; + } + } + else /*[[unlikely]]*/ + { + assert(dryRun); + return false; + } + } + for(const auto& [bridgeWeak, path] : m_bridges) { if(auto bridge = bridgeWeak.lock()) @@ -489,6 +559,9 @@ bool BlockPath::reserve(const std::shared_ptr& train, bool dryRun) } } + if(!dryRun) + m_isReserved = true; + return true; } @@ -499,22 +572,47 @@ bool BlockPath::release(bool dryRun) return false; } + if(!dryRun) + m_delayReleaseTimer.cancel(); + + auto toBlock = m_toBlock.lock(); + if(!toBlock) /*[[unlikely]]*/ + return false; + + BlockState fromState = m_fromBlock.state.value(); + BlockState toState = toBlock->state.value(); + + if((fromState == BlockState::Occupied || fromState == BlockState::Unknown) + && (toState == BlockState::Occupied || toState == BlockState::Unknown) + && !m_fromBlock.trains.empty() && !toBlock->trains.empty()) + { + // Check if train head is beyond toBlock while its end is still in fromBlock + const auto& status1 = fromSide() == BlockSide::A ? m_fromBlock.trains.front() : m_fromBlock.trains.back(); + const auto& status2 = toSide() == BlockSide::A ? toBlock->trains.front() : toBlock->trains.back(); + + if(status1->train.value() == status2->train.value()) + return false; + } + if(!m_fromBlock.release(m_fromSide, dryRun)) { assert(dryRun); return false; } - if(auto toBlock = m_toBlock.lock()) /*[[likely]]*/ + if(!dryRun && toBlock->state.value() == BlockState::Reserved) { - if(!toBlock->release(m_toSide, dryRun)) + if(toBlock->trains.size() == 1) { - assert(dryRun); - return false; + //TODO: this bypasses some checks + toBlock->removeTrainInternal(toBlock->trains[0]); + //TODO: dryRun? what if it fails? } } - else + + if(!toBlock->release(m_toSide, dryRun)) { + assert(dryRun); return false; } @@ -597,5 +695,28 @@ bool BlockPath::release(bool dryRun) } } + if(!dryRun) + m_isReserved = false; + return true; } + +bool BlockPath::delayedRelease(uint16_t timeoutMillis) +{ + if(m_delayedReleaseScheduled) + return false; + + m_delayedReleaseScheduled = true; + + m_delayReleaseTimer.expires_after(boost::asio::chrono::milliseconds(timeoutMillis)); + m_delayReleaseTimer.async_wait([this](const boost::system::error_code& ec) + { + m_delayedReleaseScheduled = false; + + if(ec) + return; + + release(); + }); + return true; +} diff --git a/server/src/board/map/blockpath.hpp b/server/src/board/map/blockpath.hpp index 6fa01afe..2863cd6b 100644 --- a/server/src/board/map/blockpath.hpp +++ b/server/src/board/map/blockpath.hpp @@ -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,7 @@ #include #include #include +#include #include "../../enum/blockside.hpp" class RailTile; @@ -36,6 +37,7 @@ class BlockRailTile; class BridgeRailTile; enum class BridgePath : uint8_t; class CrossRailTile; +class HiddenCrossOverRailTile; enum class CrossState : uint8_t; class DirectionControlRailTile; enum class DirectionControlState : uint8_t; @@ -59,15 +61,21 @@ class BlockPath : public Path, public std::enable_shared_from_this std::vector, TurnoutPosition>> m_turnouts; //!< required turnout positions for the path std::vector, DirectionControlState>> m_directionControls; //!< required direction control states for the path std::vector, CrossState>> m_crossings; //!< required crossing states for the path + std::vector, CrossState>> m_crossOvers; //!< required crossing states for the path std::vector, BridgePath>> m_bridges; //!< bridges to reserve std::vector> m_signals; //!< signals in path std::weak_ptr m_nxButtonFrom; std::weak_ptr m_nxButtonTo; + boost::asio::steady_timer m_delayReleaseTimer; + bool m_isReserved; + bool m_delayedReleaseScheduled; + public: static std::vector> find(BlockRailTile& block); BlockPath(BlockRailTile& block, BlockSide side); + BlockPath(const BlockPath& other); bool operator ==(const BlockPath& other) const noexcept; @@ -99,11 +107,17 @@ class BlockPath : public Path, public std::enable_shared_from_this return m_toSide; } + inline bool isReserved() const + { + return m_isReserved; + } + std::shared_ptr nxButtonFrom() const; std::shared_ptr nxButtonTo() const; bool reserve(const std::shared_ptr& train, bool dryRun = false); bool release(bool dryRun = false); + bool delayedRelease(uint16_t timeoutMillis); }; #endif diff --git a/server/src/board/map/connector.hpp b/server/src/board/map/connector.hpp index 26a1b6ab..58eec778 100644 --- a/server/src/board/map/connector.hpp +++ b/server/src/board/map/connector.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 @@ -72,6 +72,17 @@ constexpr Connector::Direction operator ~(Connector::Direction value) return static_cast(n <= 4 ? n + 4 : n - 4); } +constexpr Connector::Direction rotate90cw(Connector::Direction value) +{ + const auto n = static_cast>(value); + return static_cast(n <= 6 ? n + 2 : n - 6); +} + +constexpr bool isIntercardinal(Connector::Direction value) +{ + return (static_cast>(value) & 1) == 0; +} + constexpr Connector::Direction toConnectorDirection(TileRotate value) { const auto r = static_cast>(value); diff --git a/server/src/board/map/link.cpp b/server/src/board/map/link.cpp index 465f3e24..0db1d7a7 100644 --- a/server/src/board/map/link.cpp +++ b/server/src/board/map/link.cpp @@ -24,9 +24,7 @@ #include #include "node.hpp" -Link::Link() -{ -} +Link::Link() = default; Link::Link(std::vector> tiles) : m_tiles{std::move(tiles)} diff --git a/server/src/board/map/path.cpp b/server/src/board/map/path.cpp index 2be30b1a..2451efc9 100644 --- a/server/src/board/map/path.cpp +++ b/server/src/board/map/path.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 @@ -45,7 +45,7 @@ tcb::span Path::getTurnoutLinks(TurnoutRailTile const auto& node = turnout.node()->get(); - switch(turnout.tileId()) + switch(turnout.tileId.value()) { case TileId::RailTurnoutLeft45: case TileId::RailTurnoutLeft90: diff --git a/server/src/board/nx/nxmanager.cpp b/server/src/board/nx/nxmanager.cpp index 341e4be2..1b3e02db 100644 --- a/server/src/board/nx/nxmanager.cpp +++ b/server/src/board/nx/nxmanager.cpp @@ -92,12 +92,21 @@ void NXManager::released(NXButtonRailTile& button) bool NXManager::selectPath(const NXButtonRailTile& from, const NXButtonRailTile& to) { - for(auto& path : from.block->paths()) + for(const auto& path : from.block->paths()) { if(path->nxButtonTo().get() == &to && path->nxButtonFrom().get() == &from) { LOG_DEBUG("Path found:", path->fromBlock().name.value(), "->", path->toBlock()->name.value()); + if(path->isReserved()) + { + // If user clicked an already reserved path we release it. + // TODO: make some logic to prevent releasing a path while train is inside it? + // Also releasing a path when we already set signal to "Proceed" is dangerous because train might + // have already started moving towards out path + return path->release(); + } + if(from.block->trains.empty()) { continue; // no train in from block diff --git a/server/src/board/nx/nxmanager.hpp b/server/src/board/nx/nxmanager.hpp index 77da8bad..54fb4a52 100644 --- a/server/src/board/nx/nxmanager.hpp +++ b/server/src/board/nx/nxmanager.hpp @@ -37,15 +37,15 @@ class NXManager : public SubObject private: std::list> m_pressedButtons; - bool selectPath(const NXButtonRailTile& from, const NXButtonRailTile& to); + static bool selectPath(const NXButtonRailTile& from, const NXButtonRailTile& to); public: Method&, const std::shared_ptr&)> select; NXManager(Object& parent_, std::string_view parentPropertyName); - void pressed(NXButtonRailTile& tile); - void released(NXButtonRailTile& tile); + void pressed(NXButtonRailTile& button); + void released(NXButtonRailTile& button); }; #endif diff --git a/server/src/board/tile/hidden/hiddencrossoverrailtile.cpp b/server/src/board/tile/hidden/hiddencrossoverrailtile.cpp new file mode 100644 index 00000000..42dc85ca --- /dev/null +++ b/server/src/board/tile/hidden/hiddencrossoverrailtile.cpp @@ -0,0 +1,78 @@ +/** + * server/src/board/tile/hidden/hiddencrossoverrailtile.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 "hiddencrossoverrailtile.hpp" +#include + +HiddenCrossOverRailTile::HiddenCrossOverRailTile(World& world) + : HiddenTile(world, TileId::HiddenRailCrossOver) + , m_node{*this, 4} + , m_crossState{CrossState::Unset} +{ +} + +std::string_view HiddenCrossOverRailTile::getClassId() const +{ + assert(false); + return {}; +} + +void HiddenCrossOverRailTile::getConnectors(std::vector& connectors) const +{ + // x x+1 + // +--+--+ + // y | | | + // +--X--+ + // y+1 | | | + // +--+--+ + // + // The hidden crossing is actually at (x+0.5, y+0.5), but that can't be stored. + // So we store (x, y) and trick it a bit. + connectors.emplace_back(location().adjusted(0, 1), Connector::Direction::NorthEast, Connector::Type::Rail); + connectors.emplace_back(location(), Connector::Direction::SouthEast, Connector::Type::Rail); + connectors.emplace_back(location().adjusted(1, 0), Connector::Direction::SouthWest, Connector::Type::Rail); + connectors.emplace_back(location().adjusted(1, 1), Connector::Direction::NorthWest, Connector::Type::Rail); +} + +bool HiddenCrossOverRailTile::reserve(CrossState crossState, bool dryRun) +{ + if(m_crossState != CrossState::Unset) + { + return false; + } + + if(!dryRun) + { + m_crossState = crossState; + } + + return true; +} + +bool HiddenCrossOverRailTile::release(bool dryRun) +{ + if(!dryRun) + { + m_crossState = CrossState::Unset; + } + return true; +} diff --git a/server/src/board/tile/hidden/hiddencrossoverrailtile.hpp b/server/src/board/tile/hidden/hiddencrossoverrailtile.hpp new file mode 100644 index 00000000..deb85ea3 --- /dev/null +++ b/server/src/board/tile/hidden/hiddencrossoverrailtile.hpp @@ -0,0 +1,50 @@ +/** + * server/src/board/tile/hidden/hiddencrossoverrailtile.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_BOARD_TILE_HIDDEN_HIDDENCROSSOVERRAILTILE_HPP +#define TRAINTASTIC_SERVER_BOARD_TILE_HIDDEN_HIDDENCROSSOVERRAILTILE_HPP + +#include "hiddentile.hpp" +#include "../../map/node.hpp" + +enum class CrossState : uint8_t; + +class HiddenCrossOverRailTile : public HiddenTile +{ + private: + Node m_node; + CrossState m_crossState; //!< indicates which path is reserved + + public: + HiddenCrossOverRailTile(World& world); + + std::string_view getClassId() const final; + + std::optional> node() const final { return m_node; } + std::optional> node() final { return m_node; } + void getConnectors(std::vector& connectors) const final; + + bool reserve(CrossState crossState, bool dryRun = false); + bool release(bool dryRun = false); +}; + +#endif diff --git a/server/src/enum/worldevent.hpp b/server/src/board/tile/hidden/hiddentile.hpp similarity index 68% rename from server/src/enum/worldevent.hpp rename to server/src/board/tile/hidden/hiddentile.hpp index 3fc8447a..b6e383cd 100644 --- a/server/src/enum/worldevent.hpp +++ b/server/src/board/tile/hidden/hiddentile.hpp @@ -1,9 +1,9 @@ /** - * server/src/enum/worldevent.hpp + * server/src/board/tile/hidden/hiddentile.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,18 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef TRAINTASTIC_SERVER_ENUM_WORLDEVENT_HPP -#define TRAINTASTIC_SERVER_ENUM_WORLDEVENT_HPP +#ifndef TRAINTASTIC_SERVER_BOARD_TILE_HIDDEN_HIDDENTILE_HPP +#define TRAINTASTIC_SERVER_BOARD_TILE_HIDDEN_HIDDENTILE_HPP -#include +#include "../tile.hpp" + +class HiddenTile : public Tile +{ + protected: + HiddenTile(World& world, TileId tileId_) + : Tile(world, {}, tileId_) + { + } +}; #endif diff --git a/server/src/board/tile/misc/switchtile.cpp b/server/src/board/tile/misc/switchtile.cpp index 6b1f88e0..9a1f0873 100644 --- a/server/src/board/tile/misc/switchtile.cpp +++ b/server/src/board/tile/misc/switchtile.cpp @@ -89,6 +89,18 @@ SwitchTile::SwitchTile(World& world, std::string_view _id) m_interfaceItems.add(onValueChanged); } +void SwitchTile::destroying() +{ + outputMap->parentObject.setValueInternal(nullptr); + Tile::destroying(); +} + +void SwitchTile::addToWorld() +{ + outputMap->parentObject.setValueInternal(shared_from_this()); + Tile::addToWorld(); +} + void SwitchTile::worldEvent(WorldState worldState, WorldEvent worldEvent) { Tile::worldEvent(worldState, worldEvent); diff --git a/server/src/board/tile/misc/switchtile.hpp b/server/src/board/tile/misc/switchtile.hpp index d0ccb63f..cdacb34d 100644 --- a/server/src/board/tile/misc/switchtile.hpp +++ b/server/src/board/tile/misc/switchtile.hpp @@ -38,6 +38,8 @@ class SwitchTile : public Tile CREATE_DEF(SwitchTile) protected: + void destroying() override; + void addToWorld() final; void worldEvent(WorldState worldState, WorldEvent worldEvent) final; public: diff --git a/server/src/board/tile/rail/blockrailtile.cpp b/server/src/board/tile/rail/blockrailtile.cpp index 6ab7f4ff..7dd6f771 100644 --- a/server/src/board/tile/rail/blockrailtile.cpp +++ b/server/src/board/tile/rail/blockrailtile.cpp @@ -133,35 +133,7 @@ BlockRailTile::BlockRailTile(World& world, std::string_view _id) : throw LogMessageException(LogMessage::E3006_CANT_REMOVE_TRAIN_TRAIN_CAN_ONLY_BE_REMOVED_FROM_HEAD_OR_TAIL_BLOCK); } - status->destroy(); - status.reset(); - - updateTrainMethodEnabled(); - if(state == BlockState::Reserved) - updateState(); - Log::log(*this, LogMessage::N3002_REMOVED_TRAIN_X_FROM_BLOCK_X, oldTrain->name.value(), name.value()); - - if(oldTrain->blocks.empty()) - { - oldTrain->active = false; - } - - if(m_world.simulation) - { - for(const auto& item : *inputMap) - { - if(item->input && item->input->interface) - { - if(item->type == SensorType::OccupancyDetector) - item->input->simulateChange(item->invert.value() ? SimulateInputAction::SetTrue : SimulateInputAction::SetFalse); - else - assert(false); // not yet implemented - } - } - } - - oldTrain->fireBlockRemoved(shared_ptr()); - fireEvent(onTrainRemoved, oldTrain, self); + removeTrainInternal(status); } }} , flipTrain{*this, "flip_train", @@ -285,11 +257,9 @@ void BlockRailTile::inputItemValueChanged(BlockInputMapItem& item) //! \todo log something (at least in debug) break; } - else - { - train = status->train.value(); - direction = path->toSide() == BlockSide::A ? BlockTrainDirection::TowardsB : BlockTrainDirection::TowardsA; - } + + train = status->train.value(); + direction = path->toSide() == BlockSide::A ? BlockTrainDirection::TowardsB : BlockTrainDirection::TowardsA; } } } @@ -312,7 +282,7 @@ void BlockRailTile::inputItemValueChanged(BlockInputMapItem& item) if(enterA != enterB) { - auto& blockStatus = enterA ? trains.front() : trains.back(); + const auto& blockStatus = enterA ? trains.front() : trains.back(); TrainTracking::enter(blockStatus); } else @@ -374,6 +344,7 @@ void BlockRailTile::identificationEvent(BlockInputMapItem& /*item*/, Identificat case IdentificationEventType::Present: //!< \todo assign train (if allowed and possible) trains.appendInternal(TrainBlockStatus::create(*this, std::string("#").append(std::to_string(identifier)), blockDirection)); + updateTrainMethodEnabled(); if(state == BlockState::Free || state == BlockState::Unknown) updateState(); break; @@ -409,7 +380,7 @@ void BlockRailTile::identificationEvent(BlockInputMapItem& /*item*/, Identificat } } -const std::shared_ptr BlockRailTile::getReservedPath(BlockSide side) const +std::shared_ptr BlockRailTile::getReservedPath(BlockSide side) const { assert(side == BlockSide::A || side == BlockSide::B); return m_reservedPaths[static_cast(side)].lock(); @@ -475,6 +446,46 @@ bool BlockRailTile::release(BlockSide side, bool dryRun) return true; } +bool BlockRailTile::removeTrainInternal(const std::shared_ptr &status) +{ + if(!status) + return false; + + const auto self = shared_ptr(); + const std::shared_ptr oldTrain = status->train.value(); + + status->destroy(); + + updateTrainMethodEnabled(); + if(state == BlockState::Reserved) + updateState(); + Log::log(*this, LogMessage::N3002_REMOVED_TRAIN_X_FROM_BLOCK_X, oldTrain->name.value(), name.value()); + + if(oldTrain->blocks.empty()) + { + oldTrain->active = false; + } + + if(m_world.simulation) + { + for(const auto& item : *inputMap) + { + if(item->input && item->input->interface) + { + if(item->type == SensorType::OccupancyDetector) + item->input->simulateChange(item->invert.value() ? SimulateInputAction::SetTrue : SimulateInputAction::SetFalse); + else + assert(false); // not yet implemented + } + } + } + + oldTrain->fireBlockRemoved(shared_ptr()); + fireEvent(onTrainRemoved, oldTrain, self); + + return true; +} + void BlockRailTile::updateState() { if(!inputMap->items.empty()) diff --git a/server/src/board/tile/rail/blockrailtile.hpp b/server/src/board/tile/rail/blockrailtile.hpp index 65788e4f..2ec77608 100644 --- a/server/src/board/tile/rail/blockrailtile.hpp +++ b/server/src/board/tile/rail/blockrailtile.hpp @@ -107,9 +107,11 @@ class BlockRailTile : public RailTile void inputItemValueChanged(BlockInputMapItem& item); void identificationEvent(BlockInputMapItem& item, IdentificationEventType eventType, uint16_t identifier, Direction direction, uint8_t category); - const std::shared_ptr getReservedPath(BlockSide side) const; + std::shared_ptr getReservedPath(BlockSide side) const; bool reserve(const std::shared_ptr& blockPath, const std::shared_ptr& train, BlockSide side, bool dryRun = false); bool release(BlockSide side, bool dryRun = false); + + bool removeTrainInternal(const std::shared_ptr& status); }; #endif diff --git a/server/src/board/tile/rail/crossrailtile.cpp b/server/src/board/tile/rail/crossrailtile.cpp index e40d1952..4f218054 100644 --- a/server/src/board/tile/rail/crossrailtile.cpp +++ b/server/src/board/tile/rail/crossrailtile.cpp @@ -53,6 +53,7 @@ bool CrossRailTile::release(bool dryRun) if(!dryRun) { + m_crossState = CrossState::Unset; RailTile::release(); } return true; diff --git a/server/src/board/tile/rail/decouplerrailtile.cpp b/server/src/board/tile/rail/decouplerrailtile.cpp index 5524fc1b..18a309da 100644 --- a/server/src/board/tile/rail/decouplerrailtile.cpp +++ b/server/src/board/tile/rail/decouplerrailtile.cpp @@ -74,6 +74,18 @@ DecouplerRailTile::DecouplerRailTile(World& world, std::string_view _id) }); } +void DecouplerRailTile::destroying() +{ + outputMap->parentObject.setValueInternal(nullptr); + StraightRailTile::destroying(); +} + +void DecouplerRailTile::addToWorld() +{ + outputMap->parentObject.setValueInternal(shared_from_this()); + StraightRailTile::addToWorld(); +} + void DecouplerRailTile::worldEvent(WorldState worldState, WorldEvent worldEvent) { StraightRailTile::worldEvent(worldState, worldEvent); diff --git a/server/src/board/tile/rail/decouplerrailtile.hpp b/server/src/board/tile/rail/decouplerrailtile.hpp index 00252542..7803adf7 100644 --- a/server/src/board/tile/rail/decouplerrailtile.hpp +++ b/server/src/board/tile/rail/decouplerrailtile.hpp @@ -40,6 +40,8 @@ class DecouplerRailTile final : public StraightRailTile void setState(DecouplerState value, bool skipAction = false); protected: + void destroying() override; + void addToWorld() final; void worldEvent(WorldState worldState, WorldEvent worldEvent) final; public: diff --git a/server/src/board/tile/rail/directioncontrolrailtile.cpp b/server/src/board/tile/rail/directioncontrolrailtile.cpp index 3acbe6d1..76ec9dd3 100644 --- a/server/src/board/tile/rail/directioncontrolrailtile.cpp +++ b/server/src/board/tile/rail/directioncontrolrailtile.cpp @@ -32,6 +32,7 @@ CREATE_IMPL(DirectionControlRailTile) DirectionControlRailTile::DirectionControlRailTile(World& world, std::string_view _id) : StraightRailTile(world, _id, TileId::RailDirectionControl) , m_node{*this, 2} + , m_reservedState(DirectionControlState::None) , name{this, "name", id, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly} , useNone{this, "use_none", true, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly, [this](const bool /*value*/) @@ -61,6 +62,14 @@ DirectionControlRailTile::DirectionControlRailTile(World& world, std::string_vie , setState{*this, "set_state", MethodFlags::ScriptCallable, [this](DirectionControlState newState) { + if(reservedState() && newState != m_reservedState) + { + // Direction control is currently locked by reserved path + // Allow setting to Both directions unless reserved direction is None + if(m_reservedState == DirectionControlState::None || newState != DirectionControlState::Both) + return false; + } + const auto& states = setState.getVectorAttribute(AttributeName::Values); if(std::find(states.begin(), states.end(), newState) == states.end()) return false; @@ -121,13 +130,14 @@ DirectionControlRailTile::DirectionControlRailTile(World& world, std::string_vie bool DirectionControlRailTile::reserve(DirectionControlState directionControlState, bool dryRun) { - if(state != directionControlState && state != DirectionControlState::Both) + if(reservedState() || (state != directionControlState && state != DirectionControlState::Both)) { return false; } if(!dryRun) { + m_reservedState = directionControlState; StraightRailTile::reserve(); } diff --git a/server/src/board/tile/rail/directioncontrolrailtile.hpp b/server/src/board/tile/rail/directioncontrolrailtile.hpp index 48b3ea56..92ce68ff 100644 --- a/server/src/board/tile/rail/directioncontrolrailtile.hpp +++ b/server/src/board/tile/rail/directioncontrolrailtile.hpp @@ -36,6 +36,7 @@ class DirectionControlRailTile final : public StraightRailTile private: Node m_node; + DirectionControlState m_reservedState; void updateEnabled(); void updateStateValues(); @@ -61,7 +62,7 @@ class DirectionControlRailTile final : public StraightRailTile std::optional> node() const final { return m_node; } std::optional> node() final { return m_node; } - bool reserve(DirectionControlState turnoutPosition, bool dryRun = false); + bool reserve(DirectionControlState directionControlState, bool dryRun = false); }; #endif diff --git a/server/src/board/tile/rail/nxbuttonrailtile.cpp b/server/src/board/tile/rail/nxbuttonrailtile.cpp index 209f1c00..d6691541 100644 --- a/server/src/board/tile/rail/nxbuttonrailtile.cpp +++ b/server/src/board/tile/rail/nxbuttonrailtile.cpp @@ -38,9 +38,9 @@ static std::shared_ptr findBlock(Node& node, uint8_t linkIndex) return {}; } auto* tile = &link->getNext(node).tile(); - while(tile->tileId() != TileId::RailBlock) + while(tile->tileId != TileId::RailBlock) { - if(isRailBridge(tile->tileId())) + if(isRailBridge(tile->tileId)) { auto& bridgeNode = (*tile->node()).get(); size_t index = bridgeNode.getIndex(*link); diff --git a/server/src/board/tile/rail/railtile.cpp b/server/src/board/tile/rail/railtile.cpp index 425184c9..b5f94b52 100644 --- a/server/src/board/tile/rail/railtile.cpp +++ b/server/src/board/tile/rail/railtile.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020,2023 Reinder Feenstra + * Copyright (C) 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 @@ -23,8 +23,8 @@ #include "railtile.hpp" #include "../../board.hpp" -RailTile::RailTile(World& world, std::string_view _id, TileId tileId) : - Tile(world, _id, tileId) +RailTile::RailTile(World& world, std::string_view _id, TileId tileId_) : + Tile(world, _id, tileId_) { } diff --git a/server/src/board/tile/rail/railtile.hpp b/server/src/board/tile/rail/railtile.hpp index 230c116e..9a09fbdd 100644 --- a/server/src/board/tile/rail/railtile.hpp +++ b/server/src/board/tile/rail/railtile.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020,2023 Reinder Feenstra + * Copyright (C) 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 @@ -31,7 +31,7 @@ class RailTile : public Tile uint8_t m_reservedState = 0; protected: - RailTile(World& world, std::string_view _id, TileId tileId); + RailTile(World& world, std::string_view _id, TileId tileId_); uint8_t reservedState() const final { diff --git a/server/src/board/tile/rail/signal/signalrailtile.cpp b/server/src/board/tile/rail/signal/signalrailtile.cpp index bb538d56..bd723a69 100644 --- a/server/src/board/tile/rail/signal/signalrailtile.cpp +++ b/server/src/board/tile/rail/signal/signalrailtile.cpp @@ -22,11 +22,16 @@ #include "signalrailtile.hpp" #include "../../../map/abstractsignalpath.hpp" +#include "../../../map/blockpath.hpp" #include "../../../../core/attributes.hpp" #include "../../../../core/method.tpp" #include "../../../../core/objectproperty.tpp" #include "../../../../world/getworld.hpp" #include "../../../../utils/displayname.hpp" +#include "../blockrailtile.hpp" +#include "../../../../train/trainblockstatus.hpp" +#include "../../../../train/train.hpp" +#include "../../../../log/log.hpp" std::optional SignalRailTile::getDefaultActionValue(SignalAspect signalAspect, OutputType outputType, size_t outputIndex) { @@ -59,11 +64,11 @@ std::optional SignalRailTile::getDefaultActionValue(SignalAsp { return static_cast(0); } - else if(signalAspect == SignalAspect::ProceedReducedSpeed) + if(signalAspect == SignalAspect::ProceedReducedSpeed) { return static_cast(1); } - else if(signalAspect == SignalAspect::Proceed) + if(signalAspect == SignalAspect::Proceed) { return static_cast(16); } @@ -76,9 +81,10 @@ std::optional SignalRailTile::getDefaultActionValue(SignalAsp return {}; } -SignalRailTile::SignalRailTile(World& world, std::string_view _id, TileId tileId) : - StraightRailTile(world, _id, tileId), +SignalRailTile::SignalRailTile(World& world, std::string_view _id, TileId tileId_) : + StraightRailTile(world, _id, tileId_), m_node{*this, 2}, + m_retryCount(0), name{this, "name", std::string(_id), PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly}, requireReservation{this, "require_reservation", AutoYesNo::Auto, PropertyFlags::ReadWrite | PropertyFlags::Store}, aspect{this, "aspect", SignalAspect::Unknown, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}, @@ -92,6 +98,7 @@ SignalRailTile::SignalRailTile(World& world, std::string_view _id, TileId tileId Attributes::addEnabled(name, editable); m_interfaceItems.add(name); + Attributes::addDisplayName(requireReservation, "board_tile.rail.signal:require_reservation"); Attributes::addEnabled(requireReservation, editable); Attributes::addValues(requireReservation, autoYesNoValues); m_interfaceItems.add(requireReservation); @@ -133,6 +140,18 @@ bool SignalRailTile::reserve(const std::shared_ptr& blockPath, bool d return true; } +void SignalRailTile::destroying() +{ + outputMap->parentObject.setValueInternal(nullptr); + StraightRailTile::destroying(); +} + +void SignalRailTile::addToWorld() +{ + outputMap->parentObject.setValueInternal(shared_from_this()); + StraightRailTile::addToWorld(); +} + void SignalRailTile::worldEvent(WorldState state, WorldEvent event) { StraightRailTile::worldEvent(state, event); @@ -191,14 +210,89 @@ void SignalRailTile::connectOutputMap() { outputMap->onOutputStateMatchFound.connect([this](SignalAspect value) { - bool changed = (value == aspect); - if(doSetAspect(value, true)) + bool changed = (value != aspect); + if(!doSetAspect(value, true) || !changed) + return; // No change + + if(!m_signalPath || !hasReservedPath()) + return; // Not locked + + // If we are in a signal path, re-evaluate our aspect + // This corrects accidental modifications of aspect done + // by the user with an handset or command station. + Log::log(id, LogMessage::W3004_LOCKED_SIGNAL_CHANGED); + + if(m_world.correctOutputPosWhenLocked) { - // If we are in a signal path, re-evaluate our aspect - // This corrects accidental modifications of aspect done - // by the user with an handset or command station. - if(changed && m_signalPath) + auto now = std::chrono::steady_clock::now(); + if((now - m_lastRetryStart) >= RETRY_DURATION) + { + // Reset retry count + m_lastRetryStart = now; + m_retryCount = 0; + } + + if(m_retryCount < MAX_RETRYCOUNT) + { + // Try to reset output to reseved state + m_retryCount++; evaluate(); + + Log::log(id, LogMessage::N3004_SIGNAL_RESET_TO_RESERVED_ASPECT); + return; + } + } + + // We reached maximum retry count + // We cannot lock this signal. Take action. + switch (m_world.extOutputChangeAction.value()) + { + default: + case ExternalOutputChangeAction::DoNothing: + { + // Do nothing + break; + } + case ExternalOutputChangeAction::EmergencyStopTrain: + { + if(auto blockPath = reservedPath()) + { + std::vector> alreadyStoppedTrains; + + for(auto it : blockPath->fromBlock().trains) + { + it->train.value()->emergencyStop.setValue(true); + alreadyStoppedTrains.push_back(it->train.value()); + Log::log(it->train->id, LogMessage::E3004_TRAIN_STOPPED_ON_SIGNAL_X_CHANGED, id.value()); + } + + auto toBlock = blockPath->toBlock(); + if(toBlock) + { + for(auto it : blockPath->toBlock()->trains) + { + if(std::find(alreadyStoppedTrains.cbegin(), alreadyStoppedTrains.cend(), it->train.value()) != alreadyStoppedTrains.cend()) + continue; // Do not stop train twice + + it->train.value()->emergencyStop.setValue(true); + Log::log(it->train->id, LogMessage::E3004_TRAIN_STOPPED_ON_SIGNAL_X_CHANGED, id.value()); + } + } + } + break; + } + case ExternalOutputChangeAction::EmergencyStopWorld: + { + m_world.stop(); + Log::log(m_world, LogMessage::E3008_WORLD_STOPPED_ON_SIGNAL_X_CHANGED, id.value()); + break; + } + case ExternalOutputChangeAction::PowerOffWorld: + { + m_world.powerOff(); + Log::log(m_world, LogMessage::E3010_WORLD_POWER_OFF_ON_SIGNAL_X_CHANGED, id.value()); + break; + } } }); diff --git a/server/src/board/tile/rail/signal/signalrailtile.hpp b/server/src/board/tile/rail/signal/signalrailtile.hpp index 70c3083f..431a081f 100644 --- a/server/src/board/tile/rail/signal/signalrailtile.hpp +++ b/server/src/board/tile/rail/signal/signalrailtile.hpp @@ -23,6 +23,7 @@ #ifndef TRAINTASTIC_SERVER_BOARD_TILE_RAIL_SIGNAL_SIGNALRAILTILE_HPP #define TRAINTASTIC_SERVER_BOARD_TILE_RAIL_SIGNAL_SIGNALRAILTILE_HPP +#include #include "../straightrailtile.hpp" #include #include "../../../map/node.hpp" @@ -43,9 +44,15 @@ class SignalRailTile : public StraightRailTile Node m_node; std::unique_ptr m_signalPath; std::weak_ptr m_blockPath; + std::chrono::steady_clock::time_point m_lastRetryStart; + uint8_t m_retryCount; + static constexpr uint8_t MAX_RETRYCOUNT = 3; + static constexpr std::chrono::steady_clock::duration RETRY_DURATION = std::chrono::minutes(1); - SignalRailTile(World& world, std::string_view _id, TileId tileId); + SignalRailTile(World& world, std::string_view _id, TileId tileId_); + void destroying() override; + void addToWorld() override; void worldEvent(WorldState state, WorldEvent event) override; void boardModified() override; diff --git a/server/src/board/tile/rail/straightrailtile.cpp b/server/src/board/tile/rail/straightrailtile.cpp index 980e27ff..eea44e15 100644 --- a/server/src/board/tile/rail/straightrailtile.cpp +++ b/server/src/board/tile/rail/straightrailtile.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020,2022 Reinder Feenstra + * Copyright (C) 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 @@ -22,8 +22,8 @@ #include "straightrailtile.hpp" -StraightRailTile::StraightRailTile(World& world, std::string_view _id, TileId tileId) : - RailTile(world, _id, tileId) +StraightRailTile::StraightRailTile(World& world, std::string_view _id, TileId tileId_) : + RailTile(world, _id, tileId_) { } diff --git a/server/src/board/tile/rail/straightrailtile.hpp b/server/src/board/tile/rail/straightrailtile.hpp index c166cbcb..d75379ea 100644 --- a/server/src/board/tile/rail/straightrailtile.hpp +++ b/server/src/board/tile/rail/straightrailtile.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020,2022 Reinder Feenstra + * Copyright (C) 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 @@ -31,7 +31,7 @@ class StraightRailTile : public RailTile CREATE(StraightRailTile) public: - StraightRailTile(World& world, std::string_view _id, TileId tileId = TileId::RailStraight); + StraightRailTile(World& world, std::string_view _id, TileId tileId_ = TileId::RailStraight); void getConnectors(std::vector& connectors) const final; }; diff --git a/server/src/board/tile/rail/turnout/turnout3wayrailtile.cpp b/server/src/board/tile/rail/turnout/turnout3wayrailtile.cpp index 15118b77..c69d6cdc 100644 --- a/server/src/board/tile/rail/turnout/turnout3wayrailtile.cpp +++ b/server/src/board/tile/rail/turnout/turnout3wayrailtile.cpp @@ -29,10 +29,33 @@ static const std::array positionValues = {TurnoutPosition::U static std::optional getDefaultActionValue(TurnoutPosition turnoutPosition, OutputType outputType, size_t outputIndex) { - // FIXME: implement defaults - (void)turnoutPosition; - (void)outputType; - (void)outputIndex; + // FIXME: implement more defaults + switch(outputType) + { + case OutputType::Aspect: + if(outputIndex == 0) + { + // There is no official/defacto standard yet, until there is use values used by YaMoRC YD8116. + switch(turnoutPosition) + { + case TurnoutPosition::Left: + return static_cast(0); + + case TurnoutPosition::Right: + return static_cast(1); + + case TurnoutPosition::Straight: + return static_cast(16); + + default: + break; + } + } + break; + + default: + break; + } return {}; } diff --git a/server/src/board/tile/rail/turnout/turnoutdoublesliprailtile.cpp b/server/src/board/tile/rail/turnout/turnoutdoublesliprailtile.cpp index 333e0248..497b2b8d 100644 --- a/server/src/board/tile/rail/turnout/turnoutdoublesliprailtile.cpp +++ b/server/src/board/tile/rail/turnout/turnoutdoublesliprailtile.cpp @@ -25,26 +25,26 @@ #include "../../../../core/objectproperty.tpp" #include "../../../../hardware/output/outputcontroller.hpp" -static const std::array positionValues = {TurnoutPosition::Unknown, - TurnoutPosition::Left, TurnoutPosition::Right, - TurnoutPosition::Crossed, TurnoutPosition::Diverged, - TurnoutPosition::DoubleSlipStraightA, TurnoutPosition::DoubleSlipStraightB}; - -static std::optional getDefaultActionValue(TurnoutPosition turnoutPosition, OutputType outputType, size_t outputIndex) +namespace PositionValues { - // FIXME: implement defaults - (void)turnoutPosition; - (void)outputType; - (void)outputIndex; - return {}; + static const std::array singleMotor = { + TurnoutPosition::Unknown, + TurnoutPosition::Crossed, TurnoutPosition::Diverged + }; + static const std::array dualMotor = { + TurnoutPosition::Unknown, + TurnoutPosition::Left, TurnoutPosition::Right, TurnoutPosition::DoubleSlipStraightA, TurnoutPosition::DoubleSlipStraightB + }; } -TurnoutDoubleSlipRailTile::TurnoutDoubleSlipRailTile(World& world, std::string_view _id) - : TurnoutRailTile(world, _id, TileId::RailTurnoutDoubleSlip, 4) -{ - // Skip Unknown position - tcb::span setPositionValues = tcb::make_span(positionValues).subspan<1>(); +static constexpr tcb::span positionValuesSingleMotor = tcb::make_span(PositionValues::singleMotor); +static constexpr tcb::span positionValuesDualMotor = tcb::make_span(PositionValues::dualMotor); +static constexpr tcb::span setPositionValuesSingleMotor = tcb::make_span(PositionValues::singleMotor).subspan<1>(); +static constexpr tcb::span setPositionValuesDualMotor = tcb::make_span(PositionValues::dualMotor).subspan<1>(); +TurnoutDoubleSlipRailTile::TurnoutDoubleSlipRailTile(World& world, std::string_view _id) + : TurnoutSlipRailTile(world, _id, TileId::RailTurnoutDoubleSlip) +{ outputMap.setValueInternal(std::make_shared(*this, outputMap.name(), std::initializer_list{ TurnoutPosition::Left, TurnoutPosition::Right, @@ -52,13 +52,14 @@ TurnoutDoubleSlipRailTile::TurnoutDoubleSlipRailTile(World& world, std::string_v TurnoutPosition::DoubleSlipStraightA, TurnoutPosition::DoubleSlipStraightB}, getDefaultActionValue)); - Attributes::addValues(position, positionValues); + Attributes::addValues(position, positionValuesSingleMotor); m_interfaceItems.add(position); - Attributes::addValues(setPosition, setPositionValues); + Attributes::addValues(setPosition, setPositionValuesSingleMotor); m_interfaceItems.add(setPosition); connectOutputMap(); + dualMotorChanged(); } void TurnoutDoubleSlipRailTile::getConnectors(std::vector& connectors) const @@ -68,3 +69,24 @@ void TurnoutDoubleSlipRailTile::getConnectors(std::vector& connectors connectors.emplace_back(location(), rotate + TileRotate::Deg180, Connector::Type::Rail); connectors.emplace_back(location(), rotate + TileRotate::Deg315, Connector::Type::Rail); } + +void TurnoutDoubleSlipRailTile::dualMotorChanged() +{ + (*outputMap)[TurnoutPosition::Diverged]->visible.setValueInternal(!dualMotor); + (*outputMap)[TurnoutPosition::Crossed]->visible.setValueInternal(!dualMotor); + (*outputMap)[TurnoutPosition::Left]->visible.setValueInternal(dualMotor); + (*outputMap)[TurnoutPosition::Right]->visible.setValueInternal(dualMotor); + (*outputMap)[TurnoutPosition::DoubleSlipStraightA]->visible.setValueInternal(dualMotor); + (*outputMap)[TurnoutPosition::DoubleSlipStraightB]->visible.setValueInternal(dualMotor); + + if(dualMotor) + { + Attributes::setValues(position, positionValuesDualMotor); + Attributes::setValues(setPosition, setPositionValuesDualMotor); + } + else + { + Attributes::setValues(position, positionValuesSingleMotor); + Attributes::setValues(setPosition, setPositionValuesSingleMotor); + } +} diff --git a/server/src/board/tile/rail/turnout/turnoutdoublesliprailtile.hpp b/server/src/board/tile/rail/turnout/turnoutdoublesliprailtile.hpp index fbebd6d8..03e38df3 100644 --- a/server/src/board/tile/rail/turnout/turnoutdoublesliprailtile.hpp +++ b/server/src/board/tile/rail/turnout/turnoutdoublesliprailtile.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020-2022 Reinder Feenstra + * Copyright (C) 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 @@ -23,13 +23,16 @@ #ifndef TRAINTASTIC_SERVER_BOARD_TILE_RAIL_TURNOUT_TURNOUTDOUBLESLIPRAILTILE_HPP #define TRAINTASTIC_SERVER_BOARD_TILE_RAIL_TURNOUT_TURNOUTDOUBLESLIPRAILTILE_HPP -#include "turnoutrailtile.hpp" +#include "turnoutsliprailtile.hpp" -class TurnoutDoubleSlipRailTile : public TurnoutRailTile +class TurnoutDoubleSlipRailTile : public TurnoutSlipRailTile { CLASS_ID("board_tile.rail.turnout_doubleslip") CREATE(TurnoutDoubleSlipRailTile) +protected: + void dualMotorChanged() final; + public: TurnoutDoubleSlipRailTile(World& world, std::string_view _id); diff --git a/server/src/board/tile/rail/turnout/turnoutleftrailtile.cpp b/server/src/board/tile/rail/turnout/turnoutleftrailtile.cpp index 5efaa5c5..e1a674a3 100644 --- a/server/src/board/tile/rail/turnout/turnoutleftrailtile.cpp +++ b/server/src/board/tile/rail/turnout/turnoutleftrailtile.cpp @@ -63,8 +63,8 @@ static std::optional getDefaultActionValue(TurnoutPosition tu return {}; } -TurnoutLeftRailTile::TurnoutLeftRailTile(World& world, std::string_view _id, TileId tileId) - : TurnoutRailTile(world, _id, tileId, 3) +TurnoutLeftRailTile::TurnoutLeftRailTile(World& world, std::string_view _id, TileId tileId_) + : TurnoutRailTile(world, _id, tileId_, 3) { // Skip Unknown position tcb::span setPositionValues = tcb::make_span(positionValues).subspan<1>(); diff --git a/server/src/board/tile/rail/turnout/turnoutleftrailtile.hpp b/server/src/board/tile/rail/turnout/turnoutleftrailtile.hpp index 5f1dd81c..ee4897a9 100644 --- a/server/src/board/tile/rail/turnout/turnoutleftrailtile.hpp +++ b/server/src/board/tile/rail/turnout/turnoutleftrailtile.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020-2022 Reinder Feenstra + * Copyright (C) 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 @@ -28,7 +28,7 @@ class TurnoutLeftRailTile : public TurnoutRailTile { protected: - TurnoutLeftRailTile(World& world, std::string_view _id, TileId tileId); + TurnoutLeftRailTile(World& world, std::string_view _id, TileId tileId_); }; #endif diff --git a/server/src/board/tile/rail/turnout/turnoutrailtile.cpp b/server/src/board/tile/rail/turnout/turnoutrailtile.cpp index 9606c139..93fc123b 100644 --- a/server/src/board/tile/rail/turnout/turnoutrailtile.cpp +++ b/server/src/board/tile/rail/turnout/turnoutrailtile.cpp @@ -26,16 +26,27 @@ #include "../../../../core/method.tpp" #include "../../../../world/world.hpp" #include "../../../../utils/displayname.hpp" +#include "../../../map/blockpath.hpp" +#include "../blockrailtile.hpp" +#include "../../../../train/trainblockstatus.hpp" +#include "../../../../train/train.hpp" +#include "../../../../log/log.hpp" -TurnoutRailTile::TurnoutRailTile(World& world, std::string_view _id, TileId tileId, size_t connectors) : - RailTile(world, _id, tileId), +TurnoutRailTile::TurnoutRailTile(World& world, std::string_view _id, TileId tileId_, size_t connectors) : + RailTile(world, _id, tileId_), m_node{*this, connectors}, name{this, "name", std::string(_id), PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly}, position{this, "position", TurnoutPosition::Unknown, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}, outputMap{this, "output_map", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject | PropertyFlags::NoScript}, - setPosition{*this, "set_position", MethodFlags::ScriptCallable, [this](TurnoutPosition value) { return doSetPosition(value); }} + setPosition{*this, "set_position", MethodFlags::ScriptCallable, [this](TurnoutPosition value) + { + TurnoutPosition reservedPosition = getReservedPosition(); + if(reservedPosition != TurnoutPosition::Unknown && reservedPosition != value) + return false; // Turnout is locked by reservation path + return doSetPosition(value); + }} { - assert(isRailTurnout(tileId)); + assert(isRailTurnout(tileId_)); const bool editable = contains(m_world.state.value(), WorldState::Edit); @@ -54,12 +65,23 @@ TurnoutRailTile::TurnoutRailTile(World& world, std::string_view _id, TileId tile // setPosition is added by sub class } -bool TurnoutRailTile::reserve(TurnoutPosition turnoutPosition, bool dryRun) +bool TurnoutRailTile::reserve(const std::shared_ptr &blockPath, TurnoutPosition turnoutPosition, bool dryRun) { if(!isValidPosition(turnoutPosition)) { return false; } + + const TurnoutPosition reservedPos = getReservedPosition(); + if(reservedPos != TurnoutPosition::Unknown && reservedPos != turnoutPosition) + { + // TODO: what if 2 path reserve same turnout for same position? + // Upon release one path it will make turnout free while it's still reserved by second path + + // Turnout is already reserved for another position + return false; + } + if(!dryRun) { if(!doSetPosition(turnoutPosition)) /*[[unlikely]]*/ @@ -67,6 +89,8 @@ bool TurnoutRailTile::reserve(TurnoutPosition turnoutPosition, bool dryRun) return false; } + m_reservedPath = blockPath; + RailTile::setReservedState(static_cast(turnoutPosition)); } return true; @@ -78,11 +102,30 @@ bool TurnoutRailTile::release(bool dryRun) if(!dryRun) { + m_reservedPath.reset(); + RailTile::release(); } return true; } +void TurnoutRailTile::destroying() +{ + outputMap->parentObject.setValueInternal(nullptr); + RailTile::destroying(); +} + +void TurnoutRailTile::addToWorld() +{ + outputMap->parentObject.setValueInternal(shared_from_this()); + RailTile::addToWorld(); +} + +TurnoutPosition TurnoutRailTile::getReservedPosition() const +{ + return static_cast(RailTile::reservedState()); +} + void TurnoutRailTile::worldEvent(WorldState state, WorldEvent event) { RailTile::worldEvent(state, event); @@ -116,7 +159,91 @@ void TurnoutRailTile::connectOutputMap() { outputMap->onOutputStateMatchFound.connect([this](TurnoutPosition pos) { - doSetPosition(pos, true); + bool changed = (pos != position); + if(!doSetPosition(pos, true) || !changed) + return; // No change + + TurnoutPosition reservedPosition = getReservedPosition(); + if(reservedPosition == TurnoutPosition::Unknown || reservedPosition == position.value()) + return; // Not locked + + // If turnout is inside a reserved path, force it to reserved position + // This corrects accidental modifications of position done + // by the user with an handset or command station. + Log::log(id, LogMessage::W3003_LOCKED_TURNOUT_CHANGED); + + if(m_world.correctOutputPosWhenLocked) + { + auto now = std::chrono::steady_clock::now(); + if((now - m_lastRetryStart) >= RETRY_DURATION) + { + // Reset retry count + m_lastRetryStart = now; + m_retryCount = 0; + } + + if(m_retryCount < MAX_RETRYCOUNT) + { + // Try to reset output to reseved state + m_retryCount++; + doSetPosition(reservedPosition, false); + + Log::log(id, LogMessage::N3003_TURNOUT_RESET_TO_RESERVED_POSITION); + return; + } + } + + // We reached maximum retry count + // We cannot lock this turnout. Take action. + switch (m_world.extOutputChangeAction.value()) + { + default: + case ExternalOutputChangeAction::DoNothing: + { + // Do nothing + break; + } + case ExternalOutputChangeAction::EmergencyStopTrain: + { + if(auto blockPath = m_reservedPath.lock()) + { + std::vector> alreadyStoppedTrains; + + for(auto it : blockPath->fromBlock().trains) + { + it->train.value()->emergencyStop.setValue(true); + alreadyStoppedTrains.push_back(it->train.value()); + Log::log(it->train->id, LogMessage::E3003_TRAIN_STOPPED_ON_TURNOUT_X_CHANGED, id.value()); + } + + auto toBlock = blockPath->toBlock(); + if(toBlock) + { + for(auto it : blockPath->toBlock()->trains) + { + if(std::find(alreadyStoppedTrains.cbegin(), alreadyStoppedTrains.cend(), it->train.value()) != alreadyStoppedTrains.cend()) + continue; // Do not stop train twice + + it->train.value()->emergencyStop.setValue(true); + Log::log(it->train->id, LogMessage::E3003_TRAIN_STOPPED_ON_TURNOUT_X_CHANGED, id.value()); + } + } + } + break; + } + case ExternalOutputChangeAction::EmergencyStopWorld: + { + m_world.stop(); + Log::log(m_world, LogMessage::E3007_WORLD_STOPPED_ON_TURNOUT_X_CHANGED, id.value()); + break; + } + case ExternalOutputChangeAction::PowerOffWorld: + { + m_world.powerOff(); + Log::log(m_world, LogMessage::E3009_WORLD_POWER_OFF_ON_TURNOUT_X_CHANGED, id.value()); + break; + } + } }); //TODO: disconnect somewhere? diff --git a/server/src/board/tile/rail/turnout/turnoutrailtile.hpp b/server/src/board/tile/rail/turnout/turnoutrailtile.hpp index 4ff6549c..ce244fc7 100644 --- a/server/src/board/tile/rail/turnout/turnoutrailtile.hpp +++ b/server/src/board/tile/rail/turnout/turnoutrailtile.hpp @@ -23,23 +23,34 @@ #ifndef TRAINTASTIC_SERVER_BOARD_TILE_RAIL_TURNOUT_TURNOUTRAILTILE_HPP #define TRAINTASTIC_SERVER_BOARD_TILE_RAIL_TURNOUT_TURNOUTRAILTILE_HPP +#include #include "../railtile.hpp" #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; + class TurnoutRailTile : public RailTile { DEFAULT_ID("turnout") private: Node m_node; + std::weak_ptr m_reservedPath; + + std::chrono::steady_clock::time_point m_lastRetryStart; + uint8_t m_retryCount; + static constexpr uint8_t MAX_RETRYCOUNT = 3; + static constexpr std::chrono::steady_clock::duration RETRY_DURATION = std::chrono::minutes(1); protected: - TurnoutRailTile(World& world, std::string_view _id, TileId tileId, size_t connectors); + TurnoutRailTile(World& world, std::string_view _id, TileId tileId_, size_t connectors); + void destroying() override; + void addToWorld() override; void worldEvent(WorldState state, WorldEvent event) override; bool isValidPosition(TurnoutPosition value); @@ -58,8 +69,10 @@ class TurnoutRailTile : public RailTile std::optional> node() const final { return m_node; } std::optional> node() final { return m_node; } - virtual bool reserve(TurnoutPosition turnoutPosition, bool dryRun = false); + virtual bool reserve(const std::shared_ptr& blockPath, TurnoutPosition turnoutPosition, bool dryRun = false); bool release(bool dryRun = false); + + TurnoutPosition getReservedPosition() const; }; #endif diff --git a/server/src/board/tile/rail/turnout/turnoutrightrailtile.cpp b/server/src/board/tile/rail/turnout/turnoutrightrailtile.cpp index da3ed740..38ae3ed2 100644 --- a/server/src/board/tile/rail/turnout/turnoutrightrailtile.cpp +++ b/server/src/board/tile/rail/turnout/turnoutrightrailtile.cpp @@ -63,8 +63,8 @@ static std::optional getDefaultActionValue(TurnoutPosition tu return {}; } -TurnoutRightRailTile::TurnoutRightRailTile(World& world, std::string_view _id, TileId tileId) - : TurnoutRailTile(world, _id, tileId, 3) +TurnoutRightRailTile::TurnoutRightRailTile(World& world, std::string_view _id, TileId tileId_) + : TurnoutRailTile(world, _id, tileId_, 3) { // Skip Unknown position tcb::span setPositionValues = tcb::make_span(positionValues).subspan<1>(); diff --git a/server/src/board/tile/rail/turnout/turnoutrightrailtile.hpp b/server/src/board/tile/rail/turnout/turnoutrightrailtile.hpp index ef73c90e..ef40aff6 100644 --- a/server/src/board/tile/rail/turnout/turnoutrightrailtile.hpp +++ b/server/src/board/tile/rail/turnout/turnoutrightrailtile.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020-2022 Reinder Feenstra + * Copyright (C) 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 @@ -28,7 +28,7 @@ class TurnoutRightRailTile : public TurnoutRailTile { protected: - TurnoutRightRailTile(World& world, std::string_view _id, TileId tileId); + TurnoutRightRailTile(World& world, std::string_view _id, TileId tileId_); }; #endif diff --git a/server/src/board/tile/rail/turnout/turnoutsinglesliprailtile.cpp b/server/src/board/tile/rail/turnout/turnoutsinglesliprailtile.cpp index 552f9842..9c5a7459 100644 --- a/server/src/board/tile/rail/turnout/turnoutsinglesliprailtile.cpp +++ b/server/src/board/tile/rail/turnout/turnoutsinglesliprailtile.cpp @@ -25,38 +25,40 @@ #include "../../../../core/objectproperty.tpp" #include "../../../../hardware/output/outputcontroller.hpp" -static const std::array positionValues = {TurnoutPosition::Unknown, - TurnoutPosition::Crossed, TurnoutPosition::Diverged, - TurnoutPosition::DoubleSlipStraightA, TurnoutPosition::DoubleSlipStraightB}; - -static std::optional getDefaultActionValue(TurnoutPosition turnoutPosition, OutputType outputType, size_t outputIndex) +namespace PositionValues { - // FIXME: implement defaults - (void)turnoutPosition; - (void)outputType; - (void)outputIndex; - return {}; + static const std::array singleMotor = { + TurnoutPosition::Unknown, + TurnoutPosition::Crossed, TurnoutPosition::Diverged + }; + static const std::array dualMotor = { + TurnoutPosition::Unknown, + TurnoutPosition::Diverged, TurnoutPosition::DoubleSlipStraightA, TurnoutPosition::DoubleSlipStraightB + }; } -TurnoutSingleSlipRailTile::TurnoutSingleSlipRailTile(World& world, std::string_view _id) - : TurnoutRailTile(world, _id, TileId::RailTurnoutSingleSlip, 4) -{ - // Skip Unknown position - tcb::span setPositionValues = tcb::make_span(positionValues).subspan<1>(); +static constexpr tcb::span positionValuesSingleMotor = tcb::make_span(PositionValues::singleMotor); +static constexpr tcb::span positionValuesDualMotor = tcb::make_span(PositionValues::dualMotor); +static constexpr tcb::span setPositionValuesSingleMotor = tcb::make_span(PositionValues::singleMotor).subspan<1>(); +static constexpr tcb::span setPositionValuesDualMotor = tcb::make_span(PositionValues::dualMotor).subspan<1>(); +TurnoutSingleSlipRailTile::TurnoutSingleSlipRailTile(World& world, std::string_view _id) + : TurnoutSlipRailTile(world, _id, TileId::RailTurnoutSingleSlip) +{ outputMap.setValueInternal(std::make_shared(*this, outputMap.name(), std::initializer_list{ TurnoutPosition::Crossed, TurnoutPosition::Diverged, TurnoutPosition::DoubleSlipStraightA, TurnoutPosition::DoubleSlipStraightB}, getDefaultActionValue)); - Attributes::addValues(position, positionValues); + Attributes::addValues(position, positionValuesSingleMotor); m_interfaceItems.add(position); - Attributes::addValues(setPosition, setPositionValues); + Attributes::addValues(setPosition, setPositionValuesSingleMotor); m_interfaceItems.add(setPosition); connectOutputMap(); + dualMotorChanged(); } void TurnoutSingleSlipRailTile::getConnectors(std::vector& connectors) const @@ -66,3 +68,21 @@ void TurnoutSingleSlipRailTile::getConnectors(std::vector& connectors connectors.emplace_back(location(), rotate + TileRotate::Deg180, Connector::Type::Rail); connectors.emplace_back(location(), rotate + TileRotate::Deg315, Connector::Type::Rail); } + +void TurnoutSingleSlipRailTile::dualMotorChanged() +{ + (*outputMap)[TurnoutPosition::Crossed]->visible.setValueInternal(!dualMotor); + (*outputMap)[TurnoutPosition::DoubleSlipStraightA]->visible.setValueInternal(dualMotor); + (*outputMap)[TurnoutPosition::DoubleSlipStraightB]->visible.setValueInternal(dualMotor); + + if(dualMotor) + { + Attributes::setValues(position, positionValuesDualMotor); + Attributes::setValues(setPosition, setPositionValuesDualMotor); + } + else + { + Attributes::setValues(position, positionValuesSingleMotor); + Attributes::setValues(setPosition, setPositionValuesSingleMotor); + } +} diff --git a/server/src/board/tile/rail/turnout/turnoutsinglesliprailtile.hpp b/server/src/board/tile/rail/turnout/turnoutsinglesliprailtile.hpp index d9f5fb48..ca56122c 100644 --- a/server/src/board/tile/rail/turnout/turnoutsinglesliprailtile.hpp +++ b/server/src/board/tile/rail/turnout/turnoutsinglesliprailtile.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020-2022 Reinder Feenstra + * Copyright (C) 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 @@ -23,13 +23,16 @@ #ifndef TRAINTASTIC_SERVER_BOARD_TILE_RAIL_TURNOUT_TURNOUTSINGLESLIPRAILTILE_HPP #define TRAINTASTIC_SERVER_BOARD_TILE_RAIL_TURNOUT_TURNOUTSINGLESLIPRAILTILE_HPP -#include "turnoutrailtile.hpp" +#include "turnoutsliprailtile.hpp" -class TurnoutSingleSlipRailTile : public TurnoutRailTile +class TurnoutSingleSlipRailTile : public TurnoutSlipRailTile { CLASS_ID("board_tile.rail.turnout_singleslip") CREATE(TurnoutSingleSlipRailTile) +protected: + void dualMotorChanged() final; + public: TurnoutSingleSlipRailTile(World& world, std::string_view _id); diff --git a/server/src/board/tile/rail/turnout/turnoutsliprailtile.cpp b/server/src/board/tile/rail/turnout/turnoutsliprailtile.cpp new file mode 100644 index 00000000..650f2db6 --- /dev/null +++ b/server/src/board/tile/rail/turnout/turnoutsliprailtile.cpp @@ -0,0 +1,77 @@ +/** + * server/src/board/tile/rail/turnout/turnoutsliprailtile.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 "turnoutsliprailtile.hpp" +#include "../../../../core/attributes.hpp" + +std::optional TurnoutSlipRailTile::getDefaultActionValue(TurnoutPosition turnoutPosition, OutputType outputType, size_t outputIndex) +{ + // FIXME: implement more defaults + switch(outputType) + { + case OutputType::Aspect: + if(outputIndex == 0) + { + // There is no official/defacto standard yet, until there is use values used by YaMoRC YD8116. + switch(turnoutPosition) + { + case TurnoutPosition::Left: + return static_cast(0); + + case TurnoutPosition::Right: + return static_cast(1); + + case TurnoutPosition::DoubleSlipStraightA: + return static_cast(17); + + case TurnoutPosition::DoubleSlipStraightB: + return static_cast(16); + + default: + break; + } + } + break; + + default: + break; + } + return {}; +} + +TurnoutSlipRailTile::TurnoutSlipRailTile(World& world, std::string_view _id, TileId tileId_) + : TurnoutRailTile(world, _id, tileId_, 4) + , dualMotor{this, "dual_motor", false, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly, + [this](bool /*value*/) + { + dualMotorChanged(); + }} +{ + Attributes::addDisplayName(dualMotor, "board_tile.rail.turnout_slip:dual_motor"); + m_interfaceItems.add(dualMotor); +} + +void TurnoutSlipRailTile::loaded() +{ + TurnoutRailTile::loaded(); + dualMotorChanged(); +} diff --git a/server/src/board/tile/rail/turnout/turnoutsliprailtile.hpp b/server/src/board/tile/rail/turnout/turnoutsliprailtile.hpp new file mode 100644 index 00000000..0da00c53 --- /dev/null +++ b/server/src/board/tile/rail/turnout/turnoutsliprailtile.hpp @@ -0,0 +1,43 @@ +/** + * server/src/board/tile/rail/turnout/turnoutsliprailtile.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_BOARD_TILE_RAIL_TURNOUT_TURNOUTSLIPRAILTILE_HPP +#define TRAINTASTIC_SERVER_BOARD_TILE_RAIL_TURNOUT_TURNOUTSLIPRAILTILE_HPP + +#include "turnoutrailtile.hpp" + +class TurnoutSlipRailTile : public TurnoutRailTile +{ +protected: + static std::optional getDefaultActionValue(TurnoutPosition turnoutPosition, OutputType outputType, size_t outputIndex); + + TurnoutSlipRailTile(World& world, std::string_view _id, TileId tileId_); + + virtual void loaded() override; + + virtual void dualMotorChanged() = 0; + +public: + Property dualMotor; +}; + +#endif diff --git a/server/src/board/tile/tile.cpp b/server/src/board/tile/tile.cpp index 37244602..4596ac71 100644 --- a/server/src/board/tile/tile.cpp +++ b/server/src/board/tile/tile.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020-2021,2023 Reinder Feenstra + * Copyright (C) 2020-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 @@ -27,15 +27,19 @@ #include "../boardlist.hpp" #include "../../world/world.hpp" -Tile::Tile(World& world, std::string_view _id, TileId tileId) +Tile::Tile(World& world, std::string_view _id, TileId tileId_) : IdObject(world, _id) - , m_tileId{tileId} + , tileId{this, "tile_id", tileId_, PropertyFlags::Constant | PropertyFlags::NoStore | PropertyFlags::NoScript} , x{this, "x", 0, PropertyFlags::ReadOnly | PropertyFlags::Store} , y{this, "y", 0, PropertyFlags::ReadOnly | PropertyFlags::Store} , rotate{this, "rotate", TileRotate::Deg0, PropertyFlags::ReadOnly | PropertyFlags::Store} , height{this, "height", 1, PropertyFlags::ReadOnly | PropertyFlags::Store} , width{this, "width", 1, PropertyFlags::ReadOnly | PropertyFlags::Store} { + Attributes::addObjectEditor(tileId, false); + Attributes::addValues(tileId, tcb::span{}); + m_interfaceItems.add(tileId); + Attributes::addObjectEditor(x, false); m_interfaceItems.add(x); @@ -55,6 +59,21 @@ Tile::Tile(World& world, std::string_view _id, TileId tileId) m_interfaceItems.add(width); } +std::optional Tile::getConnector(Connector::Direction direction) const +{ + std::vector connectors; + connectors.reserve(8); + getConnectors(connectors); + for(const auto& c : connectors) + { + if(c.direction == direction) + { + return c; + } + } + return std::nullopt; +} + Board& Tile::getBoard() { for(const auto& board : *m_world.boards) diff --git a/server/src/board/tile/tile.hpp b/server/src/board/tile/tile.hpp index fac6b847..7272f8a0 100644 --- a/server/src/board/tile/tile.hpp +++ b/server/src/board/tile/tile.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2020-2023 Reinder Feenstra + * Copyright (C) 2020-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,9 +39,7 @@ class Tile : public IdObject friend class WorldLoader; protected: - const TileId m_tileId; - - Tile(World& world, std::string_view _id, TileId tileId); + Tile(World& world, std::string_view _id, TileId tileId_); Board& getBoard(); @@ -57,19 +55,20 @@ class Tile : public IdObject public: static constexpr std::string_view defaultId = "tile"; + Property tileId; Property x; Property y; Property rotate; Property height; Property width; - TileId tileId() const { return m_tileId; } inline TileLocation location() const { return {x.value(), y.value()}; } - inline TileData data() const { return TileData{m_tileId, rotate, width, height, reservedState()}; } + inline TileData data() const { return TileData{tileId.value(), rotate, width, height, reservedState()}; } virtual std::optional> node() const { return {}; } virtual std::optional> node() { return {}; } virtual void getConnectors(std::vector& /*connectors*/) const {} + std::optional getConnector(Connector::Direction direction) const; }; #endif diff --git a/server/src/core/attributes.hpp b/server/src/core/attributes.hpp index 98e465a6..fbc9dd54 100644 --- a/server/src/core/attributes.hpp +++ b/server/src/core/attributes.hpp @@ -74,13 +74,21 @@ struct Attributes property.setAttribute(AttributeName::AliasValues, values); } + template + static inline void addAliases(UnitProperty& property, tcb::span keys, tcb::span values) + { + assert(keys.size() == values.size()); + property.addAttribute(AttributeName::AliasKeys, keys); + property.addAttribute(AttributeName::AliasValues, values); + } + static inline void addCategory(InterfaceItem& item, std::string_view value) { item.addAttribute(AttributeName::Category, value); } 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); } @@ -138,6 +146,20 @@ struct Attributes property.addAttribute(AttributeName::Max, convertUnit(max, unit, property.unit())); } + template + static inline void addMin(UnitProperty& property, T value) + { + static_assert(std::is_floating_point_v); + property.addAttribute(AttributeName::Min, value); + } + + template + static inline void setMin(UnitProperty& property, T value) + { + static_assert(std::is_floating_point_v); + property.setAttribute(AttributeName::Min, value); + } + template static inline void addMinMax(VectorProperty& property, T min, T max) { @@ -255,6 +277,12 @@ struct Attributes item.addAttribute(AttributeName::ObjectList, id); } + template + static inline void addUnit(Property& property, std::string_view unit) + { + property.addAttribute(AttributeName::Unit, unit); + } + template static inline void addValues(Method& method, const std::array& values) { @@ -273,6 +301,12 @@ struct Attributes property.addAttribute(AttributeName::Values, values); } + template + static inline void addValues(UnitProperty& property, tcb::span values) + { + property.addAttribute(AttributeName::Values, values); + } + template static inline void addValues(Property& property, const std::array& values) { @@ -315,6 +349,12 @@ struct Attributes property.setAttribute(AttributeName::Values, values); } + template + static inline void setValues(UnitProperty& property, tcb::span values) + { + property.setAttribute(AttributeName::Values, values); + } + template static inline void setValues(Property& property, const std::vector* values) { @@ -332,6 +372,12 @@ struct Attributes { method.setAttribute(AttributeName::Values, std::move(values)); } + + template + static inline void setValues(Method& method, tcb::span values) + { + method.setAttribute(AttributeName::Values, values); + } }; #endif diff --git a/server/src/core/eventloop.hpp b/server/src/core/eventloop.hpp index fc654126..0a52f1a5 100644 --- a/server/src/core/eventloop.hpp +++ b/server/src/core/eventloop.hpp @@ -48,6 +48,10 @@ class EventLoop #ifdef TRAINTASTIC_TEST threadId = std::this_thread::get_id(); #endif + if(ioContext.stopped()) + { + ioContext.restart(); + } auto work = std::make_shared(ioContext); ioContext.run(); } diff --git a/server/src/core/idobject.cpp b/server/src/core/idobject.cpp index 3ebb92b2..ac8c9c20 100644 --- a/server/src/core/idobject.cpp +++ b/server/src/core/idobject.cpp @@ -49,10 +49,7 @@ IdObject::IdObject(World& world, std::string_view _id) : m_interfaceItems.add(id); } -IdObject::~IdObject() -{ - //assert(m_world.expired()); // is destroy() called ?? -} +IdObject::~IdObject() = default; void IdObject::destroying() { 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/core/speedlimitproperty.cpp b/server/src/core/speedlimitproperty.cpp new file mode 100644 index 00000000..52d0a4b0 --- /dev/null +++ b/server/src/core/speedlimitproperty.cpp @@ -0,0 +1,110 @@ +/** + * server/src/core/speedlimitproperty.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 "speedlimitproperty.hpp" +#include +#include "attributes.hpp" + +constexpr std::array aliasKeys = {{SpeedLimitProperty::noLimitValue}}; +inline static const std::array aliasValues{{"$speed_limit_property:no_limit$"}}; + +static tcb::span getValues(SpeedUnit unit) +{ + switch(unit) + { + case SpeedUnit::KiloMeterPerHour: + { + static const std::array values = { + 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, + 200, 250, 300, + SpeedLimitProperty::noLimitValue + }; + return values; + } + case SpeedUnit::MilePerHour: + { + static const std::array values = { + 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, + 60, 70, 80, 90, 100, + 150, 200, + SpeedLimitProperty::noLimitValue + }; + return values; + } + case SpeedUnit::MeterPerSecond: + { + static const std::array values = { + 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, + 25, 30, 35, 40, 45, 50, + 60, 70, 80, + SpeedLimitProperty::noLimitValue + }; + return values; + } + } + return {}; +} + +SpeedLimitProperty::SpeedLimitProperty(Object& object, std::string_view name, double value, SpeedUnit unit, PropertyFlags flags) + : SpeedProperty(object, name, value, unit, flags, + [this](double /*value*/, SpeedUnit unit_) + { + if(m_attributeUnit != unit_) + { + updateAttributes(); + } + }) +{ + addAttributes(); +} + +SpeedLimitProperty::SpeedLimitProperty(Object& object, std::string_view name, double value, SpeedUnit unit, PropertyFlags flags, OnChanged onChanged) + : SpeedProperty(object, name, value, unit, flags, + [this, onChanged](double value_, SpeedUnit unit_) + { + if(m_attributeUnit != unit_) + { + updateAttributes(); + } + onChanged(value_, unit_); + }) +{ + assert(onChanged); + addAttributes(); +} + +void SpeedLimitProperty::addAttributes() +{ + auto values = getValues(unit()); + ::Attributes::addMin(*this, values.front()); + ::Attributes::addValues(*this, values); + ::Attributes::addAliases(*this, tcb::span(aliasKeys), tcb::span(aliasValues)); + m_attributeUnit = m_unit; +} + +void SpeedLimitProperty::updateAttributes() +{ + auto values = getValues(unit()); + ::Attributes::setMin(*this, values.front()); + ::Attributes::setValues(*this, values); + m_attributeUnit = m_unit; +} diff --git a/server/src/core/speedlimitproperty.hpp b/server/src/core/speedlimitproperty.hpp new file mode 100644 index 00000000..1a496eb5 --- /dev/null +++ b/server/src/core/speedlimitproperty.hpp @@ -0,0 +1,44 @@ +/** + * server/src/core/speedlimitproperty.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_CORE_SPEEDLIMITPROPERTY_HPP +#define TRAINTASTIC_SERVER_CORE_SPEEDLIMITPROPERTY_HPP + +#include "speedproperty.hpp" +#include + +class SpeedLimitProperty : public SpeedProperty +{ +private: + SpeedUnit m_attributeUnit; + + void addAttributes(); + void updateAttributes(); + +public: + static constexpr double noLimitValue = std::numeric_limits::infinity(); + + SpeedLimitProperty(Object& object, std::string_view name, double value, SpeedUnit unit, PropertyFlags flags); + SpeedLimitProperty(Object& object, std::string_view name, double value, SpeedUnit unit, PropertyFlags flags, OnChanged onChanged); +}; + +#endif diff --git a/server/src/core/to.hpp b/server/src/core/to.hpp index 20807755..fe8aa5a5 100644 --- a/server/src/core/to.hpp +++ b/server/src/core/to.hpp @@ -140,6 +140,22 @@ To to(const From& value) from_json(value, e); return e; } + else if constexpr(std::is_floating_point_v) + { + if(value == "Inf") + { + return std::numeric_limits::infinity(); + } + else if(value == "-Inf") + { + return -std::numeric_limits::infinity(); + } + else if(value == "NaN") + { + return std::numeric_limits::quiet_NaN();; + } + return value; + } else return value; } @@ -156,6 +172,18 @@ To to(const From& value) to_json(json, value); return json; } + else if constexpr(std::is_floating_point_v) + { + if(std::isinf(value)) + { + return (value > 0) ? "Inf" : "-Inf"; + } + else if(std::isnan(value)) + { + return "NaN"; + } + return value; + } else return value; } diff --git a/server/src/hardware/decoder/decoder.cpp b/server/src/hardware/decoder/decoder.cpp index c8f0e49a..a58fa301 100644 --- a/server/src/hardware/decoder/decoder.cpp +++ b/server/src/hardware/decoder/decoder.cpp @@ -193,10 +193,15 @@ void Decoder::loaded() bool Decoder::hasFunction(uint32_t number) const { - for(const auto& f : *functions) - if(f->number == number) - return true; - return false; + return std::any_of(functions->begin(), functions->end(), + [number](const auto& f) + { + return f->number == number; + }); + //for(const auto& f : *functions) + // if(f->number == number) + // return true; + //return false; } std::shared_ptr Decoder::getFunction(uint32_t number) const @@ -390,7 +395,7 @@ bool Decoder::checkProtocol() { const auto protocols = protocol.getSpanAttribute(AttributeName::Values).values(); assert(!protocols.empty()); - if(auto it = std::find(protocols.begin(), protocols.end(), protocol); it == protocols.end()) + if(const auto* it = std::find(protocols.begin(), protocols.end(), protocol); it == protocols.end()) { protocol = protocols.front(); return true; @@ -413,7 +418,7 @@ bool Decoder::checkAddress() bool Decoder::checkSpeedSteps() { const auto values = speedSteps.getSpanAttribute(AttributeName::Values).values(); - if(auto it = std::find(values.begin(), values.end(), speedSteps); it == values.end()) + if(const auto* it = std::find(values.begin(), values.end(), speedSteps); it == values.end()) { speedSteps = values.back(); return true; diff --git a/server/src/hardware/interface/dccexinterface.cpp b/server/src/hardware/interface/dccexinterface.cpp index bb864b9f..bc73115a 100644 --- a/server/src/hardware/interface/dccexinterface.cpp +++ b/server/src/hardware/interface/dccexinterface.cpp @@ -146,7 +146,7 @@ tcb::span DCCEXInterface::decoderSpeedSteps(DecoderProtocol proto assert(protocol == DecoderProtocol::DCCShort || protocol == DecoderProtocol::DCCLong); const auto& speedStepValues = DCCEX::Settings::speedStepValues; // find value in array so we can create a span, using a span of a variable won't work due to the compare with prevous value in the attribute setter - if(auto it = std::find(speedStepValues.begin(), speedStepValues.end(), dccex->speedSteps); it != speedStepValues.end()) /*[[likely]]/*/ + if(const auto it = std::find(speedStepValues.begin(), speedStepValues.end(), dccex->speedSteps); it != speedStepValues.end()) /*[[likely]]/*/ // NOLINT(readability-qualified-auto) windows requires const auto return {&(*it), 1}; assert(false); return {}; @@ -158,7 +158,7 @@ void DCCEXInterface::decoderChanged(const Decoder& decoder, DecoderChangeFlags c m_kernel->decoderChanged(decoder, changes, functionNumber); } -std::pair DCCEXInterface::inputAddressMinMax(uint32_t) const +std::pair DCCEXInterface::inputAddressMinMax(uint32_t /*channel*/) const { return {DCCEX::Kernel::idMin, DCCEX::Kernel::idMax}; } @@ -373,7 +373,7 @@ void DCCEXInterface::check() const checkDecoder(*decoder); } -void DCCEXInterface::checkDecoder(const Decoder& decoder) const +void DCCEXInterface::checkDecoder(const Decoder& decoder) { for(const auto& function : *decoder.functions) if(function->number > DCCEX::Config::functionNumberMax) diff --git a/server/src/hardware/interface/dccexinterface.hpp b/server/src/hardware/interface/dccexinterface.hpp index e6be46e7..9439b931 100644 --- a/server/src/hardware/interface/dccexinterface.hpp +++ b/server/src/hardware/interface/dccexinterface.hpp @@ -60,7 +60,7 @@ class DCCEXInterface final void worldEvent(WorldState state, WorldEvent event) final; void check() const; - void checkDecoder(const Decoder& decoder) const; + static void checkDecoder(const Decoder& decoder); void updateEnabled(); 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/interface/loconetinterface.cpp b/server/src/hardware/interface/loconetinterface.cpp index 16d8eb8b..c68245aa 100644 --- a/server/src/hardware/interface/loconetinterface.cpp +++ b/server/src/hardware/interface/loconetinterface.cpp @@ -155,7 +155,7 @@ void LocoNetInterface::decoderChanged(const Decoder& decoder, DecoderChangeFlags m_kernel->decoderChanged(decoder, changes, functionNumber); } -std::pair LocoNetInterface::inputAddressMinMax(uint32_t) const +std::pair LocoNetInterface::inputAddressMinMax(uint32_t /*channel*/) const { return {LocoNet::Kernel::inputAddressMin, LocoNet::Kernel::inputAddressMax}; } @@ -189,7 +189,7 @@ bool LocoNetInterface::setOutputValue(OutputChannel channel, uint32_t address, O m_kernel->setOutput(channel, static_cast(address), value); } -std::pair LocoNetInterface::identificationAddressMinMax(uint32_t) const +std::pair LocoNetInterface::identificationAddressMinMax(uint32_t /*channel*/) const { return {LocoNet::Kernel::identificationAddressMin, LocoNet::Kernel::identificationAddressMax}; } diff --git a/server/src/hardware/interface/traintasticdiyinterface.cpp b/server/src/hardware/interface/traintasticdiyinterface.cpp index 839ce605..24abbcbb 100644 --- a/server/src/hardware/interface/traintasticdiyinterface.cpp +++ b/server/src/hardware/interface/traintasticdiyinterface.cpp @@ -102,7 +102,7 @@ TraintasticDIYInterface::TraintasticDIYInterface(World& world, std::string_view updateVisible(); } -std::pair TraintasticDIYInterface::inputAddressMinMax(uint32_t) const +std::pair TraintasticDIYInterface::inputAddressMinMax(uint32_t /*channel*/) const { return {TraintasticDIY::Kernel::ioAddressMin, TraintasticDIY::Kernel::ioAddressMax}; } diff --git a/server/src/hardware/interface/xpressnetinterface.cpp b/server/src/hardware/interface/xpressnetinterface.cpp index b89a89fc..2651caed 100644 --- a/server/src/hardware/interface/xpressnetinterface.cpp +++ b/server/src/hardware/interface/xpressnetinterface.cpp @@ -200,7 +200,7 @@ void XpressNetInterface::decoderChanged(const Decoder& decoder, DecoderChangeFla m_kernel->decoderChanged(decoder, changes, functionNumber); } -std::pair XpressNetInterface::inputAddressMinMax(uint32_t) const +std::pair XpressNetInterface::inputAddressMinMax(uint32_t /*channel*/) const { return {XpressNet::Kernel::inputAddressMin, XpressNet::Kernel::inputAddressMax}; } diff --git a/server/src/hardware/output/keyboard/pairoutputkeyboard.cpp b/server/src/hardware/output/keyboard/pairoutputkeyboard.cpp index ef287d26..01907065 100644 --- a/server/src/hardware/output/keyboard/pairoutputkeyboard.cpp +++ b/server/src/hardware/output/keyboard/pairoutputkeyboard.cpp @@ -41,7 +41,7 @@ PairOutputKeyboard::PairOutputKeyboard(OutputController& controller, OutputChann std::vector PairOutputKeyboard::getOutputInfo() const { std::vector states; - for(auto& it : m_controller.outputMap()) + for(const auto& it : m_controller.outputMap()) { if(it.second->channel == channel) { diff --git a/server/src/hardware/output/keyboard/singleoutputkeyboard.cpp b/server/src/hardware/output/keyboard/singleoutputkeyboard.cpp index d4a25f5b..c01d0784 100644 --- a/server/src/hardware/output/keyboard/singleoutputkeyboard.cpp +++ b/server/src/hardware/output/keyboard/singleoutputkeyboard.cpp @@ -41,7 +41,7 @@ SingleOutputKeyboard::SingleOutputKeyboard(OutputController& controller, OutputC std::vector SingleOutputKeyboard::getOutputInfo() const { std::vector states; - for(auto& it : m_controller.outputMap()) + for(const auto& it : m_controller.outputMap()) { if(it.second->channel == channel) { diff --git a/server/src/hardware/output/list/outputlisttablemodel.cpp b/server/src/hardware/output/list/outputlisttablemodel.cpp index 08464cbb..97518cfa 100644 --- a/server/src/hardware/output/list/outputlisttablemodel.cpp +++ b/server/src/hardware/output/list/outputlisttablemodel.cpp @@ -96,7 +96,7 @@ std::string OutputListTableModel::getText(uint32_t column, uint32_t row) const break; case OutputListColumn::Address: - if(auto* addressOutput = dynamic_cast(&output)) + if(const auto* addressOutput = dynamic_cast(&output)) { return std::to_string(addressOutput->address.value()); } diff --git a/server/src/hardware/output/map/outputmap.cpp b/server/src/hardware/output/map/outputmap.cpp index 02852018..b72ddcab 100644 --- a/server/src/hardware/output/map/outputmap.cpp +++ b/server/src/hardware/output/map/outputmap.cpp @@ -39,8 +39,22 @@ #include "../../../utils/displayname.hpp" #include "../../../utils/inrange.hpp" +namespace +{ + +template +void swap(Property& a, Property& b) +{ + T tmp = a; + a = b.value(); + b = tmp; +} + +} + OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) : SubObject(_parent, parentPropertyName) + , parentObject{this, "parent", nullptr, PropertyFlags::Constant | PropertyFlags::NoStore | PropertyFlags::NoScript} , interface{this, "interface", nullptr, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::NoScript, [this](const std::shared_ptr& /*newValue*/) { @@ -71,7 +85,7 @@ OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) { // New interface doesn't support channel or channel has different output type. const auto channels = newValue->outputChannels(); - auto it = std::find_if(channels.begin(), channels.end(), + const auto* it = std::find_if(channels.begin(), channels.end(), [&controller=*newValue, outputType=interface->outputType(channel)](OutputChannel outputChannel) { return controller.outputType(outputChannel) == outputType; @@ -283,10 +297,33 @@ OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) addressesSizeChanged(); } }} + , swapOutputs{*this, "swap_outputs", MethodFlags::NoScript, + [this]() + { + if(!interface) + { + return; + } + + switch(interface->outputType(channel)) + { + case OutputType::Pair: + if(m_outputs.size() == 1 && items.size() == 2) + { + swap(static_cast(*items[0]->outputActions[0]).action, static_cast(*items[1]->outputActions[0]).action); + } + break; + + default: + break; + } + }} { auto& world = getWorld(&_parent); const bool editable = contains(world.state.value(), WorldState::Edit); + m_interfaceItems.add(parentObject); + Attributes::addDisplayName(interface, DisplayName::Hardware::interface); Attributes::addEnabled(interface, editable); Attributes::addObjectList(interface, world.outputControllers); @@ -323,6 +360,10 @@ OutputMap::OutputMap(Object& _parent, std::string_view parentPropertyName) Attributes::addVisible(removeAddress, false); m_interfaceItems.add(removeAddress); + Attributes::addDisplayName(swapOutputs, "output_map:swap_outputs"); + Attributes::addVisible(swapOutputs, false); + m_interfaceItems.add(swapOutputs); + updateEnabled(); } @@ -506,6 +547,17 @@ void OutputMap::updateOutputActions(OutputType outputType) assert(m_outputs.size() == item->outputActions.size()); } + + switch(outputType) + { + case OutputType::Pair: + Attributes::setVisible(swapOutputs, m_outputs.size() == 1 && items.size() == 2); + break; + + default: + Attributes::setVisible(swapOutputs, false); + break; + } } void OutputMap::updateEnabled() @@ -596,7 +648,7 @@ int OutputMap::getMatchingActionOnCurrentState() { return i; // We got a full match } - else if(value == OutputMapItem::MatchResult::WildcardMatch) + if(value == OutputMapItem::MatchResult::WildcardMatch) { // We give wildcard matches a lower priority. // Save it for later, in the meantime we check for a better full match diff --git a/server/src/hardware/output/map/outputmap.hpp b/server/src/hardware/output/map/outputmap.hpp index 1305c311..4ac1df2f 100644 --- a/server/src/hardware/output/map/outputmap.hpp +++ b/server/src/hardware/output/map/outputmap.hpp @@ -82,6 +82,7 @@ class OutputMap : public SubObject virtual void updateStateFromOutput(); public: + ObjectProperty parentObject; // UI needs access to parent object ObjectProperty interface; Property channel; VectorProperty addresses; @@ -89,6 +90,7 @@ class OutputMap : public SubObject ObjectVectorProperty items; Method addAddress; Method removeAddress; + Method swapOutputs; OutputMap(Object& _parent, std::string_view parentPropertyName); ~OutputMap() override; diff --git a/server/src/hardware/output/map/outputmapaspectoutputaction.hpp b/server/src/hardware/output/map/outputmapaspectoutputaction.hpp index e8ea0d3c..6b68bc69 100644 --- a/server/src/hardware/output/map/outputmapaspectoutputaction.hpp +++ b/server/src/hardware/output/map/outputmapaspectoutputaction.hpp @@ -41,7 +41,7 @@ class OutputMapAspectOutputAction final : public OutputMapOutputAction public: Property aspect; - OutputMapAspectOutputAction(OutputMap& _parent, size_t outputIndex); + OutputMapAspectOutputAction(OutputMap& parent_, size_t outputIndex); void execute() final; diff --git a/server/src/hardware/output/map/outputmapecosstateoutputaction.hpp b/server/src/hardware/output/map/outputmapecosstateoutputaction.hpp index d1ee6264..86461c15 100644 --- a/server/src/hardware/output/map/outputmapecosstateoutputaction.hpp +++ b/server/src/hardware/output/map/outputmapecosstateoutputaction.hpp @@ -41,7 +41,7 @@ class OutputMapECoSStateOutputAction final : public OutputMapOutputAction public: Property state; - OutputMapECoSStateOutputAction(OutputMap& _parent, size_t outputIndex); + OutputMapECoSStateOutputAction(OutputMap& parent_, size_t outputIndex); void execute() final; diff --git a/server/src/hardware/output/map/outputmapitem.cpp b/server/src/hardware/output/map/outputmapitem.cpp index 28af9ee7..acf796bf 100644 --- a/server/src/hardware/output/map/outputmapitem.cpp +++ b/server/src/hardware/output/map/outputmapitem.cpp @@ -79,7 +79,7 @@ OutputMapItem::MatchResult OutputMapItem::matchesCurrentOutputState() const void OutputMapItem::worldEvent(WorldState state, WorldEvent event) { Object::worldEvent(state, event); - for(auto& action : outputActions) + for(const auto& action : outputActions) { action->worldEvent(state, event); } diff --git a/server/src/hardware/output/map/outputmapitembase.hpp b/server/src/hardware/output/map/outputmapitembase.hpp index 0fe79e33..85c54076 100644 --- a/server/src/hardware/output/map/outputmapitembase.hpp +++ b/server/src/hardware/output/map/outputmapitembase.hpp @@ -40,7 +40,6 @@ class OutputMapItemBase : public OutputMapItem public: Property key; - Property use; Method (uint32_t)> getOutputAction; OutputMapItemBase(Object& map, Key _key); diff --git a/server/src/hardware/output/map/outputmapitembase.tpp b/server/src/hardware/output/map/outputmapitembase.tpp index e5166214..b32f4dbd 100644 --- a/server/src/hardware/output/map/outputmapitembase.tpp +++ b/server/src/hardware/output/map/outputmapitembase.tpp @@ -1,3 +1,25 @@ +/** + * server/src/hardware/output/map/outputmapitembase.tpp + * + * This file is part of the traintastic source code. + * + * 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 + * 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_HARDWARE_OUTPUT_MAP_OUTPUTMAPITEMBASE_TPP #define TRAINTASTIC_SERVER_HARDWARE_OUTPUT_MAP_OUTPUTMAPITEMBASE_TPP @@ -12,7 +34,6 @@ OutputMapItemBase::OutputMapItemBase(Object& map, Key _key) : OutputMapItem(map) , m_keyValues{{_key}} , key{this, "key", _key, PropertyFlags::ReadOnly | PropertyFlags::Store} - , use{this, "use", true, PropertyFlags::ReadWrite | PropertyFlags::Store} , getOutputAction{*this, "get_output_action", [this](uint32_t index) { @@ -21,8 +42,7 @@ OutputMapItemBase::OutputMapItemBase(Object& map, Key _key) { Attributes::addValues(key, m_keyValues); m_interfaceItems.add(key); - Attributes::addEnabled(use, false); - m_interfaceItems.add(use); + m_interfaceItems.add(getOutputAction); } diff --git a/server/src/hardware/output/map/outputmappairoutputaction.hpp b/server/src/hardware/output/map/outputmappairoutputaction.hpp index 4fdd216e..5b6e2d65 100644 --- a/server/src/hardware/output/map/outputmappairoutputaction.hpp +++ b/server/src/hardware/output/map/outputmappairoutputaction.hpp @@ -42,7 +42,7 @@ class OutputMapPairOutputAction final : public OutputMapOutputAction public: Property action; - OutputMapPairOutputAction(OutputMap& _parent, size_t outputIndex); + OutputMapPairOutputAction(OutputMap& parent_, size_t outputIndex); void execute() final; diff --git a/server/src/hardware/output/map/outputmapsingleoutputaction.hpp b/server/src/hardware/output/map/outputmapsingleoutputaction.hpp index 2b2bff30..52f68d5b 100644 --- a/server/src/hardware/output/map/outputmapsingleoutputaction.hpp +++ b/server/src/hardware/output/map/outputmapsingleoutputaction.hpp @@ -42,7 +42,7 @@ class OutputMapSingleOutputAction final : public OutputMapOutputAction public: Property action; - OutputMapSingleOutputAction(OutputMap& _parent, size_t outputIndex); + OutputMapSingleOutputAction(OutputMap& parent_, size_t outputIndex); void execute() final; diff --git a/server/src/hardware/output/map/signaloutputmapitem.cpp b/server/src/hardware/output/map/signaloutputmapitem.cpp index efb48595..00d150a5 100644 --- a/server/src/hardware/output/map/signaloutputmapitem.cpp +++ b/server/src/hardware/output/map/signaloutputmapitem.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2021 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 @@ -25,6 +25,8 @@ SignalOutputMapItem::SignalOutputMapItem(Object& map, SignalAspect aspect) : OutputMapItemBase(map, aspect) + , use{this, "use", true, PropertyFlags::ReadWrite | PropertyFlags::Store} { - Attributes::setEnabled(use, !isRequiredSignalAspect(aspect)); + Attributes::addEnabled(use, !isRequiredSignalAspect(aspect)); + m_interfaceItems.add(use); } diff --git a/server/src/hardware/output/map/signaloutputmapitem.hpp b/server/src/hardware/output/map/signaloutputmapitem.hpp index 7d8874f8..a51040a5 100644 --- a/server/src/hardware/output/map/signaloutputmapitem.hpp +++ b/server/src/hardware/output/map/signaloutputmapitem.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2021 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 @@ -32,6 +32,8 @@ class SignalOutputMapItem : public OutputMapItemBase CLASS_ID("output_map_item.signal") public: + Property use; + SignalOutputMapItem(Object& map, SignalAspect aspect); }; diff --git a/server/src/hardware/output/map/turnoutoutputmapitem.cpp b/server/src/hardware/output/map/turnoutoutputmapitem.cpp index 879d4de4..eee8749b 100644 --- a/server/src/hardware/output/map/turnoutoutputmapitem.cpp +++ b/server/src/hardware/output/map/turnoutoutputmapitem.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2021 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 @@ -26,5 +26,7 @@ TurnoutOutputMapItem::TurnoutOutputMapItem(Object& map, TurnoutPosition position) : OutputMapItemBase(map, position) + , visible{this, "visible", true, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::NoScript} { + m_interfaceItems.add(visible); } diff --git a/server/src/hardware/output/map/turnoutoutputmapitem.hpp b/server/src/hardware/output/map/turnoutoutputmapitem.hpp index 63165316..be482065 100644 --- a/server/src/hardware/output/map/turnoutoutputmapitem.hpp +++ b/server/src/hardware/output/map/turnoutoutputmapitem.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2021 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,7 +24,7 @@ #define TRAINTASTIC_SERVER_HARDWARE_OUTPUT_MAP_TURNOUTOUTPUTMAPITEM_HPP #include "outputmapitembase.hpp" -#include "../../../enum/turnoutposition.hpp" +#include class TurnoutOutputMapItem : public OutputMapItemBase { @@ -32,6 +32,8 @@ class TurnoutOutputMapItem : public OutputMapItemBase public: TurnoutOutputMapItem(Object& map, TurnoutPosition position); + + Property visible; }; #endif diff --git a/server/src/hardware/output/outputcontroller.cpp b/server/src/hardware/output/outputcontroller.cpp index 9a48f482..cb7f2b8b 100644 --- a/server/src/hardware/output/outputcontroller.cpp +++ b/server/src/hardware/output/outputcontroller.cpp @@ -34,6 +34,7 @@ #include "../../core/attributes.hpp" #include "../../core/controllerlist.hpp" #include "../../core/objectproperty.tpp" +#include "../../utils/contains.hpp" #include "../../utils/displayname.hpp" #include "../../utils/inrange.hpp" #include "../../world/getworld.hpp" @@ -100,16 +101,7 @@ std::pair, tcb::span> OutputControl bool OutputController::isOutputChannel(OutputChannel channel) const { - const auto channels = outputChannels(); - // FIXME: return std::find(channels.begin(), channels.end(), channel) != channels.end(); - for(auto ch : channels) - { - if(channel == ch) - { - return true; - } - } - return false; + return contains(outputChannels(), channel); } bool OutputController::isOutputId(OutputChannel channel, uint32_t id) const diff --git a/server/src/hardware/protocol/ecos/kernel.cpp b/server/src/hardware/protocol/ecos/kernel.cpp index ebcb88af..a8442f42 100644 --- a/server/src/hardware/protocol/ecos/kernel.cpp +++ b/server/src/hardware/protocol/ecos/kernel.cpp @@ -184,7 +184,7 @@ void Kernel::stop(Simulation* simulation) m_thread.join(); - if(simulation) // get simulation data + if(simulation && !m_objects.empty()) // get simulation data { simulation->clear(); @@ -625,7 +625,7 @@ void Kernel::objectChanged(Object& object) } std::string objectName; - if(auto* sw = dynamic_cast(&object)) + if(const auto* sw = dynamic_cast(&object)) { objectName = sw->nameUI(); } diff --git a/server/src/hardware/protocol/ecos/object/object.cpp b/server/src/hardware/protocol/ecos/object/object.cpp index f62df460..8e8ce5d4 100644 --- a/server/src/hardware/protocol/ecos/object/object.cpp +++ b/server/src/hardware/protocol/ecos/object/object.cpp @@ -45,8 +45,8 @@ bool Object::receiveReply(const Reply& reply) { std::string_view key; std::string_view value; - for(auto option : reply.options) - if(parseOptionValue(option, key, value)) + for(auto text : reply.options) + if(parseOptionValue(text, key, value)) update(key, value); } diff --git a/server/src/hardware/protocol/loconet/checksum.cpp b/server/src/hardware/protocol/loconet/checksum.cpp index febeaa60..5d253297 100644 --- a/server/src/hardware/protocol/loconet/checksum.cpp +++ b/server/src/hardware/protocol/loconet/checksum.cpp @@ -27,7 +27,7 @@ namespace LocoNet { uint8_t calcChecksum(const Message& message) { - const uint8_t* p = reinterpret_cast(&message); + const auto* p = reinterpret_cast(&message); const int size = message.size() - 1; uint8_t checksum = 0xFF; for(int i = 0; i < size; i++) diff --git a/server/src/hardware/protocol/loconet/iohandler/lbserveriohandler.cpp b/server/src/hardware/protocol/loconet/iohandler/lbserveriohandler.cpp index 356fb6a5..87a3663e 100644 --- a/server/src/hardware/protocol/loconet/iohandler/lbserveriohandler.cpp +++ b/server/src/hardware/protocol/loconet/iohandler/lbserveriohandler.cpp @@ -119,7 +119,7 @@ void LBServerIOHandler::read() if(startsWith(line, "RECEIVE ")) { std::vector bytes = readHexBytes(line.substr(8)); - const Message* message = reinterpret_cast(bytes.data()); + const auto* message = reinterpret_cast(bytes.data()); if(isValid(*message)) m_kernel.receive(*message); } diff --git a/server/src/hardware/protocol/loconet/iohandler/serialiohandler.cpp b/server/src/hardware/protocol/loconet/iohandler/serialiohandler.cpp index 0b149ed5..40c42ea8 100644 --- a/server/src/hardware/protocol/loconet/iohandler/serialiohandler.cpp +++ b/server/src/hardware/protocol/loconet/iohandler/serialiohandler.cpp @@ -86,7 +86,7 @@ void SerialIOHandler::read() while(bytesTransferred > 1) { - const Message* message = reinterpret_cast(pos); + const auto* message = reinterpret_cast(pos); size_t drop = 0; while((message->size() == 0 || (message->size() <= bytesTransferred && !isValid(*message))) && bytesTransferred > 0) diff --git a/server/src/hardware/protocol/loconet/iohandler/tcpbinaryiohandler.cpp b/server/src/hardware/protocol/loconet/iohandler/tcpbinaryiohandler.cpp index 000c1d7e..53b15736 100644 --- a/server/src/hardware/protocol/loconet/iohandler/tcpbinaryiohandler.cpp +++ b/server/src/hardware/protocol/loconet/iohandler/tcpbinaryiohandler.cpp @@ -63,7 +63,7 @@ void TCPBinaryIOHandler::read() while(bytesTransferred > 1) { - const Message* message = reinterpret_cast(pos); + const auto* message = reinterpret_cast(pos); size_t drop = 0; while((message->size() == 0 || (message->size() <= bytesTransferred && !isValid(*message))) && bytesTransferred > 0) diff --git a/server/src/hardware/protocol/loconet/iohandler/z21iohandler.cpp b/server/src/hardware/protocol/loconet/iohandler/z21iohandler.cpp index f40ca70d..a7f137b0 100644 --- a/server/src/hardware/protocol/loconet/iohandler/z21iohandler.cpp +++ b/server/src/hardware/protocol/loconet/iohandler/z21iohandler.cpp @@ -126,7 +126,7 @@ void Z21IOHandler::receive() case Z21::LAN_LOCONET_Z21_TX: case Z21::LAN_LOCONET_FROM_LAN: { - const Message* msg = reinterpret_cast(pos + sizeof(Z21::Message)); + const auto* msg = reinterpret_cast(pos + sizeof(Z21::Message)); if(isValid(*msg)) m_kernel.receive(*msg); break; diff --git a/server/src/hardware/protocol/loconet/kernel.cpp b/server/src/hardware/protocol/loconet/kernel.cpp index 69c7f42b..c0a3651e 100644 --- a/server/src/hardware/protocol/loconet/kernel.cpp +++ b/server/src/hardware/protocol/loconet/kernel.cpp @@ -55,7 +55,7 @@ static void updateDecoderSpeed(const std::shared_ptr& decoder, uint8_t else { speed--; // decrement one for ESTOP: 2..127 -> 1..126 - const uint8_t currentStep = Decoder::throttleToSpeedStep(decoder->throttle.value(), SPEED_MAX - 1); + const auto currentStep = Decoder::throttleToSpeedStep(decoder->throttle.value(), SPEED_MAX - 1); if(currentStep != speed) // only update trottle if it is a different step decoder->throttle.setValueInternal(Decoder::speedStepToThrottle(speed, SPEED_MAX - 1)); } @@ -378,7 +378,7 @@ void Kernel::receive(const Message& message) case OPC_LOCO_SPD: if(m_decoderController) { - const LocoSpd& locoSpd = static_cast(message); + const auto& locoSpd = static_cast(message); if(LocoSlot* slot = getLocoSlot(locoSpd.slot)) { if(!slot->isAddressValid()) @@ -692,7 +692,7 @@ void Kernel::receive(const Message& message) case OPC_D4: if(m_decoderController) { - const uint8_t* bytes = reinterpret_cast(&message); + const auto* bytes = reinterpret_cast(&message); if(bytes[1] == 0x20) { switch(bytes[3]) @@ -765,7 +765,7 @@ void Kernel::receive(const Message& message) case OPC_MULTI_SENSE_LONG: { - const MultiSenseLong& multiSense = static_cast(message); + const auto& multiSense = static_cast(message); if(multiSense.code() == MultiSenseLong::Code::ReleaseTransponder || multiSense.code() == MultiSenseLong::Code::DetectTransponder) { EventLoop::call( @@ -979,7 +979,7 @@ void Kernel::decoderChanged(const Decoder& decoder, DecoderChangeFlags changes, if(has(changes, DecoderChangeFlags::EmergencyStop | DecoderChangeFlags::Throttle)) { - const uint8_t speedStep = Decoder::throttleToSpeedStep(decoder.throttle, SPEED_MAX - 1); + const auto speedStep = Decoder::throttleToSpeedStep(decoder.throttle, SPEED_MAX - 1); if(m_emergencyStop == TriState::False || decoder.emergencyStop || speedStep == SPEED_STOP) { // only send speed updates if bus estop isn't active, except for speed STOP and ESTOP @@ -1365,7 +1365,7 @@ void Kernel::send(uint16_t address, Message& message, uint8_t& slot) } else // try get a slot { - std::byte* ptr = reinterpret_cast(&message); + auto* ptr = reinterpret_cast(&message); auto pendingSlotMessage = m_pendingSlotMessages.find(address); if(pendingSlotMessage == m_pendingSlotMessages.end()) @@ -1393,7 +1393,7 @@ void Kernel::sendNextMessage() if(m_ioHandler->send(message)) { - m_sentMessagePriority = static_cast(priority); + m_sentMessagePriority = priority; m_waitingForEcho = true; m_waitingForEchoTimer.expires_after(boost::asio::chrono::milliseconds(m_config.echoTimeout)); diff --git a/server/src/hardware/protocol/loconet/messages.cpp b/server/src/hardware/protocol/loconet/messages.cpp index 8e6f7dcd..3acf5963 100644 --- a/server/src/hardware/protocol/loconet/messages.cpp +++ b/server/src/hardware/protocol/loconet/messages.cpp @@ -61,7 +61,7 @@ void setSlot(Message& message, uint8_t slot) case OPC_D4: { - uint8_t* bytes = reinterpret_cast(&message); + auto* bytes = reinterpret_cast(&message); if(bytes[1] == 0x20 && (bytes[3] == 0x08 || bytes[3] == 0x05 || bytes[3] == 0x09)) // LocoF13F19 or LocoF12F20F28 or LocoF21F27 { bytes[2] = slot; @@ -235,14 +235,14 @@ std::string toString(const Message& message) case OPC_LOCO_SPD: { - const LocoSpd& locoSpd = static_cast(message); + const auto& locoSpd = static_cast(message); s.append(" slot=").append(std::to_string(locoSpd.slot)); s.append(" speed=").append(std::to_string(locoSpd.speed)); break; } case OPC_LOCO_DIRF: { - const LocoDirF& locoDirF = static_cast(message); + const auto& locoDirF = static_cast(message); s.append(" slot=").append(std::to_string(locoDirF.slot)); s.append(" dir=").append(locoDirF.direction() == Direction::Forward ? "fwd" : "rev"); s.append(" f0=").append(locoDirF.f0() ? "on" : "off"); @@ -254,7 +254,7 @@ std::string toString(const Message& message) } case OPC_LOCO_SND: { - const LocoSnd& locoSnd = static_cast(message); + const auto& locoSnd = static_cast(message); s.append(" slot=").append(std::to_string(locoSnd.slot)); s.append(" f5=").append(locoSnd.f5() ? "on" : "off"); s.append(" f6=").append(locoSnd.f6() ? "on" : "off"); @@ -264,7 +264,7 @@ std::string toString(const Message& message) } case OPC_LOCO_F9F12: { - const LocoF9F12& locoF9F12 = static_cast(message); + const auto& locoF9F12 = static_cast(message); s.append(" slot=").append(std::to_string(locoF9F12.slot)); s.append(" f9=").append(locoF9F12.f9() ? "on" : "off"); s.append(" f10=").append(locoF9F12.f10() ? "on" : "off"); @@ -274,7 +274,7 @@ std::string toString(const Message& message) } case OPC_INPUT_REP: { - const InputRep& inputRep = static_cast(message); + const auto& inputRep = static_cast(message); s.append(" fullAddress=").append(std::to_string(inputRep.fullAddress())); s.append(" address=").append(std::to_string(inputRep.address())); s.append(" input=").append(inputRep.isAuxInput() ? "aux" : "switch"); @@ -283,7 +283,7 @@ std::string toString(const Message& message) } case OPC_SW_REQ: { - const SwitchRequest& switchRequest = static_cast(message); + const auto& switchRequest = static_cast(message); s.append(" address=").append(std::to_string(switchRequest.address())); s.append(" dir=").append(switchRequest.dir() ? "closed/green" : "thrown/red"); s.append(" on=").append(switchRequest.on() ? "high" : "low"); @@ -291,16 +291,16 @@ std::string toString(const Message& message) } case OPC_RQ_SL_DATA: { - const RequestSlotData& requestSlotData = static_cast(message); + const auto& requestSlotData = static_cast(message); s.append(" slot=").append(std::to_string(requestSlotData.slot)); break; } case OPC_MULTI_SENSE: { - const MultiSense& multiSense = static_cast(message); + const auto& multiSense = static_cast(message); if(multiSense.isTransponder()) { - const MultiSenseTransponder& multiSenseTransponder = static_cast(multiSense); + const auto& multiSenseTransponder = static_cast(multiSense); s.append(multiSenseTransponder.isPresent() ? " present" : " absent"); s.append(" sensorAddress=").append(std::to_string(multiSenseTransponder.sensorAddress())); s.append(" transponderAddress=").append(std::to_string(multiSenseTransponder.transponderAddress())); @@ -309,14 +309,14 @@ std::string toString(const Message& message) } case OPC_D4: { - const uint8_t* bytes = reinterpret_cast(&message); + const auto* bytes = reinterpret_cast(&message); if(bytes[1] == 0x20) { switch(bytes[3]) { case 0x08: { - const LocoF13F19& locoF13F19 = static_cast(message); + const auto& locoF13F19 = static_cast(message); s.append(" slot=").append(std::to_string(locoF13F19.slot)); s.append(" f13=").append(locoF13F19.f13() ? "on" : "off"); s.append(" f14=").append(locoF13F19.f14() ? "on" : "off"); @@ -338,7 +338,7 @@ std::string toString(const Message& message) } case 0x09: { - const LocoF21F27& locoF21F27 = static_cast(message); + const auto& locoF21F27 = static_cast(message); s.append(" slot=").append(std::to_string(locoF21F27.slot)); s.append(" f21=").append(locoF21F27.f21() ? "on" : "off"); s.append(" f22=").append(locoF21F27.f22() ? "on" : "off"); @@ -355,7 +355,7 @@ std::string toString(const Message& message) } case OPC_MULTI_SENSE_LONG: { - const MultiSenseLong& multiSense = static_cast(message); + const auto& multiSense = static_cast(message); s.append(::toString(multiSense.code())); s.append(" sensorAddress=").append(std::to_string(multiSense.sensorAddress())); s.append(" transponderAddress=").append(std::to_string(multiSense.transponderAddress())); @@ -558,7 +558,7 @@ std::string toString(const Message& message) // raw bytes: s.append(" ["); - const uint8_t* bytes = reinterpret_cast(&message); + const auto* bytes = reinterpret_cast(&message); for(int i = 0; i < message.size(); i++) { if(i != 0) diff --git a/server/src/hardware/protocol/marklincan/configdatastreamcollector.cpp b/server/src/hardware/protocol/marklincan/configdatastreamcollector.cpp index 72ac79dc..a6c2aa30 100644 --- a/server/src/hardware/protocol/marklincan/configdatastreamcollector.cpp +++ b/server/src/hardware/protocol/marklincan/configdatastreamcollector.cpp @@ -51,7 +51,7 @@ ConfigDataStreamCollector::Status ConfigDataStreamCollector::process(const Confi m_offset += 8; return Collecting; } - else if(message.isStart() && m_crc == 0x0000) + if(message.isStart() && m_crc == 0x0000) { m_data.resize(message.length()); m_crc = message.crc(); diff --git a/server/src/hardware/protocol/marklincan/iohandler/simulationiohandler.cpp b/server/src/hardware/protocol/marklincan/iohandler/simulationiohandler.cpp index d5769e1d..5d7c7aa5 100644 --- a/server/src/hardware/protocol/marklincan/iohandler/simulationiohandler.cpp +++ b/server/src/hardware/protocol/marklincan/iohandler/simulationiohandler.cpp @@ -264,7 +264,7 @@ bool SimulationIOHandler::send(const Message& message) break; // prepend uncompressed size (big endian): - uint32_t uncompressedSize = host_to_be(emptyLoks.size()); + auto uncompressedSize = host_to_be(emptyLoks.size()); for(int i = sizeof(uncompressedSize) - 1; i >= 0; i--) data.insert(data.begin(), reinterpret_cast(&uncompressedSize)[i]); diff --git a/server/src/hardware/protocol/marklincan/kernel.cpp b/server/src/hardware/protocol/marklincan/kernel.cpp index f53e3ea2..cf0d64be 100644 --- a/server/src/hardware/protocol/marklincan/kernel.cpp +++ b/server/src/hardware/protocol/marklincan/kernel.cpp @@ -59,8 +59,8 @@ static std::tuple uidToProtocolAddress(uint32_t const uint16_t address = uid - UID::Range::locomotiveDCC.first; if(address <= DCC::addressShortMax) return {true, DecoderProtocol::DCCShort, address}; - else - return {true, DecoderProtocol::DCCLong, address}; + + return {true, DecoderProtocol::DCCLong, address}; } return {false, DecoderProtocol::None, 0}; } diff --git a/server/src/hardware/protocol/traintasticdiy/messages.cpp b/server/src/hardware/protocol/traintasticdiy/messages.cpp index c12fe18f..6b550e5b 100644 --- a/server/src/hardware/protocol/traintasticdiy/messages.cpp +++ b/server/src/hardware/protocol/traintasticdiy/messages.cpp @@ -41,7 +41,7 @@ static constexpr std::string_view toString(ThrottleSubUnsub::Action action) Checksum calcChecksum(const Message& message) { - const uint8_t* p = reinterpret_cast(&message); + const auto* p = reinterpret_cast(&message); const size_t dataSize = message.dataSize(); uint8_t checksum = p[0]; for(size_t i = 1; i <= dataSize; i++) @@ -144,7 +144,7 @@ std::string toString(const Message& message) } s.append(" ["); - const uint8_t* bytes = reinterpret_cast(&message); + const auto* bytes = reinterpret_cast(&message); for(size_t i = 0; i < message.size(); i++) { if(i != 0) diff --git a/server/src/hardware/protocol/xpressnet/kernel.cpp b/server/src/hardware/protocol/xpressnet/kernel.cpp index a8c7759e..7db56f8b 100644 --- a/server/src/hardware/protocol/xpressnet/kernel.cpp +++ b/server/src/hardware/protocol/xpressnet/kernel.cpp @@ -130,7 +130,7 @@ void Kernel::receive(const Message& message) { case idFeedbackBroadcast: { - const FeedbackBroadcast* feedback = static_cast(&message); + const auto* feedback = static_cast(&message); for(uint8_t i = 0; i < feedback->pairCount(); i++) { @@ -412,7 +412,7 @@ void Kernel::simulateInputChange(uint16_t address, SimulateInputAction action) return; // no change const uint16_t groupAddress = (address - 1) >> 2; - const uint8_t index = static_cast((address - 1) & 0x0003); + const auto index = static_cast((address - 1) & 0x0003); std::byte message[sizeof(FeedbackBroadcast) + sizeof(FeedbackBroadcast::Pair) + 1]; memset(message, 0, sizeof(message)); diff --git a/server/src/hardware/protocol/xpressnet/messages.cpp b/server/src/hardware/protocol/xpressnet/messages.cpp index 45ff52ba..2932e212 100644 --- a/server/src/hardware/protocol/xpressnet/messages.cpp +++ b/server/src/hardware/protocol/xpressnet/messages.cpp @@ -27,7 +27,7 @@ namespace XpressNet { uint8_t calcChecksum(const Message& msg, const int dataSize) { - const uint8_t* p = reinterpret_cast(&msg); + const auto* p = reinterpret_cast(&msg); uint8_t checksum = p[0]; for(int i = 1; i <= dataSize; i++) checksum ^= p[i]; diff --git a/server/src/hardware/protocol/z21/clientkernel.cpp b/server/src/hardware/protocol/z21/clientkernel.cpp index 7deee983..7932a18d 100644 --- a/server/src/hardware/protocol/z21/clientkernel.cpp +++ b/server/src/hardware/protocol/z21/clientkernel.cpp @@ -38,6 +38,7 @@ ClientKernel::ClientKernel(std::string logId_, const ClientConfig& config, bool , m_simulation{simulation} , m_keepAliveTimer(m_ioContext) , m_inactiveDecoderPurgeTimer(m_ioContext) + , m_schedulePendingRequestTimer(m_ioContext) , m_config{config} { } @@ -55,11 +56,13 @@ void ClientKernel::receive(const Message& message) { if(m_config.debugLogRXTX) EventLoop::call( - [logId_=logId, msg=toString(message)]() + [this, msg=toString(message)]() { - Log::log(logId_, LogMessage::D2002_RX_X, msg); + Log::log(logId, LogMessage::D2002_RX_X, msg); }); + auto matchedRequest = matchPendingReplyAndRemove(message); + switch(message.header()) { case LAN_X: @@ -155,6 +158,18 @@ void ClientKernel::receive(const Message& message) case LAN_X_LOCO_INFO: { + bool isAnswerToOurRequest = false; + + if(matchedRequest) + { + auto* msgData = matchedRequest.value().messageBytes.data(); + const LanX& requestMsg = *reinterpret_cast(msgData); + + // If we explicitly requested loco info then we treat it as external change + if(requestMsg.xheader != LAN_X_GET_LOCO_INFO) + isAnswerToOurRequest = true; + } + if(message.dataLen() >= LanXLocoInfo::minMessageSize && message.dataLen() <= LanXLocoInfo::maxMessageSize) { const auto& reply = static_cast(message); @@ -170,7 +185,7 @@ void ClientKernel::receive(const Message& message) LocoCache &cache = getLocoCache(reply.address()); - DecoderChangeFlags changes = DecoderChangeFlags(0); + auto changes = static_cast(0); //Rescale everything to 126 steps int currentSpeedStep = reply.speedStep(); @@ -181,69 +196,19 @@ void ClientKernel::receive(const Message& message) currentSpeedStep = cache.lastReceivedSpeedStep; //Consider it a rounding error } - int targetSpeedStep = cache.speedStep; - if(cache.speedSteps != 126) + // For answers to our own requests we don't care about direction and speed step + if(cache.lastReceivedSpeedStep != currentSpeedStep) { - targetSpeedStep = float(targetSpeedStep) / float(cache.speedSteps) * 126.0; - } - - if(!cache.speedTrendExplicitlySet) - { - //Calculate new speed trend - if(cache.lastReceivedSpeedStep <= currentSpeedStep) - cache.speedTrend = LocoCache::Trend::Ascending; - else - cache.speedTrend = LocoCache::Trend::Descending; - } - cache.speedTrendExplicitlySet = false; - - if(reply.speedSteps() != cache.speedSteps || reply.speedStep() != cache.speedStep) - { - // Use a timeout of 1 second to prevent reacting to Z21 feedback messages - // of our own changes. This would be problematic because in the meatime our - // changes were sent to Z21 processed and received back here, decoder state - // might have changed (and sent again to Z21) so we should discard "old" state. - // This has the potential of ignoring genuine user changes for this decoder - // (made by a physical throttle or other hardware connected to Z21) but - // being the timeout short it should rarely happen. - // Theoretically the timeout should only be of Train::updateSpeed() timer timeout - // (100ms) because values will be refreshed and timeout restarted by Train itself - // but we need to accout also for network trasmission and Z21 processing time. - - if((std::chrono::steady_clock::now() - cache.lastSetTime) > std::chrono::milliseconds(1000)) - { - if(reply.speedSteps() != cache.speedSteps) - { + if(!isAnswerToOurRequest) changes |= DecoderChangeFlags::SpeedSteps; - } - if(reply.speedStep() != cache.speedStep) - { - changes |= DecoderChangeFlags::Throttle; - } + cache.lastReceivedSpeedStep = currentSpeedStep; + } - cache.speedSteps = reply.speedSteps(); - cache.speedStep = reply.speedStep(); - } - else - { - bool maybeOldFeedback = false; - - if((cache.speedTrend == LocoCache::Trend::Ascending && currentSpeedStep <= targetSpeedStep) - || (cache.speedTrend == LocoCache::Trend::Descending && currentSpeedStep >= targetSpeedStep)) - { - //When accelerating or decelerating ignore all speeds between original speed and target speed. - //These messages are probably feedback of our own changes arrived with some delay. - //If we get values outside this range or changing trend we pass them to let Train adjust. - maybeOldFeedback = true; - } - - if(!maybeOldFeedback) - { - changes |= DecoderChangeFlags::Throttle | DecoderChangeFlags::SpeedSteps; - cache.speedSteps = reply.speedSteps(); - cache.speedStep = reply.speedStep(); - } - } + if(reply.speedSteps() != cache.speedSteps) + { + if(!isAnswerToOurRequest) + changes |= DecoderChangeFlags::SpeedSteps; + cache.speedSteps = reply.speedSteps(); } //Emergency stop is urgent so bypass timeout @@ -252,9 +217,11 @@ void ClientKernel::receive(const Message& message) //It can at worst cause a short flickering changing direction n times and then settle down if(reply.direction() != cache.direction) { - changes |= DecoderChangeFlags::Direction; + if(!isAnswerToOurRequest) + changes |= DecoderChangeFlags::Direction; cache.direction = reply.direction(); } + if(reply.isEmergencyStop() || reply.isEmergencyStop() != cache.isEStop) { //Force change when emergency stop is set to be sure it gets received @@ -267,6 +234,15 @@ void ClientKernel::receive(const Message& message) //Store last received speed step converted to 126 steps scale cache.lastReceivedSpeedStep = currentSpeedStep; + // Update last seen time to prevent decoder to be purged + cache.lastSetTime = std::chrono::steady_clock::now(); + + if(isAnswerToOurRequest && changes == DecoderChangeFlags(0)) + { + // No need to notify Decoder + break; + } + EventLoop::call( [this, address=reply.address(), isEStop=reply.isEmergencyStop(), speed = reply.speedStep(), speedMax=reply.speedSteps(), @@ -600,19 +576,6 @@ void ClientKernel::decoderChanged(const Decoder& decoder, DecoderChangeFlags cha } } - //Rescale everything to 126 steps - int oldTargetSpeedStep = cache.speedStep; - if(cache.speedSteps != 126) - { - oldTargetSpeedStep = float(oldTargetSpeedStep) / float(cache.speedSteps) * 126.0; - } - - int newTargetSpeedStep = cmd.speedStep(); - if(cmd.speedSteps() != 126) - { - newTargetSpeedStep = float(newTargetSpeedStep) / float(cmd.speedSteps()) * 126.0; - } - if(changed) { cache.speedSteps = cmd.speedSteps(); @@ -620,23 +583,7 @@ void ClientKernel::decoderChanged(const Decoder& decoder, DecoderChangeFlags cha cache.direction = cmd.direction(); cache.isEStop = cmd.isEmergencyStop(); - if(newTargetSpeedStep >= oldTargetSpeedStep) - { - cache.speedTrend = LocoCache::Trend::Ascending; - if(cache.lastReceivedSpeedStep > newTargetSpeedStep) - cache.lastReceivedSpeedStep = 0; //Reset to minimum - } - else - { - cache.speedTrend = LocoCache::Trend::Descending; - if(cache.lastReceivedSpeedStep < newTargetSpeedStep) - cache.lastReceivedSpeedStep = 126; //Reset to maximum - } - cache.speedTrendExplicitlySet = true; - - //Update last seen time to ignore feedback messages of our own changes - //This potentially ignores also user commands coming from Z21 if issued - //In less than 1 seconds from now + // Update last seen time to prevent decoder to be purged cache.lastSetTime = std::chrono::steady_clock::now(); } @@ -663,7 +610,7 @@ bool ClientKernel::setOutput(OutputChannel channel, uint16_t address, OutputValu }); return true; } - else if(channel == OutputChannel::DCCext) + if(channel == OutputChannel::DCCext) { if(m_firmwareVersionMajor == 1 && m_firmwareVersionMinor < 40) { @@ -798,19 +745,61 @@ void ClientKernel::onStop() m_keepAliveTimer.cancel(); m_inactiveDecoderPurgeTimer.cancel(); + m_schedulePendingRequestTimer.cancel(); m_locoCache.clear(); + m_pendingRequests.clear(); } -void ClientKernel::send(const Message& message) +void ClientKernel::send(const Message& message, bool wantReply, uint8_t customRetryCount) { if(m_ioHandler->send(message)) { if(m_config.debugLogRXTX) EventLoop::call( - [logId_=logId, msg=toString(message)]() + [this, msg=toString(message)]() { - Log::log(logId_, LogMessage::D2001_TX_X, msg); + Log::log(logId, LogMessage::D2001_TX_X, msg); }); + + if(wantReply) + { + PendingRequest request; + request.reply = getReplyType(message); + + if(request.reply.header != MessageReplyType::noReply) + { + if(customRetryCount > 0) + { + request.retryCount = customRetryCount; + } + else + { + // Calculate from priority + switch (request.reply.priority()) + { + case MessageReplyType::Priority::Low: + request.retryCount = 1; + break; + + default: + case MessageReplyType::Priority::Normal: + request.retryCount = 2; + break; + + case MessageReplyType::Priority::Urgent: + request.retryCount = 5; + break; + } + } + + // Save copy of original message + request.messageBytes.resize(message.dataLen()); + std::memcpy(request.messageBytes.data(), &message, message.dataLen()); + + // Enque pending request + addPendingRequest(request); + } + } } else {} // log message and go to error state @@ -832,7 +821,7 @@ void ClientKernel::startKeepAliveTimer() else { //Normal keep alive - assert(ClientConfig::keepAliveInterval > 0); + static_assert(ClientConfig::keepAliveInterval > 0); m_keepAliveTimer.expires_after(boost::asio::chrono::seconds(ClientConfig::keepAliveInterval)); } @@ -862,7 +851,7 @@ void ClientKernel::keepAliveTimerExpired(const boost::system::error_code& ec) void ClientKernel::startInactiveDecoderPurgeTimer() { - assert(ClientConfig::purgeInactiveDecoderInternal > 0); + static_assert(ClientConfig::purgeInactiveDecoderInternal > 0); m_inactiveDecoderPurgeTimer.expires_after(boost::asio::chrono::seconds(ClientConfig::purgeInactiveDecoderInternal)); m_inactiveDecoderPurgeTimer.async_wait(std::bind(&ClientKernel::inactiveDecoderPurgeTimerExpired, this, std::placeholders::_1)); } @@ -903,4 +892,172 @@ ClientKernel::LocoCache& ClientKernel::getLocoCache(uint16_t dccAddr) return it->second; } +void ClientKernel::addPendingRequest(const PendingRequest &request) +{ + //Enqueue this request to track reply from Z21 + bool wasEmpty = m_pendingRequests.empty(); + PendingRequest req = request; + req.sendTime = std::chrono::steady_clock::now(); + m_pendingRequests.push_back(req); + if(wasEmpty) + startSchedulePendingRequestTimer(); +} + +std::optional ClientKernel::matchPendingReplyAndRemove(const Message &message) +{ + const auto currentTime = std::chrono::steady_clock::now(); + + //TODO: depends on priority and network estimated speed + const auto timeout = std::chrono::seconds(3); + + for(auto request = m_pendingRequests.begin(); request != m_pendingRequests.end(); request++) + { + //If it's last retry and we exceeded timeout, skip request + if(request->retryCount == 0 && (currentTime - request->sendTime) > timeout) + continue; + + if(message.header() != request->reply.header) + continue; + + if(message.header() == LAN_X) + { + const LanX& lanX = static_cast(message); + if(lanX.xheader != request->reply.xHeader) + continue; + + if(request->reply.hasFlag(MessageReplyType::Flags::CheckDb0)) + { + // Cast to any LanX message with a db0 to check its value + const auto& hack = static_cast(lanX); + if(hack.db0 != request->reply.db0) + continue; + } + } + + if(request->reply.hasFlag(MessageReplyType::Flags::CheckAddress)) + { + uint16_t address = 0; + switch (message.header()) + { + case LAN_GET_LOCO_MODE: + { + address = static_cast(message).address(); + break; + } + + case LAN_GET_TURNOUTMODE: + { + // NOTE: not (yet) supported + break; + } + + case LAN_X: + { + const LanX& lanX = static_cast(message); + switch (lanX.xheader) + { + case LAN_X_TURNOUT_INFO: + address = static_cast(lanX).address(); + break; + + case LAN_X_LOCO_INFO: + address = static_cast(lanX).address(); + break; + + default: + break; + } + } + + default: + break; + } + + if(address != request->reply.address) + continue; + } + + if(request->reply.hasFlag(MessageReplyType::Flags::CheckSpeedStep)) + { + if(message.header() == LAN_X + && static_cast(message).xheader == LAN_X_LOCO_INFO) + { + const auto& locoInfo = static_cast(message); + if(locoInfo.speedAndDirection != request->reply.speedAndDirection) + continue; + if(locoInfo.speedSteps() != request->reply.speedSteps()) + continue; + } + } + + // We matched a previously sent request + // NOTE: In theory we could have matched a reply generated by other clients operations + // But for our purposes this should be fine + + // Remove it from pending queue + PendingRequest copy = *request; + m_pendingRequests.erase(request); + return copy; + } + + return {}; +} + +void ClientKernel::startSchedulePendingRequestTimer() +{ + //TODO: depends on priority and network estimated speed + const auto timeout = std::chrono::seconds(1); + + m_schedulePendingRequestTimer.expires_after(timeout); + m_schedulePendingRequestTimer.async_wait(std::bind(&ClientKernel::schedulePendingRequestTimerExpired, this, std::placeholders::_1)); +} + +void ClientKernel::schedulePendingRequestTimerExpired(const boost::system::error_code &ec) +{ + if(ec) + return; + + rescheduleTimedoutRequests(); +} + +void ClientKernel::rescheduleTimedoutRequests() +{ + const auto deadlineTime = std::chrono::steady_clock::now(); + + //TODO: depends on priority and network estimated speed + const auto timeout = std::chrono::seconds(3); + + auto request = m_pendingRequests.begin(); + while(request != m_pendingRequests.end()) + { + if((deadlineTime - request->sendTime) > timeout) + { + if(request->retryCount <= 0) + { + // Give up with this request and remove it + request = m_pendingRequests.erase(request); + continue; + } + + // Decrement retry count + request->retryCount--; + + // Re-schedule request + const auto* msgData = request->messageBytes.data(); + const Message& requestMsg = *reinterpret_cast(msgData); + + // Send original request again but without adding it to pending queue + send(requestMsg, false); + + // Restart timeout + request->sendTime = std::chrono::steady_clock::now(); + } + + request++; + } + + if(!m_pendingRequests.empty()) + startSchedulePendingRequestTimer(); +} + } diff --git a/server/src/hardware/protocol/z21/clientkernel.hpp b/server/src/hardware/protocol/z21/clientkernel.hpp index 29dad86d..80e9b0f8 100644 --- a/server/src/hardware/protocol/z21/clientkernel.hpp +++ b/server/src/hardware/protocol/z21/clientkernel.hpp @@ -24,6 +24,7 @@ #define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_Z21_CLIENTKERNEL_HPP #include +#include #include "kernel.hpp" #include @@ -67,6 +68,7 @@ class ClientKernel final : public Kernel const bool m_simulation; boost::asio::steady_timer m_keepAliveTimer; boost::asio::steady_timer m_inactiveDecoderPurgeTimer; + boost::asio::steady_timer m_schedulePendingRequestTimer; BroadcastFlags m_broadcastFlags; int m_broadcastFlagsRetryCount; static constexpr int maxBroadcastFlagsRetryCount = 10; @@ -122,26 +124,44 @@ class ClientKernel final : public Kernel struct LocoCache { - enum class Trend : bool - { - Ascending = 0, - Descending - }; - uint16_t dccAddress = 0; bool isEStop = false; uint8_t speedStep = 0; uint8_t speedSteps = 0; uint8_t lastReceivedSpeedStep = 0; //Always in 126 steps - Trend speedTrend = Trend::Ascending; - bool speedTrendExplicitlySet = false; Direction direction = Direction::Unknown; std::chrono::steady_clock::time_point lastSetTime; }; + /*! + * \brief m_locoCache stores last decoder states + * + * \note It must be accessed only from kernel thread or from + * Z21::ClientKernel::onStart(). + */ std::unordered_map m_locoCache; + + /*! + * \brief m_isUpdatingDecoderFromKernel prevents mirroring changes to Z21 + * + * \note It must be accessed only from event loop thread or from + * Z21::ClientKernel::onStart(). + * + * \sa EventLoop + */ bool m_isUpdatingDecoderFromKernel = false; + struct PendingRequest + { + std::vector messageBytes; + Z21::MessageReplyType reply; + std::chrono::steady_clock::time_point sendTime; + + //! Decrease at each re-send, remove request when reaches 0 + uint8_t retryCount = 0; + }; + std::vector m_pendingRequests; + InputController* m_inputController = nullptr; std::array m_rbusFeedbackStatus; std::array m_loconetFeedbackStatus; @@ -156,16 +176,16 @@ class ClientKernel final : public Kernel void onStop() final; template - void postSend(const T& message) + void postSend(const T& message, bool wantReply = true, uint8_t customRetryCount = 0) { m_ioContext.post( - [this, message]() + [this, message, wantReply, customRetryCount]() { - send(message); + send(message, wantReply, customRetryCount); }); } - void send(const Message& message); + void send(const Message& message, bool wantReply = true, uint8_t customRetryCount = 0); void startKeepAliveTimer(); void keepAliveTimerExpired(const boost::system::error_code& ec); @@ -175,6 +195,12 @@ class ClientKernel final : public Kernel LocoCache &getLocoCache(uint16_t dccAddr); + void addPendingRequest(const PendingRequest& request); + std::optional matchPendingReplyAndRemove(const Message& message); + void startSchedulePendingRequestTimer(); + void schedulePendingRequestTimerExpired(const boost::system::error_code &ec); + void rescheduleTimedoutRequests(); + public: /** * @brief Create kernel and IO handler diff --git a/server/src/hardware/protocol/z21/iohandler/simulationiohandler.cpp b/server/src/hardware/protocol/z21/iohandler/simulationiohandler.cpp index ee4c88e1..77ec848e 100644 --- a/server/src/hardware/protocol/z21/iohandler/simulationiohandler.cpp +++ b/server/src/hardware/protocol/z21/iohandler/simulationiohandler.cpp @@ -49,6 +49,7 @@ bool SimulationIOHandler::send(const Message& message) switch(lanX.xheader) { case 0x21: + { if(message == LanXGetVersion()) { reply(LanXGetVersionReply(xBusVersion, CommandStationId::Z21)); @@ -85,8 +86,9 @@ bool SimulationIOHandler::send(const Message& message) } } break; - + } case LAN_X_SET_STOP: + { if(message == LanXSetStop()) { const bool changed = !m_emergencyStop; @@ -98,36 +100,104 @@ bool SimulationIOHandler::send(const Message& message) } } break; - + } case LAN_X_GET_LOCO_INFO: + { if(const auto& getLocoInfo = static_cast(message); getLocoInfo.db0 == 0xF0) { - // not (yet) supported + auto it = m_decoderCache.find(getLocoInfo.address()); + if(it != m_decoderCache.cend()) + reply(it->second); + else + { + LanXLocoInfo empty; + empty.setAddress(getLocoInfo.address(), getLocoInfo.isLongAddress()); + empty.setSpeedSteps(126); + empty.setEmergencyStop(); + empty.updateChecksum(); + reply(empty); + } } break; - + } case LAN_X_SET_LOCO: + { if(const auto& setLocoDrive = static_cast(message); setLocoDrive.db0 >= 0x10 && setLocoDrive.db0 <= 0x13) { - // not (yet) supported + auto it = m_decoderCache.find(setLocoDrive.address()); + if(it == m_decoderCache.cend()) + { + // Insert in cache + LanXLocoInfo empty; + empty.setAddress(setLocoDrive.address(), setLocoDrive.isLongAddress()); + empty.setSpeedSteps(126); + empty.setEmergencyStop(); + it = m_decoderCache.insert({setLocoDrive.address(), empty}).first; + } + + LanXLocoInfo &info = it->second; + info.setSpeedSteps(setLocoDrive.speedSteps()); + info.setDirection(setLocoDrive.direction()); + if(setLocoDrive.isEmergencyStop()) + info.setEmergencyStop(); + else + info.setSpeedStep(setLocoDrive.speedStep()); + + info.setBusy(true); + info.updateChecksum(); + + reply(info); } else if(const auto& setLocoFunction = static_cast(message); setLocoFunction.db0 == 0xF8 && setLocoFunction.switchType() != LanXSetLocoFunction::SwitchType::Invalid) { - // not (yet) supported + auto it = m_decoderCache.find(setLocoDrive.address()); + if(it == m_decoderCache.cend()) + { + // Insert in cache + LanXLocoInfo empty; + empty.setAddress(setLocoFunction.address(), setLocoFunction.isLongAddress()); + empty.setSpeedSteps(126); + empty.setEmergencyStop(); + it = m_decoderCache.insert({setLocoFunction.address(), empty}).first; + } + + LanXLocoInfo &info = it->second; + bool val = info.getFunction(setLocoFunction.functionIndex()); + switch (setLocoFunction.switchType()) + { + case LanXSetLocoFunction::SwitchType::Off: + val = false; + break; + case LanXSetLocoFunction::SwitchType::On: + val = true; + break; + case LanXSetLocoFunction::SwitchType::Toggle: + val = !val; + break; + default: + break; + } + info.setFunction(setLocoFunction.functionIndex(), val); + + info.setBusy(true); + info.updateChecksum(); + + reply(info); } break; - + } case LAN_X_GET_FIRMWARE_VERSION: + { if(message == LanXGetFirmwareVersion()) { reply(LanXGetFirmwareVersionReply(firmwareVersionMajor, ServerConfig::firmwareVersionMinor)); } break; - + } case LAN_X_SET_TURNOUT: { if(message.dataLen() == sizeof(LanXSetTurnout)) @@ -194,7 +264,7 @@ bool SimulationIOHandler::send(const Message& message) case LAN_GET_BROADCASTFLAGS: if(message == LanGetBroadcastFlags()) { - reply(LanSetBroadcastFlags(m_broadcastFlags)); + reply(LanGetBroadcastFlagsReply(m_broadcastFlags)); } break; diff --git a/server/src/hardware/protocol/z21/iohandler/simulationiohandler.hpp b/server/src/hardware/protocol/z21/iohandler/simulationiohandler.hpp index 1c4f6862..5b1792dd 100644 --- a/server/src/hardware/protocol/z21/iohandler/simulationiohandler.hpp +++ b/server/src/hardware/protocol/z21/iohandler/simulationiohandler.hpp @@ -25,6 +25,7 @@ #include "iohandler.hpp" #include +#include #include "../messages.hpp" namespace Z21 { @@ -42,6 +43,8 @@ class SimulationIOHandler final : public IOHandler bool m_trackPowerOn = false; BroadcastFlags m_broadcastFlags = BroadcastFlags::None; + std::unordered_map m_decoderCache; + void reply(const Message& message); void replyLanSystemStateDataChanged(); diff --git a/server/src/hardware/protocol/z21/messages.cpp b/server/src/hardware/protocol/z21/messages.cpp index 1621ba75..7c105533 100644 --- a/server/src/hardware/protocol/z21/messages.cpp +++ b/server/src/hardware/protocol/z21/messages.cpp @@ -160,7 +160,7 @@ std::string toString(const Message& message, bool raw) break; case LAN_X_STATUS_CHANGED: - if(const LanXStatusChanged& statusChanged = static_cast(message); statusChanged.db0 == 0x22) + if(const auto& statusChanged = static_cast(message); statusChanged.db0 == 0x22) { s = "LAN_X_STATUS_CHANGED"; s.append(" emergency_stop=").append(statusChanged.db1 & Z21_CENTRALSTATE_EMERGENCYSTOP ? "yes" : "no"); @@ -315,7 +315,7 @@ std::string toString(const Message& message, bool raw) if(raw) { s.append(" ["); - const uint8_t* bytes = reinterpret_cast(&message); + const auto* bytes = reinterpret_cast(&message); for(uint16_t i = sizeof(Message); i < message.dataLen(); i++) { if(i != sizeof(Message)) @@ -361,4 +361,191 @@ bool LanX::isChecksumValid(const LanX &lanX) return XpressNet::isChecksumValid(msg, dataSize); } +MessageReplyType getReplyType(const Message &message) +{ + switch (message.header()) + { + //Messages whose replies have same header + case LAN_GET_SERIAL_NUMBER: + case LAN_GET_CODE: + case LAN_GET_HWINFO: + { + MessageReplyType reply; + reply.header = message.header(); + reply.setPriority(MessageReplyType::Priority::Low); + return reply; + } + + case LAN_GET_BROADCASTFLAGS: + case LAN_LOCONET_DETECTOR: + case LAN_CAN_DETECTOR: + return {message.header()}; + + case LAN_GET_LOCO_MODE: + case LAN_GET_TURNOUTMODE: + { + MessageReplyType reply; + reply.header = message.header(); + reply.setFlag(MessageReplyType::Flags::CheckAddress); + + if(message.header() == LAN_GET_LOCO_MODE) + reply.address = static_cast(message).address(); + + // NOTE: not (yet) supported + //else + // reply.address = static_cast(message).address(); + + return reply; + } + + // NOTE: This message has no reply for Z21 firmware < 1.22 + // NOTE: not (yet) supported + case LAN_LOCONET_DISPATCH_ADDR: + return {LAN_LOCONET_DISPATCH_ADDR}; + + case LAN_X: + { + const LanX& lanX = static_cast(message); + switch (lanX.xheader) + { + case 0x21: + { + MessageReplyType reply; + reply.header = LAN_X; + reply.setPriority(MessageReplyType::Priority::Low); + reply.setFlag(MessageReplyType::Flags::CheckDb0); + + // Cast to any LanX message with a db0 to check its value + const auto& hack = static_cast(lanX); + switch (hack.db0) + { + case 0x21: // LAN_X_GET_VERSION + { + reply.xHeader = LAN_X_GET_VERSION_REPLY; + break; + } + + case 0x24: // LAN_X_GET_STATUS + { + reply.xHeader = LAN_X_STATUS_CHANGED; + break; + } + + case LAN_X_SET_TRACK_POWER_OFF: + { + reply.xHeader = LAN_X_BC; + reply.db0 = LAN_X_BC_TRACK_POWER_OFF; + reply.setPriority(MessageReplyType::Priority::Urgent); + break; + } + + case LAN_X_SET_TRACK_POWER_ON: + { + //LAN_X_SET_TRACK_POWER_ON + reply.xHeader = LAN_X_BC; + reply.db0 = LAN_X_BC_TRACK_POWER_ON; + reply.setPriority(MessageReplyType::Priority::Urgent); + break; + } + + default: + return {MessageReplyType::noReply}; + } + return reply; + } + + case LAN_X_TURNOUT_INFO: + case LAN_X_SET_TURNOUT: + { + MessageReplyType reply; + reply.header = LAN_X; + reply.xHeader = LAN_X_TURNOUT_INFO; + reply.setFlag(MessageReplyType::Flags::CheckAddress); + + if(lanX.xheader == LAN_X_TURNOUT_INFO) + reply.address = static_cast(lanX).address(); + else + reply.address = static_cast(lanX).address(); + + return reply; + } + + case LAN_X_SET_STOP: + { + MessageReplyType reply; + reply.header = LAN_X; + reply.xHeader = LAN_X_BC_STOPPED; + reply.setPriority(MessageReplyType::Priority::Urgent); + return reply; + } + + case LAN_X_GET_LOCO_INFO: + case LAN_X_SET_LOCO: + { + MessageReplyType reply; + reply.header = LAN_X; + reply.xHeader = LAN_X_LOCO_INFO; + reply.setFlag(MessageReplyType::Flags::CheckAddress); + + if(lanX.xheader == LAN_X_GET_LOCO_INFO) + { + reply.address = static_cast(lanX).address(); + } + else + { + if(const auto& setLocoDrive = static_cast(message); + setLocoDrive.db0 >= 0x10 && setLocoDrive.db0 <= 0x13) + { + reply.address = setLocoDrive.address(); + reply.speedAndDirection = setLocoDrive.speedAndDirection; + reply.setSpeedSteps(setLocoDrive.speedSteps()); + reply.setFlag(MessageReplyType::Flags::CheckSpeedStep); + } + else if(const auto& setLocoFunction = static_cast(message); + setLocoFunction.db0 == 0xF8) + { + reply.address = setLocoFunction.address(); + } + else + { + //Other loco function messages do not have standard reply + return {MessageReplyType::noReply}; + } + } + + return reply; + } + + case LAN_X_GET_FIRMWARE_VERSION: + { + MessageReplyType reply; + reply.header = LAN_X; + reply.xHeader = LAN_X_GET_FIRMWARE_VERSION_REPLY; + reply.setPriority(MessageReplyType::Priority::Low); + return reply; + } + + default: + break; + } + break; + } + + case LAN_RMBUS_GETDATA: + return {LAN_RMBUS_DATACHANGED}; + + case LAN_SYSTEMSTATE_GETDATA: + return {LAN_SYSTEMSTATE_DATACHANGED}; + + case LAN_RAILCOM_GETDATA: + return {LAN_RAILCOM_DATACHANGED}; + + default: + break; + } + + //Message has no reply or it's no supported by Traintastic + return {MessageReplyType::noReply}; +} + } diff --git a/server/src/hardware/protocol/z21/messages.hpp b/server/src/hardware/protocol/z21/messages.hpp index b76cdd6b..2f6b8522 100644 --- a/server/src/hardware/protocol/z21/messages.hpp +++ b/server/src/hardware/protocol/z21/messages.hpp @@ -1698,6 +1698,96 @@ constexpr std::string_view toString(LanXSetLocoFunction::SwitchType value) return {}; } +struct MessageReplyType +{ + enum class Priority : uint8_t + { + Low = 0, + Normal = 1, + Urgent = 2 + }; + + enum class Flags : uint8_t + { + CheckDb0 = 1 << 0, + CheckSpeedStep = 1 << 1, + CheckAddress = 1 << 2 + }; + + inline Priority priority() const + { + return Priority(m_flags & 0xF); + } + + inline void setPriority(Priority value) + { + m_flags = uint8_t(getFlags()) << 4 | (uint8_t(value) & 0xF); + } + + inline Flags getFlags() const + { + return Flags((m_flags >> 4) & 0xF); + } + + inline bool hasFlag(Flags flag) const + { + return uint8_t(getFlags()) & uint8_t(flag); + } + + inline void setFlag(Flags flag, bool on = true) + { + uint8_t flags = uint8_t(getFlags()); + if(on) + flags |= uint8_t(flag); + else + flags &= ~uint8_t(flag); + setFlags(Flags(flags)); + } + + inline void setFlags(Flags flags) + { + m_flags = uint8_t(flags) << 4 | (uint8_t(priority()) & 0xF); + } + + inline uint8_t speedSteps() const + { + switch(speedStepsEncoded & LanXLocoInfo::db2_speed_steps_mask) + { + case LanXLocoInfo::db2_speed_steps_14: return 14; + case LanXLocoInfo::db2_speed_steps_28: return 28; + case LanXLocoInfo::db2_speed_steps_128: return 126; + } + return 0; + } + + inline void setSpeedSteps(uint8_t value) + { + speedStepsEncoded &= ~LanXLocoInfo::db2_speed_steps_mask; + switch(value) + { + case 14: speedStepsEncoded |= LanXLocoInfo::db2_speed_steps_14; break; + case 28: speedStepsEncoded |= LanXLocoInfo::db2_speed_steps_28; break; + case 126: + case 128: + default: speedStepsEncoded |= LanXLocoInfo::db2_speed_steps_128; break; + } + } + + static constexpr Header noReply = Header(0); + + Header header = noReply; + uint8_t xHeader = 0; + uint8_t db0 = 0; + uint16_t address = 0; + uint8_t m_flags = uint8_t(Priority::Normal); + + // Encoded as LAN_X_ + uint8_t speedAndDirection = 0; + uint8_t speedStepsEncoded = 0; +}; + +MessageReplyType getReplyType(const Message &message); + } inline bool operator ==(const Z21::Message& lhs, const Z21::Message& rhs) diff --git a/server/src/hardware/protocol/z21/serverkernel.cpp b/server/src/hardware/protocol/z21/serverkernel.cpp index 69cb6ea4..dcc2b51f 100644 --- a/server/src/hardware/protocol/z21/serverkernel.cpp +++ b/server/src/hardware/protocol/z21/serverkernel.cpp @@ -249,7 +249,7 @@ void ServerKernel::receiveFrom(const Message& message, IOHandler::ClientId clien case LAN_GET_BROADCASTFLAGS: if(message == LanGetBroadcastFlags()) - sendTo(LanSetBroadcastFlags(m_clients[clientId].broadcastFlags), clientId); + sendTo(LanGetBroadcastFlagsReply(m_clients[clientId].broadcastFlags), clientId); break; case LAN_SET_BROADCASTFLAGS: diff --git a/server/src/lua/enums.hpp b/server/src/lua/enums.hpp index 6e2f6093..7e8f11d7 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" #define LUA_ENUMS \ @@ -79,6 +79,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/object/object.cpp b/server/src/lua/object/object.cpp index d8f27b8b..03f4f5fe 100644 --- a/server/src/lua/object/object.cpp +++ b/server/src/lua/object/object.cpp @@ -53,7 +53,7 @@ int Object::index(lua_State* L, ::Object& object) if(InterfaceItem* item = object.getItem(key)) { - if(AbstractProperty* property = dynamic_cast(item)) + if(auto* property = dynamic_cast(item)) { if(property->isScriptReadable()) { @@ -109,7 +109,7 @@ int Object::index(lua_State* L, ::Object& object) else lua_pushnil(L); } - else if(AbstractMethod* method = dynamic_cast(item)) + else if(auto* method = dynamic_cast(item)) { if(method->isScriptCallable()) Method::push(L, *method); diff --git a/server/src/lua/persistentvariables.cpp b/server/src/lua/persistentvariables.cpp new file mode 100644 index 00000000..b8968e1f --- /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; + } + 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..50484798 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 @@ -342,7 +364,7 @@ Sandbox::StateData::~StateData() { if(auto outputController = it.first.lock()) { - for(auto& outputWeak : it.second) + for(const auto& outputWeak : it.second) { if(auto output = outputWeak.lock()) { 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