From 16503e32ff10eacb8b5c57da135d852fcb7b594c Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Thu, 20 Jun 2024 00:06:27 +0200 Subject: [PATCH 01/37] added basic zone support with train tracking and events see #144 --- client/src/mainwindow.cpp | 8 + manual/luadoc/class.json | 8 + manual/luadoc/object/stateobject.json | 1 + manual/luadoc/object/train.json | 66 +++++ manual/luadoc/object/trainzonestatus.json | 11 + manual/luadoc/object/world.json | 5 +- manual/luadoc/object/zone.json | 64 +++++ manual/luadoc/terms/en-us.json | 104 ++++++++ server/CMakeLists.txt | 2 + server/src/board/list/blockrailtilelist.cpp | 39 +++ server/src/board/list/blockrailtilelist.hpp | 42 ++++ .../list/blockrailtilelisttablemodel.cpp | 75 ++++++ .../list/blockrailtilelisttablemodel.hpp | 46 ++++ server/src/board/tile/rail/blockrailtile.cpp | 31 ++- server/src/board/tile/rail/blockrailtile.hpp | 5 + server/src/lua/class.cpp | 6 + server/src/lua/enums.hpp | 4 +- server/src/train/train.cpp | 47 ++++ server/src/train/train.hpp | 16 ++ server/src/train/traintracking.cpp | 228 ++++++++++++++++-- server/src/train/traintracking.hpp | 16 +- server/src/train/trainzonestatus.cpp | 64 +++++ server/src/train/trainzonestatus.hpp | 52 ++++ server/src/world/world.cpp | 16 ++ server/src/world/world.hpp | 6 +- server/src/world/worldloader.cpp | 13 + server/src/zone/blockzonelist.cpp | 86 +++++++ server/src/zone/blockzonelist.hpp | 53 ++++ server/src/zone/zone.cpp | 124 ++++++++++ server/src/zone/zone.hpp | 70 ++++++ server/src/zone/zoneblocklist.cpp | 84 +++++++ server/src/zone/zoneblocklist.hpp | 53 ++++ server/src/zone/zonelist.cpp | 61 +++++ server/src/zone/zonelist.hpp | 47 ++++ server/src/zone/zonelisttablemodel.cpp | 77 ++++++ server/src/zone/zonelisttablemodel.hpp | 46 ++++ .../src/traintastic/enum/zonetrainstate.hpp | 54 +++++ shared/translations/en-us.json | 10 +- 38 files changed, 1713 insertions(+), 27 deletions(-) create mode 100644 manual/luadoc/object/stateobject.json create mode 100644 manual/luadoc/object/trainzonestatus.json create mode 100644 manual/luadoc/object/zone.json create mode 100644 server/src/board/list/blockrailtilelist.cpp create mode 100644 server/src/board/list/blockrailtilelist.hpp create mode 100644 server/src/board/list/blockrailtilelisttablemodel.cpp create mode 100644 server/src/board/list/blockrailtilelisttablemodel.hpp create mode 100644 server/src/train/trainzonestatus.cpp create mode 100644 server/src/train/trainzonestatus.hpp create mode 100644 server/src/zone/blockzonelist.cpp create mode 100644 server/src/zone/blockzonelist.hpp create mode 100644 server/src/zone/zone.cpp create mode 100644 server/src/zone/zone.hpp create mode 100644 server/src/zone/zoneblocklist.cpp create mode 100644 server/src/zone/zoneblocklist.hpp create mode 100644 server/src/zone/zonelist.cpp create mode 100644 server/src/zone/zonelist.hpp create mode 100644 server/src/zone/zonelisttablemodel.cpp create mode 100644 server/src/zone/zonelisttablemodel.hpp create mode 100644 shared/src/traintastic/enum/zonetrainstate.hpp diff --git a/client/src/mainwindow.cpp b/client/src/mainwindow.cpp index eaba884b..e97e17f1 100644 --- a/client/src/mainwindow.cpp +++ b/client/src/mainwindow.cpp @@ -406,6 +406,14 @@ MainWindow::MainWindow(QWidget* parent) : menu->addAction(Locale::tr("world:outputs") + "...", [this](){ showObject("world.outputs", Locale::tr("world:outputs")); }); menu->addAction(Locale::tr("hardware:identifications") + "...", [this](){ showObject("world.identifications", Locale::tr("hardware:identifications")); }); boardsAction = m_menuObjects->addAction(Theme::getIcon("board"), Locale::tr("world:boards") + "...", [this](){ showObject("world.boards", Locale::tr("world:boards")); }); + m_menuObjects->addAction( + Theme::getIcon("zone"), + Locale::tr("world:zones") + "...", + [this]() + { + showObject("world.zones", Locale::tr("world:zones")); + } + ); m_menuObjects->addAction(Theme::getIcon("clock"), Locale::tr("world:clock") + "...", [this](){ showObject("world.clock", Locale::tr("world:clock")); }); trainsAction = m_menuObjects->addAction(Theme::getIcon("train"), Locale::tr("world:trains") + "...", [this]() diff --git a/manual/luadoc/class.json b/manual/luadoc/class.json index f082a947..02d58a58 100644 --- a/manual/luadoc/class.json +++ b/manual/luadoc/class.json @@ -188,6 +188,10 @@ "TRAIN_LIST": { "type": "constant" }, + "TRAIN_ZONE_STATUS": { + "type": "constant", + "since": "0.3" + }, "WORLD": { "type": "constant", "since": "0.1" @@ -198,5 +202,9 @@ "NX_BUTTON_RAIL_TILE": { "type": "constant", "since": "0.3" + }, + "ZONE": { + "type": "constant", + "since": "0.3" } } diff --git a/manual/luadoc/object/stateobject.json b/manual/luadoc/object/stateobject.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/manual/luadoc/object/stateobject.json @@ -0,0 +1 @@ +{} diff --git a/manual/luadoc/object/train.json b/manual/luadoc/object/train.json index c2f9ebec..6dc8c909 100644 --- a/manual/luadoc/object/train.json +++ b/manual/luadoc/object/train.json @@ -69,5 +69,71 @@ } ], "since": "0.3" + }, + "on_zone_assigned": { + "parameters": [ + { + "name": "train" + }, + { + "name": "zone" + } + ], + "since": "0.3" + }, + "on_zone_entering": { + "parameters": [ + { + "name": "train" + }, + { + "name": "zone" + } + ], + "since": "0.3" + }, + "on_zone_entered": { + "parameters": [ + { + "name": "train" + }, + { + "name": "zone" + } + ], + "since": "0.3" + }, + "on_zone_leaving": { + "parameters": [ + { + "name": "train" + }, + { + "name": "zone" + } + ], + "since": "0.3" + }, + "on_zone_left": { + "parameters": [ + { + "name": "train" + }, + { + "name": "zone" + } + ], + "since": "0.3" + }, + "on_zone_removed": { + "parameters": [ + { + "name": "train" + }, + { + "name": "zone" + } + ], + "since": "0.3" } } diff --git a/manual/luadoc/object/trainzonestatus.json b/manual/luadoc/object/trainzonestatus.json new file mode 100644 index 00000000..356e7891 --- /dev/null +++ b/manual/luadoc/object/trainzonestatus.json @@ -0,0 +1,11 @@ +{ + "state": { + "since": "0.3" + }, + "train": { + "since": "0.3" + }, + "zone": { + "since": "0.3" + } +} diff --git a/manual/luadoc/object/world.json b/manual/luadoc/object/world.json index de0ba3a5..8e614eb1 100644 --- a/manual/luadoc/object/world.json +++ b/manual/luadoc/object/world.json @@ -46,5 +46,6 @@ } ], "since": "0.1" - } -} \ No newline at end of file + }, + "zones": {} +} diff --git a/manual/luadoc/object/zone.json b/manual/luadoc/object/zone.json new file mode 100644 index 00000000..14fbcee0 --- /dev/null +++ b/manual/luadoc/object/zone.json @@ -0,0 +1,64 @@ +{ + "name": {}, + "trains": {}, + "on_train_assigned": { + "parameters": [ + { + "name": "train" + }, + { + "name": "zone" + } + ] + }, + "on_train_entering": { + "parameters": [ + { + "name": "train" + }, + { + "name": "zone" + } + ] + }, + "on_train_entered": { + "parameters": [ + { + "name": "train" + }, + { + "name": "zone" + } + ] + }, + "on_train_leaving": { + "parameters": [ + { + "name": "train" + }, + { + "name": "zone" + } + ] + }, + "on_train_left": { + "parameters": [ + { + "name": "train" + }, + { + "name": "zone" + } + ] + }, + "on_train_removed": { + "parameters": [ + { + "name": "train" + }, + { + "name": "zone" + } + ] + } +} diff --git a/manual/luadoc/terms/en-us.json b/manual/luadoc/terms/en-us.json index 46948bd1..986b39a4 100644 --- a/manual/luadoc/terms/en-us.json +++ b/manual/luadoc/terms/en-us.json @@ -1926,5 +1926,109 @@ { "term": "object.train.on_block_left.parameter.direction:description", "definition": "Train direction from the block perspective, a {ref:enum.block_train_direction} value." + }, + { + "term": "object.zone:title", + "definition": "Zone" + }, + { + "term": "object.zone:description", + "definition": "A group of blocks, this can for example be used to create a station or a staging area." + }, + { + "term": "object.zone.name:description", + "definition": "Zone name." + }, + { + "term": "object.zone.on_train_entering:description", + "definition": "Fired when a train reserves or enters the first block in the zone." + }, + { + "term": "object.zone.on_train_entering.parameter.train:description", + "definition": "The {ref:object.train|train} that is entering the zone." + }, + { + "term": "object.zone.on_train_entering.parameter.zone:description", + "definition": "The zone." + }, + { + "term": "object.zone.on_train_assigned:description", + "definition": "Fired when a train is assigned to the first block in the zone." + }, + { + "term": "object.zone.on_train_assigned.parameter.train:description", + "definition": "The {ref:object.train|train} that is assigned to a block in the zone." + }, + { + "term": "object.zone.on_train_assigned.parameter.zone:description", + "definition": "The zone." + }, + { + "term": "object.zone.on_train_leaving:description", + "definition": "Fired when a train reserves or enters a block that is not part of the zone." + }, + { + "term": "object.zone.on_train_leaving.parameter.train:description", + "definition": "The {ref:object.train|train} that is leaving the zone." + }, + { + "term": "object.zone.on_train_leaving.parameter.zone:description", + "definition": "The zone." + }, + { + "term": "object.zone.on_train_entered:description", + "definition": "Fired when a train is in the zone, a train is in the zone when it only occupies blocks that are part of the zone." + }, + { + "term": "object.zone.on_train_entered.parameter.train:description", + "definition": "The {ref:object.train|train} that is in the zone." + }, + { + "term": "object.zone.on_train_entered.parameter.zone:description", + "definition": "The zone." + }, + { + "term": "object.zone.on_train_left:description", + "definition": "Fired when a train is no longer in the zone, a train is not longer in the zone when it doesn't occupy any block that are part of the zone." + }, + { + "term": "object.zone.on_train_left.parameter.train:description", + "definition": "The {ref:object.train|train} that has left the zone." + }, + { + "term": "object.zone.on_train_left.parameter.zone:description", + "definition": "The zone." + }, + { + "term": "object.zone.on_train_removed:description", + "definition": "Fired when a train is removed from the last block in the zone." + }, + { + "term": "object.zone.on_train_removed.parameter.train:description", + "definition": "The {ref:object.train|train} that is removed from the zone." + }, + { + "term": "object.zone.on_train_removed.parameter.zone:description", + "definition": "The zone." + }, + { + "term": "object.trainzonestatus:title", + "definition": "Train zone status" + }, + { + "term": "object.trainzonestatus.state:description", + "definition": "State of the train in the zone, a {ref:enum.zone_train_state} value." + }, + { + "term": "object.trainzonestatus.train:description", + "definition": "The {ref:object.train|train}." + }, + { + "term": "object.trainzonestatus.zone:description", + "definition": "The {ref:object.zone|zone}." + }, + { + "term": "enum.zone_train_state:title", + "definition": "Zone train state" } ] diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 7bed0168..03cac960 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -170,6 +170,8 @@ file(GLOB SOURCES "src/utils/*.cpp" "src/world/*.hpp" "src/world/*.cpp" + "src/zone/*.hpp" + "src/zone/*.cpp" "../shared/src/traintastic/locale/locale.cpp" "../shared/src/traintastic/utils/standardpaths.cpp") diff --git a/server/src/board/list/blockrailtilelist.cpp b/server/src/board/list/blockrailtilelist.cpp new file mode 100644 index 00000000..17c83d54 --- /dev/null +++ b/server/src/board/list/blockrailtilelist.cpp @@ -0,0 +1,39 @@ +/** + * server/src/board/list/blockrailtilelist.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 "blockrailtilelist.hpp" +#include "blockrailtilelisttablemodel.hpp" + +BlockRailTileList::BlockRailTileList(Object& _parent, std::string_view parentPropertyName) + : ObjectList(_parent, parentPropertyName) +{ +} + +TableModelPtr BlockRailTileList::getModel() +{ + return std::make_shared(*this); +} + +bool BlockRailTileList::isListedProperty(std::string_view name) +{ + return BlockRailTileListTableModel::isListedProperty(name); +} diff --git a/server/src/board/list/blockrailtilelist.hpp b/server/src/board/list/blockrailtilelist.hpp new file mode 100644 index 00000000..7e5375d9 --- /dev/null +++ b/server/src/board/list/blockrailtilelist.hpp @@ -0,0 +1,42 @@ +/** + * server/src/board/list/blockrailtilelist.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_LIST_BLOCKRAILTILELIST_HPP +#define TRAINTASTIC_SERVER_BOARD_LIST_BLOCKRAILTILELIST_HPP + +#include "../../core/objectlist.hpp" +#include "../tile/rail/blockrailtile.hpp" + +class BlockRailTileList : public ObjectList +{ + CLASS_ID("list.block_rail_tile") + + protected: + bool isListedProperty(std::string_view name) final; + + public: + BlockRailTileList(Object& _parent, std::string_view parentPropertyName); + + TableModelPtr getModel() final; +}; + +#endif diff --git a/server/src/board/list/blockrailtilelisttablemodel.cpp b/server/src/board/list/blockrailtilelisttablemodel.cpp new file mode 100644 index 00000000..12e67119 --- /dev/null +++ b/server/src/board/list/blockrailtilelisttablemodel.cpp @@ -0,0 +1,75 @@ +/** + * server/src/board/list/blockrailtilelisttablemodel.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 "blockrailtilelisttablemodel.hpp" +#include "blockrailtilelist.hpp" +#include "../../utils/displayname.hpp" + +constexpr uint32_t columnId = 0; +constexpr uint32_t columnName = 1; + +bool BlockRailTileListTableModel::isListedProperty(std::string_view name) +{ + return + name == "id" || + name == "name"; +} + +BlockRailTileListTableModel::BlockRailTileListTableModel(ObjectList& list) : + ObjectListTableModel(list) +{ + setColumnHeaders({ + DisplayName::Object::id, + DisplayName::Object::name, + }); +} + +std::string BlockRailTileListTableModel::getText(uint32_t column, uint32_t row) const +{ + if(row < rowCount()) + { + const BlockRailTile& blockrailtile = getItem(row); + + switch(column) + { + case columnId: + return blockrailtile.id; + + case columnName: + return blockrailtile.name; + + default: + assert(false); + break; + } + } + + return ""; +} + +void BlockRailTileListTableModel::propertyChanged(BaseProperty& property, uint32_t row) +{ + if(property.name() == "id") + changed(row, columnId); + else if(property.name() == "name") + changed(row, columnName); +} diff --git a/server/src/board/list/blockrailtilelisttablemodel.hpp b/server/src/board/list/blockrailtilelisttablemodel.hpp new file mode 100644 index 00000000..7c8d70eb --- /dev/null +++ b/server/src/board/list/blockrailtilelisttablemodel.hpp @@ -0,0 +1,46 @@ +/** + * server/src/board/list/blockrailtilelisttablemodel.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_LIST_BLOCKRAILTILELISTTABLEMODEL_HPP +#define TRAINTASTIC_SERVER_BOARD_LIST_BLOCKRAILTILELISTTABLEMODEL_HPP + +#include "../../core/objectlisttablemodel.hpp" +#include "../tile/rail/blockrailtile.hpp" + +class BlockRailTileList; + +class BlockRailTileListTableModel : public ObjectListTableModel +{ + protected: + void propertyChanged(BaseProperty& property, uint32_t row) final; + + public: + CLASS_ID("table_model.blockrailtile_list") + + static bool isListedProperty(std::string_view name); + + BlockRailTileListTableModel(ObjectList& list); + + std::string getText(uint32_t column, uint32_t row) const final; +}; + +#endif diff --git a/server/src/board/tile/rail/blockrailtile.cpp b/server/src/board/tile/rail/blockrailtile.cpp index b4854a22..22b2dfc6 100644 --- a/server/src/board/tile/rail/blockrailtile.cpp +++ b/server/src/board/tile/rail/blockrailtile.cpp @@ -31,6 +31,9 @@ #include "../../../train/trainblockstatus.hpp" #include "../../../train/traintracking.hpp" #include "../../../utils/displayname.hpp" +#include "../../../zone/blockzonelist.hpp" +#include "../../list/blockrailtilelist.hpp" +#include "../../list/blockrailtilelisttablemodel.hpp" #include "../../map/blockpath.hpp" constexpr uint8_t toMask(BlockSide side) @@ -55,6 +58,7 @@ BlockRailTile::BlockRailTile(World& world, std::string_view _id) : state{this, "state", BlockState::Unknown, PropertyFlags::ReadOnly | PropertyFlags::StoreState}, sensorStates{*this, "sensor_states", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState} , trains{*this, "trains", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly} + , zones{this, "zones", {}, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject} , assignTrain{*this, "assign_train", [this](const std::shared_ptr& newTrain) { @@ -112,8 +116,7 @@ BlockRailTile::BlockRailTile(World& world, std::string_view _id) : } } - newTrain->fireBlockAssigned(shared_ptr()); - fireEvent(onTrainAssigned, newTrain, self); + TrainTracking::assigned(newTrain, self); } }} , removeTrain{*this, "remove_train", @@ -156,8 +159,7 @@ BlockRailTile::BlockRailTile(World& world, std::string_view _id) : } } - oldTrain->fireBlockRemoved(shared_ptr()); - fireEvent(onTrainRemoved, oldTrain, self); + TrainTracking::removed(oldTrain, shared_ptr()); } }} , flipTrain{*this, "flip_train", @@ -195,6 +197,7 @@ BlockRailTile::BlockRailTile(World& world, std::string_view _id) : , onTrainRemoved{*this, "on_train_removed", EventFlags::Scriptable} { inputMap.setValueInternal(std::make_shared(*this, inputMap.name())); + zones.setValueInternal(std::make_shared(*this, zones.name())); const bool editable = contains(m_world.state.value(), WorldState::Edit); @@ -215,6 +218,8 @@ BlockRailTile::BlockRailTile(World& world, std::string_view _id) : Attributes::addObjectEditor(trains, false); m_interfaceItems.add(trains); + m_interfaceItems.add(zones); + Attributes::addEnabled(assignTrain, true); Attributes::addObjectEditor(assignTrain, false); Attributes::addObjectList(assignTrain, world.trains); @@ -526,6 +531,13 @@ void BlockRailTile::worldEvent(WorldState worldState, WorldEvent worldEvent) Attributes::setEnabled(name, editable); } +void BlockRailTile::addToWorld() +{ + RailTile::addToWorld(); + + m_world.blockRailTiles->addObject(shared_ptr()); +} + void BlockRailTile::loaded() { RailTile::loaded(); @@ -540,6 +552,7 @@ void BlockRailTile::destroying() { trains.back()->destroy(); } + m_world.blockRailTiles->removeObject(self); RailTile::destroying(); } @@ -626,6 +639,11 @@ void BlockRailTile::updateHeightWidthMax() Attributes::setMax(width, !vertical ? TileData::widthMax : 1); } +void BlockRailTile::fireTrainAssigned(const std::shared_ptr& train) +{ + fireEvent(onTrainAssigned, train, shared_ptr()); +} + void BlockRailTile::fireTrainReserved(const std::shared_ptr& train, BlockTrainDirection trainDirection) { fireEvent( @@ -652,3 +670,8 @@ void BlockRailTile::fireTrainLeft(const std::shared_ptr& train, BlockTrai shared_ptr(), trainDirection); } + +void BlockRailTile::fireTrainRemoved(const std::shared_ptr& train) +{ + fireEvent(onTrainRemoved, train, shared_ptr()); +} diff --git a/server/src/board/tile/rail/blockrailtile.hpp b/server/src/board/tile/rail/blockrailtile.hpp index 7dfbf9fa..0b20fb5f 100644 --- a/server/src/board/tile/rail/blockrailtile.hpp +++ b/server/src/board/tile/rail/blockrailtile.hpp @@ -38,6 +38,7 @@ class Train; class TrainBlockStatus; class BlockInputMapItem; class BlockPath; +class BlockZoneList; class BlockRailTile : public RailTile { @@ -58,12 +59,15 @@ class BlockRailTile : public RailTile void updatePaths(); void updateHeightWidthMax(); + void fireTrainAssigned(const std::shared_ptr& train); void fireTrainReserved(const std::shared_ptr& train, BlockTrainDirection trainDirection); void fireTrainEntered(const std::shared_ptr& train, BlockTrainDirection trainDirection); void fireTrainLeft(const std::shared_ptr& train, BlockTrainDirection trainDirection); + void fireTrainRemoved(const std::shared_ptr& train); protected: void worldEvent(WorldState worldState, WorldEvent worldEvent) final; + void addToWorld() final; void loaded() final; void destroying() final; void setRotate(TileRotate value) final; @@ -81,6 +85,7 @@ class BlockRailTile : public RailTile Property state; VectorProperty sensorStates; ObjectVectorProperty trains; + ObjectProperty zones; Method)> assignTrain; Method removeTrain; Method flipTrain; diff --git a/server/src/lua/class.cpp b/server/src/lua/class.cpp index 6ea61bbf..0b6a712a 100644 --- a/server/src/lua/class.cpp +++ b/server/src/lua/class.cpp @@ -100,9 +100,12 @@ #include "../train/train.hpp" #include "../train/trainlist.hpp" +#include "../train/trainzonestatus.hpp" #include "../world/world.hpp" +#include "../zone/zone.hpp" + namespace Lua { static const char* metaTableName = "class"; @@ -219,8 +222,11 @@ void Class::registerValues(lua_State* L) registerValue(L, "TRAIN"); registerValue(L, "TRAIN_LIST"); + registerValue(L, "TRAIN_ZONE_STATUS"); registerValue(L, "WORLD"); + + registerValue(L, "ZONE"); } void Class::push(lua_State* L, std::string_view classId) diff --git a/server/src/lua/enums.hpp b/server/src/lua/enums.hpp index 6e2f6093..6b91996d 100644 --- a/server/src/lua/enums.hpp +++ b/server/src/lua/enums.hpp @@ -41,6 +41,7 @@ #include "../../src/enum/signalaspect.hpp" #include "../../src/enum/worldevent.hpp" #include "../../src/enum/worldscale.hpp" +#include #define LUA_ENUMS \ BlockTrainDirection, \ @@ -58,7 +59,8 @@ TurnoutPosition, \ SignalAspect, \ WorldEvent, \ - WorldScale + WorldScale, \ + ZoneTrainState namespace Lua { diff --git a/server/src/train/train.cpp b/server/src/train/train.cpp index 60fd9617..fa9260d3 100644 --- a/server/src/train/train.cpp +++ b/server/src/train/train.cpp @@ -36,6 +36,7 @@ #include "../hardware/decoder/decoder.hpp" #include "../utils/almostzero.hpp" #include "../utils/displayname.hpp" +#include "../zone/zone.hpp" CREATE_IMPL(Train) @@ -145,12 +146,19 @@ Train::Train(World& world, std::string_view _id) : std::bind(&Train::setTrainActive, this, std::placeholders::_1)}, mode{this, "mode", TrainMode::ManualUnprotected, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}, blocks{*this, "blocks", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState}, + zones{*this, "zones", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}, notes{this, "notes", "", PropertyFlags::ReadWrite | PropertyFlags::Store} , onBlockAssigned{*this, "on_block_assigned", EventFlags::Scriptable} , onBlockReserved{*this, "on_block_reserved", EventFlags::Scriptable} , onBlockEntered{*this, "on_block_entered", EventFlags::Scriptable} , onBlockLeft{*this, "on_block_left", EventFlags::Scriptable} , onBlockRemoved{*this, "on_block_removed", EventFlags::Scriptable} + , onZoneAssigned{*this, "on_zone_assigned", EventFlags::Scriptable} + , onZoneEntering{*this, "on_zone_entering", EventFlags::Scriptable} + , onZoneEntered{*this, "on_zone_entered", EventFlags::Scriptable} + , onZoneLeaving{*this, "on_zone_leaving", EventFlags::Scriptable} + , onZoneLeft{*this, "on_zone_left", EventFlags::Scriptable} + , onZoneRemoved{*this, "on_zone_removed", EventFlags::Scriptable} { vehicles.setValueInternal(std::make_shared(*this, vehicles.name())); @@ -199,6 +207,9 @@ Train::Train(World& world, std::string_view _id) : Attributes::addObjectEditor(blocks, false); m_interfaceItems.add(blocks); + Attributes::addObjectEditor(zones, false); + m_interfaceItems.add(zones); + Attributes::addObjectEditor(powered, false); m_interfaceItems.add(powered); Attributes::addDisplayName(notes, DisplayName::Object::notes); @@ -209,6 +220,12 @@ Train::Train(World& world, std::string_view _id) : m_interfaceItems.add(onBlockEntered); m_interfaceItems.add(onBlockLeft); m_interfaceItems.add(onBlockRemoved); + m_interfaceItems.add(onZoneAssigned); + m_interfaceItems.add(onZoneEntering); + m_interfaceItems.add(onZoneEntered); + m_interfaceItems.add(onZoneLeaving); + m_interfaceItems.add(onZoneLeft); + m_interfaceItems.add(onZoneRemoved); updateEnabled(); } @@ -485,3 +502,33 @@ void Train::fireBlockRemoved(const std::shared_ptr& block) shared_ptr(), block); } + +void Train::fireZoneAssigned(const std::shared_ptr& zone) +{ + fireEvent(onZoneAssigned, shared_ptr(), zone); +} + +void Train::fireZoneEntering(const std::shared_ptr& zone) +{ + fireEvent(onZoneEntering, shared_ptr(), zone); +} + +void Train::fireZoneEntered(const std::shared_ptr& zone) +{ + fireEvent(onZoneEntered, shared_ptr(), zone); +} + +void Train::fireZoneLeaving(const std::shared_ptr& zone) +{ + fireEvent(onZoneLeaving, shared_ptr(), zone); +} + +void Train::fireZoneLeft(const std::shared_ptr& zone) +{ + fireEvent(onZoneLeft, shared_ptr(), zone); +} + +void Train::fireZoneRemoved(const std::shared_ptr& zone) +{ + fireEvent(onZoneRemoved, shared_ptr(), zone); +} diff --git a/server/src/train/train.hpp b/server/src/train/train.hpp index cf1d0c52..023eef54 100644 --- a/server/src/train/train.hpp +++ b/server/src/train/train.hpp @@ -38,8 +38,10 @@ class TrainVehicleList; class TrainBlockStatus; +class TrainZoneStatus; class BlockRailTile; class PoweredRailVehicle; +class Zone; class Train : public IdObject { @@ -74,6 +76,13 @@ class Train : public IdObject void fireBlockEntered(const std::shared_ptr& block, BlockTrainDirection trainDirection); void fireBlockLeft(const std::shared_ptr& block, BlockTrainDirection trainDirection); + void fireZoneAssigned(const std::shared_ptr& zone); + void fireZoneEntering(const std::shared_ptr& zone); + void fireZoneEntered(const std::shared_ptr& zone); + void fireZoneLeaving(const std::shared_ptr& zone); + void fireZoneLeft(const std::shared_ptr& zone); + void fireZoneRemoved(const std::shared_ptr& zone); + protected: void addToWorld() override; void destroying() override; @@ -105,12 +114,19 @@ class Train : public IdObject //! Index 0 is the block where the head of the train is. //! If the train changes direction this list will be reversed. ObjectVectorProperty blocks; + ObjectVectorProperty zones; Property notes; Event&, const std::shared_ptr&> onBlockAssigned; Event&, const std::shared_ptr&, BlockTrainDirection> onBlockReserved; Event&, const std::shared_ptr&, BlockTrainDirection> onBlockEntered; Event&, const std::shared_ptr&, BlockTrainDirection> onBlockLeft; Event&, const std::shared_ptr&> onBlockRemoved; + Event&, const std::shared_ptr&> onZoneAssigned; + Event&, const std::shared_ptr&> onZoneEntering; + Event&, const std::shared_ptr&> onZoneEntered; + Event&, const std::shared_ptr&> onZoneLeaving; + Event&, const std::shared_ptr&> onZoneLeft; + Event&, const std::shared_ptr&> onZoneRemoved; Train(World& world, std::string_view _id); diff --git a/server/src/train/traintracking.cpp b/server/src/train/traintracking.cpp index 544fd418..db0938f1 100644 --- a/server/src/train/traintracking.cpp +++ b/server/src/train/traintracking.cpp @@ -25,16 +25,39 @@ #include "trainblockstatus.hpp" #include "../board/tile/rail/blockrailtile.hpp" #include "../core/objectproperty.tpp" +#include "../zone/blockzonelist.hpp" + +void TrainTracking::assigned(const std::shared_ptr& train, const std::shared_ptr& block) +{ + assert(train); + assert(block); + checkZoneAssigned(train, block); + train->fireBlockAssigned(block); + block->fireTrainAssigned(train); +} void TrainTracking::reserve(const std::shared_ptr& train, const std::shared_ptr& block, BlockTrainDirection direction) { + assert(train); + assert(block); + + checkZoneLeaving(train, block); + block->trains.appendInternal(TrainBlockStatus::create(*block, *train, direction)); + + checkZoneEntering(train, block); + train->fireBlockReserved(block, direction); block->fireTrainReserved(train, direction); } void TrainTracking::enter(const std::shared_ptr& train, const std::shared_ptr& block, BlockTrainDirection direction) { + assert(train); + assert(block); + + checkZoneLeaving(train, block); + auto status = TrainBlockStatus::create(*block, *train, direction); block->trains.appendInternal(status); @@ -43,14 +66,17 @@ void TrainTracking::enter(const std::shared_ptr& train, const std::shared enter(status); } -void TrainTracking::enter(const std::shared_ptr& status) +void TrainTracking::enter(const std::shared_ptr& blockStatus) { - assert(status); - const auto& train = status->train.value(); - const auto& block = status->block.value(); - const auto direction = status->direction.value(); + assert(blockStatus); + const auto& train = blockStatus->train.value(); + const auto& block = blockStatus->block.value(); + const auto direction = blockStatus->direction.value(); - status->train->blocks.insertInternal(0, status); // head of train + blockStatus->train->blocks.insertInternal(0, blockStatus); // head of train + + checkZoneEntering(train, block); + checkZoneEntered(train, block); train->fireBlockEntered(block, direction); block->fireTrainEntered(train, direction); @@ -73,25 +99,195 @@ void TrainTracking::enter(const std::shared_ptr& status) } } -void TrainTracking::left(std::shared_ptr status) +void TrainTracking::left(std::shared_ptr blockStatus) { - const auto& train = status->train.value(); - const auto& block = status->block.value(); - const auto direction = status->direction.value(); + assert(blockStatus); + const auto& train = blockStatus->train.value(); + const auto& block = blockStatus->block.value(); + const auto direction = blockStatus->direction.value(); - train->blocks.removeInternal(status); - block->trains.removeInternal(status); + train->blocks.removeInternal(blockStatus); + block->trains.removeInternal(blockStatus); block->updateTrainMethodEnabled(); block->updateState(); + checkZoneEntered(train, block); + checkZoneLeft(train, block); + train->fireBlockLeft(block, direction); block->fireTrainLeft(train, direction); - status->destroy(); + blockStatus->destroy(); #ifndef NDEBUG - std::weak_ptr statusWeak = status; - status.reset(); - assert(statusWeak.expired()); + std::weak_ptr blockStatusWeak = blockStatus; + blockStatus.reset(); + assert(blockStatusWeak.expired()); #endif } + +void TrainTracking::removed(const std::shared_ptr& train, const std::shared_ptr& block) +{ + assert(train); + assert(block); + checkZoneRemoved(train, block); + train->fireBlockRemoved(block); + block->fireTrainRemoved(train); +} + +void TrainTracking::checkZoneAssigned(const std::shared_ptr& train, const std::shared_ptr& block) +{ + assert(train); + assert(block); + + for(auto& zone : *block->zones) + { + auto zoneStatus = zone->getTrainZoneStatus(train); + if(zoneStatus) + { + continue; // train already known + } + + zoneStatus = TrainZoneStatus::create(*zone, *train, ZoneTrainState::Entered); + zone->trains.appendInternal(zoneStatus); + train->zones.appendInternal(zoneStatus); + + train->fireZoneAssigned(zone); + zone->fireTrainAssigned(train); + } +} + +void TrainTracking::checkZoneEntering(const std::shared_ptr& train, const std::shared_ptr& block) +{ + assert(train); + assert(block); + + for(auto& zone : *block->zones) + { + auto zoneStatus = zone->getTrainZoneStatus(train); + if(zoneStatus && (zoneStatus->state == ZoneTrainState::Entering || zoneStatus->state == ZoneTrainState::Entered)) + { + continue; // train already in or entering zone + } + + if(!zoneStatus) + { + zoneStatus = TrainZoneStatus::create(*zone, *train, ZoneTrainState::Entering); + zone->trains.appendInternal(zoneStatus); + train->zones.appendInternal(zoneStatus); + } + else + { + assert(zoneStatus->state.value() == ZoneTrainState::Leaving); + zoneStatus->state.setValueInternal(ZoneTrainState::Entering); + } + + zone->fireTrainEntering(train); + train->fireZoneEntering(zone); + } +} + +void TrainTracking::checkZoneEntered(const std::shared_ptr& train, const std::shared_ptr& block) +{ + assert(train); + assert(block); + + for(auto& zone : *block->zones) + { + auto zoneStatus = zone->getTrainZoneStatus(train); + if(zoneStatus && zoneStatus->state == ZoneTrainState::Entered) + { + continue; // train already in zone + } + + if(!zoneStatus) + { + zoneStatus = TrainZoneStatus::create(*zone, *train, ZoneTrainState::Entered); + zone->trains.appendInternal(zoneStatus); + train->zones.appendInternal(zoneStatus); + } + else + { + assert(zoneStatus->state == ZoneTrainState::Entering || zoneStatus->state.value() == ZoneTrainState::Leaving); + zoneStatus->state.setValueInternal(ZoneTrainState::Entered); + } + + zone->fireTrainEntered(train); + train->fireZoneEntered(zone); + } +} + +void TrainTracking::checkZoneLeaving(const std::shared_ptr& train, const std::shared_ptr& block) +{ + assert(train); + assert(block); + + for(const auto& zoneStatus : *train->zones) + { + const auto& zone = zoneStatus->zone.value(); + if(!block->zones->containsObject(zone)) + { + zoneStatus->state.setValueInternal(ZoneTrainState::Leaving); + zone->fireTrainLeaving(train); + train->fireZoneLeaving(zone); + } + } +} + +void TrainTracking::checkZoneLeft(const std::shared_ptr& train, const std::shared_ptr& block) +{ + assert(train); + assert(block); + + for(const auto& zone : *block->zones) + { + if(removeTrainIfNotInZone(train, zone)) + { + zone->fireTrainLeft(train); + train->fireZoneLeft(zone); + } + } +} + +void TrainTracking::checkZoneRemoved(const std::shared_ptr& train, const std::shared_ptr& block) +{ + assert(train); + assert(block); + + for(auto& zone : *block->zones) + { + if(removeTrainIfNotInZone(train, zone)) + { + train->fireZoneRemoved(zone); + zone->fireTrainRemoved(train); + } + } +} + +bool TrainTracking::removeTrainIfNotInZone(const std::shared_ptr& train, const std::shared_ptr& zone) +{ + assert(train); + assert(zone); + + if(std::find_if(train->blocks.begin(), train->blocks.end(), + [&zone](const auto& trainBlockStatus) + { + return trainBlockStatus->block->zones->containsObject(zone); + }) == train->blocks.end()) + { + auto zoneStatus = zone->getTrainZoneStatus(train); + + zone->trains.removeInternal(zoneStatus); + train->zones.removeInternal(zoneStatus); + + zoneStatus->destroy(); +#ifndef NDEBUG + std::weak_ptr zoneStatusWeak = zoneStatus; + zoneStatus.reset(); + assert(zoneStatusWeak.expired()); +#endif + + return true; // removed + } + return false; +} diff --git a/server/src/train/traintracking.hpp b/server/src/train/traintracking.hpp index 9a4491a9..14823180 100644 --- a/server/src/train/traintracking.hpp +++ b/server/src/train/traintracking.hpp @@ -29,6 +29,7 @@ class Train; class BlockRailTile; class TrainBlockStatus; +class Zone; class TrainTracking final { @@ -37,11 +38,22 @@ private: TrainTracking(const TrainTracking&) = delete; TrainTracking& operator=(const TrainTracking&) = delete; + static void checkZoneAssigned(const std::shared_ptr& train, const std::shared_ptr& block); + static void checkZoneEntering(const std::shared_ptr& train, const std::shared_ptr& block); + static void checkZoneEntered(const std::shared_ptr& train, const std::shared_ptr& block); + static void checkZoneLeaving(const std::shared_ptr& train, const std::shared_ptr& block); + static void checkZoneLeft(const std::shared_ptr& train, const std::shared_ptr& block); + static void checkZoneRemoved(const std::shared_ptr& train, const std::shared_ptr& block); + + static bool removeTrainIfNotInZone(const std::shared_ptr& train, const std::shared_ptr& zone); + public: + static void assigned(const std::shared_ptr& train, const std::shared_ptr& block); static void reserve(const std::shared_ptr& train, const std::shared_ptr& block, BlockTrainDirection direction); static void enter(const std::shared_ptr& train, const std::shared_ptr& block, BlockTrainDirection direction); // enter unreserved block - static void enter(const std::shared_ptr& status); // enter reserved block - static void left(std::shared_ptr status); + static void enter(const std::shared_ptr& blockStatus); // enter reserved block + static void left(std::shared_ptr blockStatus); + static void removed(const std::shared_ptr& train, const std::shared_ptr& block); }; #endif diff --git a/server/src/train/trainzonestatus.cpp b/server/src/train/trainzonestatus.cpp new file mode 100644 index 00000000..c6880340 --- /dev/null +++ b/server/src/train/trainzonestatus.cpp @@ -0,0 +1,64 @@ +/** + * server/src/train/trainzonestatus.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 "trainzonestatus.hpp" +#include "train.hpp" +#include "../core/objectproperty.tpp" +#include "../core/attributes.hpp" +#include "../world/world.hpp" +#include "../zone/zone.hpp" + +std::shared_ptr TrainZoneStatus::create(Zone& zone_, Train& train_, ZoneTrainState state_, std::string_view id) +{ + World& world = zone_.world(); + auto p = std::make_shared(id.empty() ? world.getUniqueId(TrainZoneStatus::classId) : std::string{id}); + p->zone.setValueInternal(zone_.shared_ptr()); + p->train.setValueInternal(train_.shared_ptr()); + p->state.setValueInternal(state_); + TrainZoneStatus::addToWorld(world, *p); + return p; +} + +TrainZoneStatus::TrainZoneStatus(std::string id) + : StateObject(std::move(id)) + , zone{this, "zone", nullptr, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly} + , train{this, "train", nullptr, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly} + , state{this, "state", ZoneTrainState::Unknown, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly} +{ + m_interfaceItems.add(zone); + + m_interfaceItems.add(train); + + Attributes::addValues(state, zoneTrainStateValues); + m_interfaceItems.add(state); +} + +void TrainZoneStatus::destroying() +{ + auto self = shared_ptr(); + if(zone) + zone->trains.removeInternal(self); + if(train) + train->zones.removeInternal(self); + removeFromWorld(zone->world(), *this); + StateObject::destroying(); +} diff --git a/server/src/train/trainzonestatus.hpp b/server/src/train/trainzonestatus.hpp new file mode 100644 index 00000000..c4be1417 --- /dev/null +++ b/server/src/train/trainzonestatus.hpp @@ -0,0 +1,52 @@ +/** + * server/src/train/trainzonestatus.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_TRAIN_TRAINZONESTATUS_HPP +#define TRAINTASTIC_SERVER_TRAIN_TRAINZONESTATUS_HPP + +#include "../core/stateobject.hpp" +#include "../core/property.hpp" +#include "../core/objectproperty.hpp" +#include + +class Zone; +class Train; + +//! \brief Represents the state of a train within a zone +class TrainZoneStatus final : public StateObject +{ + CLASS_ID("train_zone_status"); + +protected: + void destroying() final; + +public: + static std::shared_ptr create(Zone& zone_, Train& train_, ZoneTrainState state_, std::string_view id = {}); + + ObjectProperty zone; + ObjectProperty train; + Property state; + + TrainZoneStatus(std::string id); +}; + +#endif diff --git a/server/src/world/world.cpp b/server/src/world/world.cpp index e200ed41..ab35389c 100644 --- a/server/src/world/world.cpp +++ b/server/src/world/world.cpp @@ -59,10 +59,14 @@ #include "../board/board.hpp" #include "../board/boardlist.hpp" +#include "../board/list/blockrailtilelist.hpp" #include "../board/list/linkrailtilelist.hpp" #include "../board/nx/nxmanager.hpp" #include "../board/tile/rail/nxbuttonrailtile.hpp" +#include "../zone/zone.hpp" +#include "../zone/zonelist.hpp" + #include "../train/train.hpp" #include "../train/trainlist.hpp" #include "../vehicle/rail/railvehiclelist.hpp" @@ -113,11 +117,13 @@ void World::init(World& world) world.outputs.setValueInternal(std::make_shared(world, world.outputs.name(), outputListColumns)); world.identifications.setValueInternal(std::make_shared(world, world.outputs.name(), identificationListColumns)); world.boards.setValueInternal(std::make_shared(world, world.boards.name())); + world.zones.setValueInternal(std::make_shared(world, world.zones.name())); world.clock.setValueInternal(std::make_shared(world, world.clock.name())); world.trains.setValueInternal(std::make_shared(world, world.trains.name())); world.railVehicles.setValueInternal(std::make_shared(world, world.railVehicles.name())); world.luaScripts.setValueInternal(std::make_shared(world, world.luaScripts.name())); + world.blockRailTiles.setValueInternal(std::make_shared(world, world.blockRailTiles.name())); world.linkRailTiles.setValueInternal(std::make_shared(world, world.linkRailTiles.name())); world.nxManager.setValueInternal(std::make_shared(world, world.nxManager.name())); } @@ -155,10 +161,12 @@ World::World(Private /*unused*/) : outputs{this, "outputs", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore}, identifications{this, "identifications", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore}, boards{this, "boards", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}, + zones{this, "zones", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}, clock{this, "clock", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::Store | PropertyFlags::ScriptReadOnly}, trains{this, "trains", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}, railVehicles{this, "rail_vehicles", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}, luaScripts{this, "lua_scripts", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore}, + blockRailTiles{this, "block_rail_tiles", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore}, linkRailTiles{this, "link_rail_tiles", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore}, nxManager{this, "nx_manager", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore}, statuses(*this, "statuses", {}, PropertyFlags::ReadOnly | PropertyFlags::Store), @@ -331,6 +339,10 @@ World::World(Private /*unused*/) : m_interfaceItems.add(identifications); Attributes::addObjectEditor(boards, false); m_interfaceItems.add(boards); + + Attributes::addObjectEditor(zones, false); + m_interfaceItems.add(zones); + Attributes::addObjectEditor(clock, false); m_interfaceItems.add(clock); Attributes::addObjectEditor(trains, false); @@ -340,6 +352,9 @@ World::World(Private /*unused*/) : Attributes::addObjectEditor(luaScripts, false); m_interfaceItems.add(luaScripts); + Attributes::addObjectEditor(blockRailTiles, false); + m_interfaceItems.add(blockRailTiles); + Attributes::addObjectEditor(linkRailTiles, false); m_interfaceItems.add(linkRailTiles); Attributes::addObjectEditor(nxManager, false); @@ -395,6 +410,7 @@ World::~World() deleteAll(*inputs); deleteAll(*identifications); deleteAll(*boards); + deleteAll(*zones); deleteAll(*trains); deleteAll(*railVehicles); deleteAll(*luaScripts); diff --git a/server/src/world/world.hpp b/server/src/world/world.hpp index d3ed3525..701d1f93 100644 --- a/server/src/world/world.hpp +++ b/server/src/world/world.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 @@ -49,6 +49,8 @@ class InputList; class OutputList; class IdentificationList; class BoardList; +class ZoneList; +class BlockRailTileList; class LinkRailTileList; class NXManager; class Clock; @@ -115,11 +117,13 @@ class World : public Object ObjectProperty outputs; ObjectProperty identifications; ObjectProperty boards; + ObjectProperty zones; ObjectProperty clock; ObjectProperty trains; ObjectProperty railVehicles; ObjectProperty luaScripts; + ObjectProperty blockRailTiles; ObjectProperty linkRailTiles; ObjectProperty nxManager; diff --git a/server/src/world/worldloader.cpp b/server/src/world/worldloader.cpp index db80978c..9a177d70 100644 --- a/server/src/world/worldloader.cpp +++ b/server/src/world/worldloader.cpp @@ -43,6 +43,7 @@ #include "../train/train.hpp" #include "../train/trainblockstatus.hpp" #include "../lua/script.hpp" +#include "../zone/zone.hpp" using nlohmann::json; @@ -274,8 +275,20 @@ void WorldLoader::createObject(ObjectData& objectData) } } } + else if(classId == TrainZoneStatus::classId) + { + if(auto zone = std::dynamic_pointer_cast(getObject(objectData.json["zone"].get()))) /*[[likely]]*/ + { + auto train = std::dynamic_pointer_cast(getObject(objectData.json["train"].get())); + objectData.object = TrainZoneStatus::create(*zone, *train, to(objectData.json["state"]), id); + } + } else if(classId == Lua::Script::classId) objectData.object = Lua::Script::create(*m_world, id); + else if(classId == Zone::classId) + { + objectData.object = Zone::create(*m_world, id); + } if(!objectData.object) throw LogMessageException(LogMessage::C1012_UNKNOWN_CLASS_X_CANT_RECREATE_OBJECT_X, classId, id); diff --git a/server/src/zone/blockzonelist.cpp b/server/src/zone/blockzonelist.cpp new file mode 100644 index 00000000..957d9fe2 --- /dev/null +++ b/server/src/zone/blockzonelist.cpp @@ -0,0 +1,86 @@ +/** + * server/src/zone/blockzonelist.cpp + * + * This file is part of the zonetastic 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 "blockzonelist.hpp" +#include "zoneblocklist.hpp" +#include "zonelisttablemodel.hpp" +#include "zone.hpp" +#include "../world/getworld.hpp" +#include "../world/world.hpp" +#include "../core/attributes.hpp" +#include "../core/method.tpp" +#include "../core/objectproperty.tpp" +#include "../utils/displayname.hpp" + +BlockZoneList::BlockZoneList(BlockRailTile& block_, std::string_view parentPropertyName) + : ObjectList(block_, parentPropertyName) + , add{*this, "add", + [this](const std::shared_ptr& zone) + { + if(!containsObject(zone)) + { + addObject(zone); + zone->blocks->add(parent().shared_ptr()); + } + }} + , remove{*this, "remove", + [this](const std::shared_ptr& zone) + { + if(containsObject(zone)) + { + removeObject(zone); + zone->blocks->remove(parent().shared_ptr()); + } + }} +{ + const auto& world = getWorld(parent()); + + Attributes::addDisplayName(add, DisplayName::List::add); + Attributes::addEnabled(add, false); + Attributes::addObjectList(add, world.zones); + m_interfaceItems.add(add); + + Attributes::addDisplayName(remove, DisplayName::List::remove); + Attributes::addEnabled(remove, false); + m_interfaceItems.add(remove); +} + +TableModelPtr BlockZoneList::getModel() +{ + return std::make_shared(*this); +} + +void BlockZoneList::worldEvent(WorldState worldState, WorldEvent worldEvent) +{ + ObjectList::worldEvent(worldState, worldEvent); + Attributes::setEnabled({add, remove}, contains(worldState, WorldState::Edit) && !contains(worldState, WorldState::Run)); +} + +bool BlockZoneList::isListedProperty(std::string_view name) +{ + return ZoneListTableModel::isListedProperty(name); +} + +BlockRailTile& BlockZoneList::block() +{ + return static_cast(parent()); +} diff --git a/server/src/zone/blockzonelist.hpp b/server/src/zone/blockzonelist.hpp new file mode 100644 index 00000000..1106a01e --- /dev/null +++ b/server/src/zone/blockzonelist.hpp @@ -0,0 +1,53 @@ +/** + * server/src/zone/blockzonelist.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_ZONE_BLOCKZONELIST_HPP +#define TRAINTASTIC_SERVER_ZONE_BLOCKZONELIST_HPP + +#include "../core/method.hpp" +#include "../core/objectlist.hpp" +#include "zone.hpp" + +class BlockRailTile; +class Zone; + +class BlockZoneList : public ObjectList +{ + CLASS_ID("list.zone_block") + +private: + inline BlockRailTile& block(); + +protected: + void worldEvent(WorldState worldState, WorldEvent worldEvent) override; + bool isListedProperty(std::string_view name) final; + +public: + Method&)> add; + Method&)> remove; + + BlockZoneList(BlockRailTile& block_, std::string_view parentPropertyName); + + TableModelPtr getModel() final; +}; + +#endif diff --git a/server/src/zone/zone.cpp b/server/src/zone/zone.cpp new file mode 100644 index 00000000..a6ab924e --- /dev/null +++ b/server/src/zone/zone.cpp @@ -0,0 +1,124 @@ +/** + * server/src/zone/zone.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 "zone.hpp" +#include "zoneblocklist.hpp" +#include "zonelist.hpp" +#include "zonelisttablemodel.hpp" +#include "../core/attributes.hpp" +#include "../core/objectproperty.tpp" +#include "../core/objectvectorproperty.tpp" +#include "../train/train.hpp" +#include "../utils/displayname.hpp" +#include "../world/world.hpp" + +CREATE_IMPL(Zone) + +Zone::Zone(World& world, std::string_view id_) + : IdObject(world, id_) + , name{this, "name", id, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly} + , blocks{this, "blocks", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject} + , trains{*this, "trains", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly} + , onTrainAssigned{*this, "on_train_assigned", EventFlags::Scriptable} + , onTrainEntering{*this, "on_train_entering", EventFlags::Scriptable} + , onTrainEntered{*this, "on_train_entered", EventFlags::Scriptable} + , onTrainLeaving{*this, "on_train_leaving", EventFlags::Scriptable} + , onTrainLeft{*this, "on_train_left", EventFlags::Scriptable} + , onTrainRemoved{*this, "on_train_removed", EventFlags::Scriptable} +{ + blocks.setValueInternal(std::make_shared(*this, blocks.name())); + + const bool editable = contains(world.state.value(), WorldState::Edit); + + Attributes::addDisplayName(name, DisplayName::Object::name); + Attributes::addEnabled(name, editable); + m_interfaceItems.add(name); + + m_interfaceItems.add(blocks); + + Attributes::addObjectEditor(trains, false); + m_interfaceItems.add(trains); + + m_interfaceItems.add(onTrainAssigned); + m_interfaceItems.add(onTrainEntering); + m_interfaceItems.add(onTrainEntered); + m_interfaceItems.add(onTrainLeaving); + m_interfaceItems.add(onTrainLeft); + m_interfaceItems.add(onTrainRemoved); +} + +std::shared_ptr Zone::getTrainZoneStatus(const std::shared_ptr& train) +{ + auto it = std::find_if(trains.begin(), trains.end(), + [&train](const auto& status) + { + return status->train.value() == train; + }); + + return (it != trains.end()) ? *it : std::shared_ptr{}; +} + +void Zone::worldEvent(WorldState worldState, WorldEvent worldEvent) +{ + IdObject::worldEvent(worldState, worldEvent); + + const bool editable = contains(worldState, WorldState::Edit); + + Attributes::setEnabled(name, editable); +} + +void Zone::addToWorld() +{ + IdObject::addToWorld(); + + m_world.zones->addObject(shared_ptr()); +} + +void Zone::fireTrainAssigned(const std::shared_ptr& train) +{ + fireEvent(onTrainAssigned, train, shared_ptr()); +} + +void Zone::fireTrainEntering(const std::shared_ptr& train) +{ + fireEvent(onTrainEntering, train, shared_ptr()); +} + +void Zone::fireTrainEntered(const std::shared_ptr& train) +{ + fireEvent(onTrainEntered, train, shared_ptr()); +} + +void Zone::fireTrainLeaving(const std::shared_ptr& train) +{ + fireEvent(onTrainLeaving, train, shared_ptr()); +} + +void Zone::fireTrainLeft(const std::shared_ptr& train) +{ + fireEvent(onTrainLeft, train, shared_ptr()); +} + +void Zone::fireTrainRemoved(const std::shared_ptr& train) +{ + fireEvent(onTrainRemoved, train, shared_ptr()); +} diff --git a/server/src/zone/zone.hpp b/server/src/zone/zone.hpp new file mode 100644 index 00000000..84fae54e --- /dev/null +++ b/server/src/zone/zone.hpp @@ -0,0 +1,70 @@ +/** + * server/src/zone/zone.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_ZONE_ZONE_HPP +#define TRAINTASTIC_SERVER_ZONE_ZONE_HPP + +#include "../core/idobject.hpp" +#include "../core/event.hpp" +#include "../core/objectproperty.hpp" +#include "../core/objectvectorproperty.hpp" +#include "../train/trainzonestatus.hpp" + +class ZoneBlockList; +class Train; + +class Zone : public IdObject +{ + friend class TrainTracking; + + CLASS_ID("zone") + CREATE_DEF(Zone) + DEFAULT_ID("zone") + +protected: + void worldEvent(WorldState worldState, WorldEvent worldEvent) override; + void addToWorld() override; + + void fireTrainAssigned(const std::shared_ptr& train); + void fireTrainEntering(const std::shared_ptr& train); + void fireTrainEntered(const std::shared_ptr& train); + void fireTrainLeaving(const std::shared_ptr& train); + void fireTrainLeft(const std::shared_ptr& train); + void fireTrainRemoved(const std::shared_ptr& train); + +public: + Zone(World& world, std::string_view id_); + + std::shared_ptr getTrainZoneStatus(const std::shared_ptr& train); + + Property name; + ObjectProperty blocks; + ObjectVectorProperty trains; + Event&, const std::shared_ptr&> onTrainAssigned; + Event&, const std::shared_ptr&> onTrainEntering; + Event&, const std::shared_ptr&> onTrainEntered; + Event&, const std::shared_ptr&> onTrainLeaving; + Event&, const std::shared_ptr&> onTrainLeft; + Event&, const std::shared_ptr&> onTrainRemoved; +}; + +#endif diff --git a/server/src/zone/zoneblocklist.cpp b/server/src/zone/zoneblocklist.cpp new file mode 100644 index 00000000..c988ded8 --- /dev/null +++ b/server/src/zone/zoneblocklist.cpp @@ -0,0 +1,84 @@ +/** + * server/src/zone/zoneblocklist.cpp + * + * This file is part of the zonetastic 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 "zoneblocklist.hpp" +#include "blockzonelist.hpp" +#include "../board/list/blockrailtilelisttablemodel.hpp" +#include "../world/getworld.hpp" +#include "../core/attributes.hpp" +#include "../core/method.tpp" +#include "../core/objectproperty.tpp" +#include "../utils/displayname.hpp" + +ZoneBlockList::ZoneBlockList(Zone& zone_, std::string_view parentPropertyName) + : ObjectList(zone_, parentPropertyName) + , add{*this, "add", + [this](const std::shared_ptr& block) + { + if(!containsObject(block)) + { + addObject(block); + block->zones->add(parent().shared_ptr()); + } + }} + , remove{*this, "remove", + [this](const std::shared_ptr& block) + { + if(containsObject(block)) + { + removeObject(block); + block->zones->remove(parent().shared_ptr()); + } + }} +{ + const auto& world = getWorld(parent()); + + Attributes::addDisplayName(add, DisplayName::List::add); + Attributes::addEnabled(add, false); + Attributes::addObjectList(add, world.blockRailTiles); + m_interfaceItems.add(add); + + Attributes::addDisplayName(remove, DisplayName::List::remove); + Attributes::addEnabled(remove, false); + m_interfaceItems.add(remove); +} + +TableModelPtr ZoneBlockList::getModel() +{ + return std::make_shared(*this); +} + +void ZoneBlockList::worldEvent(WorldState worldState, WorldEvent worldEvent) +{ + ObjectList::worldEvent(worldState, worldEvent); + Attributes::setEnabled({add, remove}, contains(worldState, WorldState::Edit) && !contains(worldState, WorldState::Run)); +} + +bool ZoneBlockList::isListedProperty(std::string_view name) +{ + return BlockRailTileListTableModel::isListedProperty(name); +} + +Zone& ZoneBlockList::zone() +{ + return static_cast(parent()); +} diff --git a/server/src/zone/zoneblocklist.hpp b/server/src/zone/zoneblocklist.hpp new file mode 100644 index 00000000..ce403c2a --- /dev/null +++ b/server/src/zone/zoneblocklist.hpp @@ -0,0 +1,53 @@ +/** + * server/src/zone/zoneblocklist.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_ZONE_ZONEBLOCKLIST_HPP +#define TRAINTASTIC_SERVER_ZONE_ZONEBLOCKLIST_HPP + +#include "../core/method.hpp" +#include "../core/objectlist.hpp" +#include "../board/tile/rail/blockrailtile.hpp" + +class Zone; +class BlockRailTile; + +class ZoneBlockList : public ObjectList +{ +private: + inline Zone& zone(); + +protected: + void worldEvent(WorldState worldState, WorldEvent worldEvent) override; + bool isListedProperty(std::string_view name) final; + +public: + CLASS_ID("list.zone_block") + + Method&)> add; + Method&)> remove; + + ZoneBlockList(Zone& zone_, std::string_view parentPropertyName); + + TableModelPtr getModel() final; +}; + +#endif diff --git a/server/src/zone/zonelist.cpp b/server/src/zone/zonelist.cpp new file mode 100644 index 00000000..cce076a4 --- /dev/null +++ b/server/src/zone/zonelist.cpp @@ -0,0 +1,61 @@ +/** + * server/src/zone/zonelist.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 "zone.hpp" +#include "zonelist.hpp" +#include "zonelisttablemodel.hpp" +#include "../world/world.hpp" +#include "../world/getworld.hpp" +#include "../core/attributes.hpp" +#include "../core/method.tpp" +#include "../utils/displayname.hpp" + +ZoneList::ZoneList(Object& _parent, std::string_view parentPropertyName) + : ObjectList(_parent, parentPropertyName) + , create{*this, "create", + [this]() + { + auto& world = getWorld(parent()); + return Zone::create(world, world.getUniqueId(Zone::defaultId)); + }} + , delete_{*this, "delete", + [this](const std::shared_ptr& zone) + { + deleteMethodHandler(zone); + }} +{ + Attributes::addDisplayName(create, DisplayName::List::create); + m_interfaceItems.add(create); + + Attributes::addDisplayName(delete_, DisplayName::List::delete_); + m_interfaceItems.add(delete_); +} + +TableModelPtr ZoneList::getModel() +{ + return std::make_shared(*this); +} + +bool ZoneList::isListedProperty(std::string_view name) +{ + return ZoneListTableModel::isListedProperty(name); +} diff --git a/server/src/zone/zonelist.hpp b/server/src/zone/zonelist.hpp new file mode 100644 index 00000000..5297cbcc --- /dev/null +++ b/server/src/zone/zonelist.hpp @@ -0,0 +1,47 @@ +/** + * server/src/zone/zonelist.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_ZONE_ZONELIST_HPP +#define TRAINTASTIC_SERVER_ZONE_ZONELIST_HPP + +#include "../core/objectlist.hpp" +#include "../core/method.hpp" + +class Zone; + +class ZoneList : public ObjectList +{ + CLASS_ID("list.zone") + +protected: + bool isListedProperty(std::string_view name) final; + +public: + Method()> create; + Method&)> delete_; + + ZoneList(Object& _parent, std::string_view parentPropertyName); + + TableModelPtr getModel() final; +}; + +#endif diff --git a/server/src/zone/zonelisttablemodel.cpp b/server/src/zone/zonelisttablemodel.cpp new file mode 100644 index 00000000..3b83c531 --- /dev/null +++ b/server/src/zone/zonelisttablemodel.cpp @@ -0,0 +1,77 @@ +/** + * server/src/zone/zonelisttablemodel.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 "zonelisttablemodel.hpp" +#include "zone.hpp" +#include "zonelist.hpp" +#include "../core/objectproperty.tpp" +#include "../utils/displayname.hpp" + +constexpr uint32_t columnId = 0; +constexpr uint32_t columnName = 1; + +bool ZoneListTableModel::isListedProperty(std::string_view name) +{ + return + name == "id" || + name == "name"; +} + +ZoneListTableModel::ZoneListTableModel(ObjectList& list) : + ObjectListTableModel(list) +{ + setColumnHeaders({ + DisplayName::Object::id, + DisplayName::Object::name, + }); +} + +std::string ZoneListTableModel::getText(uint32_t column, uint32_t row) const +{ + if(row < rowCount()) + { + const Zone& zone = getItem(row); + + switch(column) + { + case columnId: + return zone.id; + + case columnName: + return zone.name; + + default: + assert(false); + break; + } + } + + return ""; +} + +void ZoneListTableModel::propertyChanged(BaseProperty& property, uint32_t row) +{ + if(property.name() == "id") + changed(row, columnId); + else if(property.name() == "name") + changed(row, columnName); +} diff --git a/server/src/zone/zonelisttablemodel.hpp b/server/src/zone/zonelisttablemodel.hpp new file mode 100644 index 00000000..0c01efd7 --- /dev/null +++ b/server/src/zone/zonelisttablemodel.hpp @@ -0,0 +1,46 @@ +/** + * server/src/zone/zonelisttablemodel.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_ZONE_ZONELISTTABLEMODEL_HPP +#define TRAINTASTIC_SERVER_ZONE_ZONELISTTABLEMODEL_HPP + +#include "../core/objectlisttablemodel.hpp" + +class Zone; +class ZoneList; + +class ZoneListTableModel : public ObjectListTableModel +{ + CLASS_ID("zone_list_table_model") + +protected: + void propertyChanged(BaseProperty& property, uint32_t row) final; + +public: + static bool isListedProperty(std::string_view name); + + ZoneListTableModel(ObjectList& list); + + std::string getText(uint32_t column, uint32_t row) const final; +}; + +#endif diff --git a/shared/src/traintastic/enum/zonetrainstate.hpp b/shared/src/traintastic/enum/zonetrainstate.hpp new file mode 100644 index 00000000..3138a28a --- /dev/null +++ b/shared/src/traintastic/enum/zonetrainstate.hpp @@ -0,0 +1,54 @@ +/** + * shared/src/traintastic/enum/zonetrainstate.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_SHARED_TRAINTASTIC_ENUM_ZONETRAINSTATE_HPP +#define TRAINTASTIC_SHARED_TRAINTASTIC_ENUM_ZONETRAINSTATE_HPP + +#include +#include +#include "enum.hpp" + +enum class ZoneTrainState : uint8_t +{ + Unknown = 0, + Entering = 1, + Entered = 2, + Leaving = 3, +}; + +TRAINTASTIC_ENUM(ZoneTrainState, "zone_train_state", 4, +{ + {ZoneTrainState::Unknown, "unknown"}, + {ZoneTrainState::Entering, "entering"}, + {ZoneTrainState::Entered, "entered"}, + {ZoneTrainState::Leaving, "leaving"} +}); + +constexpr std::array zoneTrainStateValues +{ + ZoneTrainState::Unknown, + ZoneTrainState::Entering, + ZoneTrainState::Entered, + ZoneTrainState::Leaving, +}; + +#endif diff --git a/shared/translations/en-us.json b/shared/translations/en-us.json index d2c1da8e..9b60dde7 100644 --- a/shared/translations/en-us.json +++ b/shared/translations/en-us.json @@ -4900,5 +4900,13 @@ "reference": "", "comment": "", "fuzzy": 0 + }, + { + "term": "world:zones", + "definition": "Zones" + }, + { + "term": "zone:blocks", + "definition": "Blocks" } -] \ No newline at end of file +] From 7f4bce3be8978750da841690662d713a6d2f8bf3 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 23 Jun 2024 23:32:53 +0200 Subject: [PATCH 02/37] luadoc: added documentation fot train.on_zone_* events see #144 --- manual/luadoc/terms/en-us.json | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/manual/luadoc/terms/en-us.json b/manual/luadoc/terms/en-us.json index 986b39a4..fe76965b 100644 --- a/manual/luadoc/terms/en-us.json +++ b/manual/luadoc/terms/en-us.json @@ -2030,5 +2030,77 @@ { "term": "enum.zone_train_state:title", "definition": "Zone train state" + }, + { + "term": "object.train.on_zone_assigned:description", + "definition": "Fired when the train is assigned to the first block of a zone." + }, + { + "term": "object.train.on_zone_assigned.parameter.train:description", + "definition": "The train." + }, + { + "term": "object.train.on_zone_assigned.parameter.zone:description", + "definition": "The {ref:object.zone|zone} that the train is assigned to." + }, + { + "term": "object.train.on_zone_entered:description", + "definition": "Fired when the train is in a zone, the train is in a zone when it only occupies blocks that are part of a zone." + }, + { + "term": "object.train.on_zone_entered.parameter.train:description", + "definition": "The train." + }, + { + "term": "object.train.on_zone_entered.parameter.zone:description", + "definition": "The {ref:object.zone|zone} that the train has entered." + }, + { + "term": "object.train.on_zone_entering:description", + "definition": "Fired when the train reserves or enters the first block of a zone it currently isn't in." + }, + { + "term": "object.train.on_zone_entering.parameter.train:description", + "definition": "The train." + }, + { + "term": "object.train.on_zone_entering.parameter.zone:description", + "definition": "The {ref:object.zone|zone} that the train is entering." + }, + { + "term": "object.train.on_zone_leaving:description", + "definition": "Fired when the train reserves or enters a block that is not part of a zone it currently is in." + }, + { + "term": "object.train.on_zone_leaving.parameter.train:description", + "definition": "The train." + }, + { + "term": "object.train.on_zone_leaving.parameter.zone:description", + "definition": "The {ref:object.zone|zone} that the train is leaving." + }, + { + "term": "object.train.on_zone_left:description", + "definition": "Fired when the train is no longer in a zone, the train is not longer in a zone when it doesn't occupy any block that is part of a zone." + }, + { + "term": "object.train.on_zone_left.parameter.train:description", + "definition": "The train." + }, + { + "term": "object.train.on_zone_left.parameter.zone:description", + "definition": "The {ref:object.zone|zone} that the train left." + }, + { + "term": "object.train.on_zone_removed:description", + "definition": "Fired when the train is removed from the last block of a zone." + }, + { + "term": "object.train.on_zone_removed.parameter.train:description", + "definition": "The train." + }, + { + "term": "object.train.on_zone_removed.parameter.zone:description", + "definition": "The {ref:object.zone|zone} that the train is removed from." } ] From f45fe36bf275d38057433e5b4544246c72140aa4 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Mon, 23 Sep 2024 23:12:08 +0200 Subject: [PATCH 03/37] zone mute/no smoke support todo: train mute/no smoke see #144 --- manual/luadoc/object/train.json | 2 ++ manual/luadoc/object/zone.json | 2 ++ server/src/train/train.cpp | 56 ++++++++++++++++++++++++++++-- server/src/train/train.hpp | 5 +++ server/src/train/traintracking.cpp | 17 +++++++++ server/src/train/traintracking.hpp | 2 ++ server/src/zone/zone.cpp | 24 ++++++++++++- server/src/zone/zone.hpp | 2 ++ 8 files changed, 107 insertions(+), 3 deletions(-) diff --git a/manual/luadoc/object/train.json b/manual/luadoc/object/train.json index 6dc8c909..ae91d3ce 100644 --- a/manual/luadoc/object/train.json +++ b/manual/luadoc/object/train.json @@ -6,6 +6,8 @@ "powered": {}, "active": {}, "mode": {}, + "mute": {}, + "no_smoke": {}, "on_block_assigned": { "parameters": [ { diff --git a/manual/luadoc/object/zone.json b/manual/luadoc/object/zone.json index 14fbcee0..f1a7d7bd 100644 --- a/manual/luadoc/object/zone.json +++ b/manual/luadoc/object/zone.json @@ -1,5 +1,7 @@ { "name": {}, + "mute": {}, + "no_smoke": {}, "trains": {}, "on_train_assigned": { "parameters": [ diff --git a/server/src/train/train.cpp b/server/src/train/train.cpp index fa9260d3..357667ef 100644 --- a/server/src/train/train.cpp +++ b/server/src/train/train.cpp @@ -144,8 +144,10 @@ Train::Train(World& world, std::string_view _id) : updateSpeed(); }, std::bind(&Train::setTrainActive, this, std::placeholders::_1)}, - mode{this, "mode", TrainMode::ManualUnprotected, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}, - blocks{*this, "blocks", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState}, + mode{this, "mode", TrainMode::ManualUnprotected, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly} + , mute{this, "mute", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly} + , noSmoke{this, "no_smoke", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly} + , blocks{*this, "blocks", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState}, zones{*this, "zones", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}, notes{this, "notes", "", PropertyFlags::ReadWrite | PropertyFlags::Store} , onBlockAssigned{*this, "on_block_assigned", EventFlags::Scriptable} @@ -204,6 +206,12 @@ Train::Train(World& world, std::string_view _id) : Attributes::addValues(mode, trainModeValues); m_interfaceItems.add(mode); + Attributes::addObjectEditor(mute, false); + m_interfaceItems.add(mute); + + Attributes::addObjectEditor(noSmoke, false); + m_interfaceItems.add(noSmoke); + Attributes::addObjectEditor(blocks, false); m_interfaceItems.add(blocks); @@ -228,6 +236,48 @@ Train::Train(World& world, std::string_view _id) : m_interfaceItems.add(onZoneRemoved); updateEnabled(); + updateMute(); + updateNoSmoke(); +} + +void Train::updateMute() +{ + bool value = contains(m_world.state, WorldState::Mute); + if(!value) + { + for(const auto& zoneStatus : zones) + { + if(zoneStatus->zone->mute) + { + value = true; + break; + } + } + } + if(value != mute) + { + mute.setValueInternal(value); + } +} + +void Train::updateNoSmoke() +{ + bool value = contains(m_world.state, WorldState::Mute); + if(!value) + { + for(const auto& zoneStatus : zones) + { + if(zoneStatus->zone->noSmoke) + { + value = true; + break; + } + } + } + if(value != noSmoke) + { + noSmoke.setValueInternal(value); + } } void Train::addToWorld() @@ -255,6 +305,8 @@ void Train::loaded() Attributes::setEnabled(weight, overrideWeight); vehiclesChanged(); + updateMute(); + updateNoSmoke(); } void Train::worldEvent(WorldState state, WorldEvent event) diff --git a/server/src/train/train.hpp b/server/src/train/train.hpp index 023eef54..5c42b8ee 100644 --- a/server/src/train/train.hpp +++ b/server/src/train/train.hpp @@ -109,6 +109,8 @@ class Train : public IdObject Property powered; Property active; Property mode; + Property mute; + Property noSmoke; //! \brief List of block status the train is in //! Index 0 is the block where the head of the train is. @@ -130,6 +132,9 @@ class Train : public IdObject Train(World& world, std::string_view _id); + void updateMute(); + void updateNoSmoke(); + void fireBlockAssigned(const std::shared_ptr& block); void fireBlockRemoved(const std::shared_ptr& block); }; diff --git a/server/src/train/traintracking.cpp b/server/src/train/traintracking.cpp index 4098c2a5..877b5056 100644 --- a/server/src/train/traintracking.cpp +++ b/server/src/train/traintracking.cpp @@ -230,6 +230,8 @@ void TrainTracking::checkZoneEntered(const std::shared_ptr& train, const zoneStatus->state.setValueInternal(ZoneTrainState::Entered); } + trainEnteredOrLeftZone(*train, *zone); + zone->fireTrainEntered(train); train->fireZoneEntered(zone); } @@ -261,6 +263,8 @@ void TrainTracking::checkZoneLeft(const std::shared_ptr& train, const std { if(removeTrainIfNotInZone(train, zone)) { + trainEnteredOrLeftZone(*train, *zone); + zone->fireTrainLeft(train); train->fireZoneLeft(zone); } @@ -309,3 +313,16 @@ bool TrainTracking::removeTrainIfNotInZone(const std::shared_ptr& train, } return false; } + +void TrainTracking::trainEnteredOrLeftZone(Train& train, Zone& zone) +{ + // Re-evaluate zone limitations: + if(zone.mute) + { + train.updateMute(); + } + if(zone.noSmoke) + { + train.updateNoSmoke(); + } +} diff --git a/server/src/train/traintracking.hpp b/server/src/train/traintracking.hpp index 14823180..1d9ac458 100644 --- a/server/src/train/traintracking.hpp +++ b/server/src/train/traintracking.hpp @@ -47,6 +47,8 @@ private: static bool removeTrainIfNotInZone(const std::shared_ptr& train, const std::shared_ptr& zone); + static void trainEnteredOrLeftZone(Train& train, Zone& zone); + public: static void assigned(const std::shared_ptr& train, const std::shared_ptr& block); static void reserve(const std::shared_ptr& train, const std::shared_ptr& block, BlockTrainDirection direction); diff --git a/server/src/zone/zone.cpp b/server/src/zone/zone.cpp index a6ab924e..7fa7dda1 100644 --- a/server/src/zone/zone.cpp +++ b/server/src/zone/zone.cpp @@ -36,6 +36,22 @@ CREATE_IMPL(Zone) Zone::Zone(World& world, std::string_view id_) : IdObject(world, id_) , name{this, "name", id, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly} + , mute{this, "mute", false, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly, + [this](bool /*value*/) + { + for(auto& status : trains) + { + status->train->updateMute(); + } + }} + , noSmoke{this, "no_smoke", false, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly, + [this](bool /*value*/) + { + for(auto& status : trains) + { + status->train->updateNoSmoke(); + } + }} , blocks{this, "blocks", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject} , trains{*this, "trains", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly} , onTrainAssigned{*this, "on_train_assigned", EventFlags::Scriptable} @@ -53,6 +69,12 @@ Zone::Zone(World& world, std::string_view id_) Attributes::addEnabled(name, editable); m_interfaceItems.add(name); + Attributes::addEnabled(mute, editable); + m_interfaceItems.add(mute); + + Attributes::addEnabled(noSmoke, editable); + m_interfaceItems.add(noSmoke); + m_interfaceItems.add(blocks); Attributes::addObjectEditor(trains, false); @@ -83,7 +105,7 @@ void Zone::worldEvent(WorldState worldState, WorldEvent worldEvent) const bool editable = contains(worldState, WorldState::Edit); - Attributes::setEnabled(name, editable); + Attributes::setEnabled({name, mute, noSmoke}, editable); } void Zone::addToWorld() diff --git a/server/src/zone/zone.hpp b/server/src/zone/zone.hpp index 84fae54e..0894e65a 100644 --- a/server/src/zone/zone.hpp +++ b/server/src/zone/zone.hpp @@ -57,6 +57,8 @@ public: std::shared_ptr getTrainZoneStatus(const std::shared_ptr& train); Property name; + Property mute; + Property noSmoke; ObjectProperty blocks; ObjectVectorProperty trains; Event&, const std::shared_ptr&> onTrainAssigned; From 09362f9b3b9404e3c8b508b78fab921527c36986 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Tue, 24 Sep 2024 22:34:02 +0200 Subject: [PATCH 04/37] added strings and lua documentation for zones see #144 --- manual/luadoc/object/train.json | 1 + manual/luadoc/terms/en-us.json | 12 ++++++++++++ shared/translations/en-us.json | 8 ++++++++ 3 files changed, 21 insertions(+) diff --git a/manual/luadoc/object/train.json b/manual/luadoc/object/train.json index ae91d3ce..95fe3e14 100644 --- a/manual/luadoc/object/train.json +++ b/manual/luadoc/object/train.json @@ -8,6 +8,7 @@ "mode": {}, "mute": {}, "no_smoke": {}, + "zones": {}, "on_block_assigned": { "parameters": [ { diff --git a/manual/luadoc/terms/en-us.json b/manual/luadoc/terms/en-us.json index 61ef200c..3020248d 100644 --- a/manual/luadoc/terms/en-us.json +++ b/manual/luadoc/terms/en-us.json @@ -2206,5 +2206,17 @@ { "term": "object.train.on_zone_removed.parameter.zone:description", "definition": "The {ref:object.zone|zone} that the train is removed from." + }, + { + "term": "object.zone.mute:description", + "definition": "If `true`, all trains in the zone are muted." + }, + { + "term": "object.zone.no_smoke:description", + "definition": "If `true`, smoke generaters are disabled of all trains in the zone." + }, + { + "term": "object.zone.trains:description", + "definition": "List of {ref:object.trainzonestatus|train zone status} objects of all trains that are entering, entered or leaving the zone." } ] diff --git a/shared/translations/en-us.json b/shared/translations/en-us.json index 1f9b280e..377ec99d 100644 --- a/shared/translations/en-us.json +++ b/shared/translations/en-us.json @@ -6423,5 +6423,13 @@ { "term": "zone:blocks", "definition": "Blocks" + }, + { + "term": "zone:mute", + "definition": "Mute" + }, + { + "term": "zone:no_smoke", + "definition": "No smoke" } ] From 8161080ceaf25de573490fd2633b3b25a2ee2fc6 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Tue, 24 Sep 2024 23:04:11 +0200 Subject: [PATCH 05/37] fix: Mute -> NoSmoke see #144 --- server/src/train/train.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/train/train.cpp b/server/src/train/train.cpp index 357667ef..3c441b4f 100644 --- a/server/src/train/train.cpp +++ b/server/src/train/train.cpp @@ -262,7 +262,7 @@ void Train::updateMute() void Train::updateNoSmoke() { - bool value = contains(m_world.state, WorldState::Mute); + bool value = contains(m_world.state, WorldState::NoSmoke); if(!value) { for(const auto& zoneStatus : zones) From d7d2df57b327b375e15caaf91b59f6316e2511ca Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Wed, 25 Sep 2024 18:26:15 +0200 Subject: [PATCH 06/37] Added zone icon see #144 --- client/gfx/dark/dark.qrc | 1 + client/gfx/dark/zone.svg | 143 +++++++++++++++++++++++++++++++++++++ client/gfx/light/light.qrc | 1 + client/gfx/light/zone.svg | 143 +++++++++++++++++++++++++++++++++++++ client/src/theme/theme.cpp | 2 + 5 files changed, 290 insertions(+) create mode 100644 client/gfx/dark/zone.svg create mode 100644 client/gfx/light/zone.svg diff --git a/client/gfx/dark/dark.qrc b/client/gfx/dark/dark.qrc index d5ee3158..ad2073e3 100644 --- a/client/gfx/dark/dark.qrc +++ b/client/gfx/dark/dark.qrc @@ -92,5 +92,6 @@ board_tile.rail.nx_button.svg board_tile.misc.switch.svg board_tile.misc.label.svg + zone.svg diff --git a/client/gfx/dark/zone.svg b/client/gfx/dark/zone.svg new file mode 100644 index 00000000..d4688db3 --- /dev/null +++ b/client/gfx/dark/zone.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/gfx/light/light.qrc b/client/gfx/light/light.qrc index da7d6ba9..07de66db 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 + zone.svg diff --git a/client/gfx/light/zone.svg b/client/gfx/light/zone.svg new file mode 100644 index 00000000..d37f0a12 --- /dev/null +++ b/client/gfx/light/zone.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/theme/theme.cpp b/client/src/theme/theme.cpp index 962c619e..68e546f8 100644 --- a/client/src/theme/theme.cpp +++ b/client/src/theme/theme.cpp @@ -56,6 +56,8 @@ QIcon Theme::getIconForClassId(const QString& classId) return getIcon("clock"); else if(classId == "train" || classId == "list.train") return getIcon("train"); + else if(classId == "zone" || classId == "list.zone") + return getIcon("zone"); else return QIcon(); } From 63692354a8b245a4b6910e909aafcad616242605 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sat, 26 Oct 2024 21:06:28 +0200 Subject: [PATCH 07/37] fix: after zone creation block couldn't be added see #144 --- server/src/zone/zoneblocklist.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/zone/zoneblocklist.cpp b/server/src/zone/zoneblocklist.cpp index c988ded8..e5064c89 100644 --- a/server/src/zone/zoneblocklist.cpp +++ b/server/src/zone/zoneblocklist.cpp @@ -51,14 +51,15 @@ ZoneBlockList::ZoneBlockList(Zone& zone_, std::string_view parentPropertyName) }} { const auto& world = getWorld(parent()); + const bool editable = contains(world.state.value(), WorldState::Edit); Attributes::addDisplayName(add, DisplayName::List::add); - Attributes::addEnabled(add, false); + Attributes::addEnabled(add, editable); Attributes::addObjectList(add, world.blockRailTiles); m_interfaceItems.add(add); Attributes::addDisplayName(remove, DisplayName::List::remove); - Attributes::addEnabled(remove, false); + Attributes::addEnabled(remove, editable); m_interfaceItems.add(remove); } From 9e180172b6c5fae7ea99d5cbfd0f1cbfe4d60dbc Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sat, 26 Oct 2024 21:11:31 +0200 Subject: [PATCH 08/37] fix: zone create/delete didn't require edit mode see #144 --- server/src/zone/zonelist.cpp | 14 ++++++++++++++ server/src/zone/zonelist.hpp | 1 + 2 files changed, 15 insertions(+) diff --git a/server/src/zone/zonelist.cpp b/server/src/zone/zonelist.cpp index cce076a4..69faa65d 100644 --- a/server/src/zone/zonelist.cpp +++ b/server/src/zone/zonelist.cpp @@ -43,10 +43,14 @@ ZoneList::ZoneList(Object& _parent, std::string_view parentPropertyName) deleteMethodHandler(zone); }} { + const bool editable = contains(getWorld(parent()).state.value(), WorldState::Edit); + Attributes::addDisplayName(create, DisplayName::List::create); + Attributes::addEnabled(create, editable); m_interfaceItems.add(create); Attributes::addDisplayName(delete_, DisplayName::List::delete_); + Attributes::addEnabled(delete_, editable); m_interfaceItems.add(delete_); } @@ -55,6 +59,16 @@ TableModelPtr ZoneList::getModel() return std::make_shared(*this); } +void ZoneList::worldEvent(WorldState state, WorldEvent event) +{ + ObjectList::worldEvent(state, event); + + const bool editable = contains(state, WorldState::Edit); + + Attributes::setEnabled(create, editable); + Attributes::setEnabled(delete_, editable); +} + bool ZoneList::isListedProperty(std::string_view name) { return ZoneListTableModel::isListedProperty(name); diff --git a/server/src/zone/zonelist.hpp b/server/src/zone/zonelist.hpp index 5297cbcc..04af78cf 100644 --- a/server/src/zone/zonelist.hpp +++ b/server/src/zone/zonelist.hpp @@ -33,6 +33,7 @@ class ZoneList : public ObjectList CLASS_ID("list.zone") protected: + void worldEvent(WorldState state, WorldEvent event) final; bool isListedProperty(std::string_view name) final; public: From f255442e4e1eb33b20f2923f7d138e6fa8cb04ae Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Wed, 30 Oct 2024 23:17:41 +0100 Subject: [PATCH 09/37] added highlight zone on board toolbar item --- client/gfx/dark/dark.qrc | 1 + client/gfx/dark/highlight_zone.svg | 111 ++++++++++++++++++ client/gfx/light/highlight_zone.svg | 111 ++++++++++++++++++ client/gfx/light/light.qrc | 1 + client/src/board/blockhighlight.cpp | 55 +++++++++ client/src/board/blockhighlight.hpp | 60 ++++++++++ client/src/board/boardareawidget.cpp | 36 +++++- client/src/board/boardareawidget.hpp | 3 + client/src/board/boardcolorscheme.cpp | 4 +- client/src/board/boardcolorscheme.hpp | 3 +- client/src/mainwindow.cpp | 2 + client/src/mainwindow.hpp | 7 ++ client/src/misc/colorpool.cpp | 57 +++++++++ client/src/misc/colorpool.hpp | 43 +++++++ client/src/widget/createwidget.cpp | 5 + client/src/widget/list/listwidget.cpp | 33 +++--- client/src/widget/list/listwidget.hpp | 2 + .../widget/objectlist/zoneblocklistwidget.cpp | 102 ++++++++++++++++ .../widget/objectlist/zoneblocklistwidget.hpp | 46 ++++++++ client/src/widget/tablewidget.cpp | 35 ++++++ client/src/widget/tablewidget.hpp | 3 + shared/translations/en-us.json | 4 + 22 files changed, 705 insertions(+), 19 deletions(-) create mode 100644 client/gfx/dark/highlight_zone.svg create mode 100644 client/gfx/light/highlight_zone.svg create mode 100644 client/src/board/blockhighlight.cpp create mode 100644 client/src/board/blockhighlight.hpp create mode 100644 client/src/misc/colorpool.cpp create mode 100644 client/src/misc/colorpool.hpp create mode 100644 client/src/widget/objectlist/zoneblocklistwidget.cpp create mode 100644 client/src/widget/objectlist/zoneblocklistwidget.hpp diff --git a/client/gfx/dark/dark.qrc b/client/gfx/dark/dark.qrc index b7eb8258..2984284b 100644 --- a/client/gfx/dark/dark.qrc +++ b/client/gfx/dark/dark.qrc @@ -94,5 +94,6 @@ board_tile.misc.label.svg zone.svg clear_persistent_variables.svg + highlight_zone.svg diff --git a/client/gfx/dark/highlight_zone.svg b/client/gfx/dark/highlight_zone.svg new file mode 100644 index 00000000..3844ad33 --- /dev/null +++ b/client/gfx/dark/highlight_zone.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/gfx/light/highlight_zone.svg b/client/gfx/light/highlight_zone.svg new file mode 100644 index 00000000..51c0a6ca --- /dev/null +++ b/client/gfx/light/highlight_zone.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/gfx/light/light.qrc b/client/gfx/light/light.qrc index 20e05821..e56d290a 100644 --- a/client/gfx/light/light.qrc +++ b/client/gfx/light/light.qrc @@ -68,5 +68,6 @@ board_tile.misc.label.svg zone.svg clear_persistent_variables.svg + highlight_zone.svg diff --git a/client/src/board/blockhighlight.cpp b/client/src/board/blockhighlight.cpp new file mode 100644 index 00000000..9b40815b --- /dev/null +++ b/client/src/board/blockhighlight.cpp @@ -0,0 +1,55 @@ +/** + * client/src/board/blockhighlight.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 "blockhighlight.hpp" + +BlockHighlight::BlockHighlight(QObject* parent) + : QObject(parent) + , colorPool{{Color::Fuchsia, Color::Green, Color::Blue, Color::Lime, Color::Purple, Color::Red, Color::Aqua}} +{ +} + +void BlockHighlight::add(const QString& blockId, Color color) +{ + auto& colors = m_blockColors[blockId]; + if(!colors.contains(color)) + { + colors.append(color); + emit colorsChanged(blockId, colors); + } +} + +void BlockHighlight::remove(const QString& blockId, Color color) +{ + if(auto it = m_blockColors.find(blockId); it != m_blockColors.end()) + { + if(int index = it->indexOf(color); index >= 0) + { + it->remove(index); + emit colorsChanged(blockId, *it); + if(it->isEmpty()) + { + m_blockColors.remove(blockId); + } + } + } +} diff --git a/client/src/board/blockhighlight.hpp b/client/src/board/blockhighlight.hpp new file mode 100644 index 00000000..0c5baef8 --- /dev/null +++ b/client/src/board/blockhighlight.hpp @@ -0,0 +1,60 @@ +/** + * client/src/board/blockhighlight.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_BOARD_BLOCKHIGHLIGHT_HPP +#define TRAINTASTIC_CLIENT_BOARD_BLOCKHIGHLIGHT_HPP + +#include +#include +#include +#include +#include +#include "../misc/colorpool.hpp" + +class BlockHighlight : public QObject +{ + Q_OBJECT + +public: + using BlockColors = QMap>; + +private: + BlockColors m_blockColors; + +public: + ColorPool colorPool; + + explicit BlockHighlight(QObject* parent); + + const BlockColors& blockColors() const + { + return m_blockColors; + } + + void add(const QString& blockId, Color color); + void remove(const QString& blockId, Color color); + +signals: + void colorsChanged(const QString& blockId, const QVector& colors); +}; + +#endif diff --git a/client/src/board/boardareawidget.cpp b/client/src/board/boardareawidget.cpp index 47391096..916fa0dc 100644 --- a/client/src/board/boardareawidget.cpp +++ b/client/src/board/boardareawidget.cpp @@ -31,6 +31,8 @@ #include "boardwidget.hpp" #include "getboardcolorscheme.hpp" #include "tilepainter.hpp" +#include "blockhighlight.hpp" +#include "../mainwindow.hpp" #include "../network/board.hpp" #include "../network/callmethod.hpp" #include "../network/object.tpp" @@ -76,6 +78,7 @@ BoardAreaWidget::BoardAreaWidget(BoardWidget& board, QWidget* parent) : m_boardBottom{board.board().getProperty("bottom")}, m_grid{Grid::Dot}, m_zoomLevel{0}, + m_blockHighlight{MainWindow::instance->blockHighlight()}, m_mouseLeftButtonPressed{false}, m_mouseRightButtonPressed{false}, m_mouseMoveAction{MouseMoveAction::None}, @@ -99,6 +102,12 @@ BoardAreaWidget::BoardAreaWidget(BoardWidget& board, QWidget* parent) : connect(&BoardSettings::instance(), &SettingsBase::changed, this, &BoardAreaWidget::settingsChanged); + connect(&m_blockHighlight, &BlockHighlight::colorsChanged, this, + [this](const QString& /*blockId*/, const QVector& /*colors*/) + { + update(); + }); + for(const auto& [l, object] : m_board.board().tileObjects()) tileObjectAdded(l.x, l.y, object); @@ -730,9 +739,32 @@ void BoardAreaWidget::paintEvent(QPaintEvent* event) break; case TileId::RailBlock: - tilePainter.drawBlock(id, r, a, state & 0x01, state & 0x02, m_board.board().getTileObject(it.first)); + { + auto block = m_board.board().getTileObject(it.first); + tilePainter.drawBlock(id, r, a, state & 0x01, state & 0x02, block); + if(auto itColors = m_blockHighlight.blockColors().find(block->getPropertyValueString("id")); + itColors != m_blockHighlight.blockColors().end() && !itColors->isEmpty()) + { + for(int i = 0; i < itColors->size(); ++i) + { + QColor color = toQColor((*itColors)[i]); + painter.setPen({}); + color.setAlphaF(m_colorScheme->blockHighlightAlpha); + painter.setBrush(color); + if(a == TileRotate::Deg0) + { + const auto h = r.height() / itColors->size(); + painter.drawRect(r.left(), r.top() + i * h, r.width(), h); + } + else + { + const auto w = r.width() / itColors->size(); + painter.drawRect(r.left() + i * w, r.top(), w, r.height()); + } + } + } break; - + } case TileId::RailDirectionControl: tilePainter.drawDirectionControl(id, r, a, isReserved, getDirectionControlState(it.first)); break; diff --git a/client/src/board/boardareawidget.hpp b/client/src/board/boardareawidget.hpp index 14eb166f..7d8f8359 100644 --- a/client/src/board/boardareawidget.hpp +++ b/client/src/board/boardareawidget.hpp @@ -39,6 +39,7 @@ #include "../network/objectptr.hpp" class BoardWidget; +class BlockHighlight; class BoardAreaWidget : public QWidget { @@ -74,6 +75,8 @@ class BoardAreaWidget : public QWidget Grid m_grid; int m_zoomLevel; + BlockHighlight& m_blockHighlight; + bool m_mouseLeftButtonPressed; TileLocation m_mouseLeftButtonPressedTileLocation; bool m_mouseRightButtonPressed; diff --git a/client/src/board/boardcolorscheme.cpp b/client/src/board/boardcolorscheme.cpp index 296c5f89..cfb5fedc 100644 --- a/client/src/board/boardcolorscheme.cpp +++ b/client/src/board/boardcolorscheme.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 @@ -42,6 +42,7 @@ const BoardColorScheme BoardColorScheme::dark = { /*.turnoutState =*/ {Qt::blue}, /*.decouplerDeactivated =*/ {0x10, 0x10, 0x10}, /*.decouplerActivated =*/ {0x00, 0xBF, 0xFF}, + /*.blockHighlightAlpha =*/ 0.2, }; const BoardColorScheme BoardColorScheme::light = { @@ -64,4 +65,5 @@ const BoardColorScheme BoardColorScheme::light = { /*.turnoutState =*/ {Qt::cyan}, /*.decouplerDeactivated =*/ {0xF5, 0xF5, 0xF5}, /*.decouplerActivated =*/ {0x00, 0xBF, 0xFF}, + /*.blockHighlightAlpha =*/ 0.3, }; diff --git a/client/src/board/boardcolorscheme.hpp b/client/src/board/boardcolorscheme.hpp index a0e19c5a..3e8549cf 100644 --- a/client/src/board/boardcolorscheme.hpp +++ b/client/src/board/boardcolorscheme.hpp @@ -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 @@ -49,6 +49,7 @@ struct BoardColorScheme const QColor turnoutState; const QColor decouplerDeactivated; const QColor decouplerActivated; + const qreal blockHighlightAlpha; }; #endif diff --git a/client/src/mainwindow.cpp b/client/src/mainwindow.cpp index 28a576d5..ad5d296c 100644 --- a/client/src/mainwindow.cpp +++ b/client/src/mainwindow.cpp @@ -39,6 +39,7 @@ #include #include "mdiarea.hpp" #include "mainwindow/mainwindowstatusbar.hpp" +#include "board/blockhighlight.hpp" #include "clock/clock.hpp" #include "dialog/connectdialog.hpp" #include "settings/settingsdialog.hpp" @@ -111,6 +112,7 @@ MainWindow::MainWindow(QWidget* parent) : m_mdiArea{new MdiArea(m_splitter)}, m_statusBar{new MainWindowStatusBar(*this)}, m_serverLog{nullptr}, + m_blockHighlight{new BlockHighlight(this)}, m_toolbar{new QToolBar(this)} { instance = this; diff --git a/client/src/mainwindow.hpp b/client/src/mainwindow.hpp index 307d1459..65bfb272 100644 --- a/client/src/mainwindow.hpp +++ b/client/src/mainwindow.hpp @@ -47,6 +47,7 @@ class IntroductionWizard; class AddInterfaceWizard; class NewBoardWizard; class WorldListDialog; +class BlockHighlight; class MainWindow final : public QMainWindow { @@ -67,6 +68,7 @@ class MainWindow final : public QMainWindow QMdiSubWindow* m_clockWindow = nullptr; QMap m_subWindows; QMdiSubWindow* m_trainAndRailVehiclesSubWindow = nullptr; + BlockHighlight* m_blockHighlight; // Main menu: QAction* m_actionConnectToServer; QAction* m_actionDisconnectFromServer; @@ -133,6 +135,11 @@ class MainWindow final : public QMainWindow MainWindow(QWidget *parent = nullptr); ~MainWindow() final; + BlockHighlight& blockHighlight() + { + return *m_blockHighlight; + } + const std::shared_ptr& connection() { return m_connection; } const ObjectPtr& world() const; diff --git a/client/src/misc/colorpool.cpp b/client/src/misc/colorpool.cpp new file mode 100644 index 00000000..1e611a34 --- /dev/null +++ b/client/src/misc/colorpool.cpp @@ -0,0 +1,57 @@ +/** + * client/src/misc/colorpool.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 "colorpool.hpp" + +ColorPool::ColorPool(std::initializer_list colors) +{ + m_colors.reserve(colors.size()); + for(auto color : colors) + { + m_colors.emplace_back(std::make_pair(color, false)); + } +} + +Color ColorPool::aquire() +{ + for(auto& item : m_colors) + { + if(!item.second) + { + item.second = true; + return item.first; + } + } + return Color::None; +} + +void ColorPool::release(Color color) +{ + for(auto& item: m_colors) + { + if(item.first == color) + { + item.second = false; + break; + } + } +} diff --git a/client/src/misc/colorpool.hpp b/client/src/misc/colorpool.hpp new file mode 100644 index 00000000..da80cc03 --- /dev/null +++ b/client/src/misc/colorpool.hpp @@ -0,0 +1,43 @@ +/** + * client/src/misc/colorpool.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_COLORPOOL_HPP +#define TRAINTASTIC_CLIENT_MISC_COLORPOOL_HPP + +#include +#include +#include +#include + +class ColorPool +{ +private: + std::vector> m_colors; + +public: + explicit ColorPool(std::initializer_list colors); + + Color aquire(); + void release(Color color); +}; + +#endif diff --git a/client/src/widget/createwidget.cpp b/client/src/widget/createwidget.cpp index 7f382e67..5eda036a 100644 --- a/client/src/widget/createwidget.cpp +++ b/client/src/widget/createwidget.cpp @@ -25,6 +25,7 @@ #include "objectlist/boardlistwidget.hpp" #include "objectlist/throttleobjectlistwidget.hpp" #include "objectlist/trainlistwidget.hpp" +#include "objectlist/zoneblocklistwidget.hpp" #include "object/luascripteditwidget.hpp" #include "object/objecteditwidget.hpp" #include "object/itemseditwidget.hpp" @@ -66,6 +67,10 @@ QWidget* createWidgetIfCustom(const ObjectPtr& object, QWidget* parent) { return new TrainListWidget(object, parent); } + if(classId == "list.zone_block") + { + return new ZoneBlockListWidget(object, parent); + } else if(object->classId().startsWith("list.")) return new ObjectListWidget(object, parent); else if(classId == "lua.script") diff --git a/client/src/widget/list/listwidget.cpp b/client/src/widget/list/listwidget.cpp index 0629e9b2..2c2faa4d 100644 --- a/client/src/widget/list/listwidget.cpp +++ b/client/src/widget/list/listwidget.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 @@ -53,20 +53,7 @@ ListWidget::ListWidget(const ObjectPtr& object, QWidget* parent) if(tableModel) { m_requestId = Connection::invalidRequestId; - - m_tableWidget->setTableModel(tableModel); - connect(m_tableWidget, &TableWidget::doubleClicked, this, - [this](const QModelIndex& index) - { - tableDoubleClicked(index); - }); - connect(m_tableWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, - [this](const QItemSelection&, const QItemSelection&) - { - tableSelectionChanged(); - }); - tableSelectionChanged(); - + setTableModel(tableModel); delete spinner; } else if(error) @@ -84,3 +71,19 @@ ListWidget::~ListWidget() { object()->connection()->cancelRequest(m_requestId); } + +void ListWidget::setTableModel(const TableModelPtr& tableModel) +{ + m_tableWidget->setTableModel(tableModel); + connect(m_tableWidget, &TableWidget::doubleClicked, this, + [this](const QModelIndex& index) + { + tableDoubleClicked(index); + }); + connect(m_tableWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this](const QItemSelection&, const QItemSelection&) + { + tableSelectionChanged(); + }); + tableSelectionChanged(); +} diff --git a/client/src/widget/list/listwidget.hpp b/client/src/widget/list/listwidget.hpp index a4904f47..848f2bd3 100644 --- a/client/src/widget/list/listwidget.hpp +++ b/client/src/widget/list/listwidget.hpp @@ -25,6 +25,7 @@ #include #include "../../network/objectptr.hpp" +#include "../../network/tablemodelptr.hpp" class TableWidget; @@ -39,6 +40,7 @@ protected: const ObjectPtr& object() const { return m_object; } + virtual void setTableModel(const TableModelPtr& tableModel); virtual void tableSelectionChanged() {} virtual void tableDoubleClicked(const QModelIndex& /*index*/) {} diff --git a/client/src/widget/objectlist/zoneblocklistwidget.cpp b/client/src/widget/objectlist/zoneblocklistwidget.cpp new file mode 100644 index 00000000..64e8b0c5 --- /dev/null +++ b/client/src/widget/objectlist/zoneblocklistwidget.cpp @@ -0,0 +1,102 @@ +/** + * client/src/widget/objectlist/zoneblocklistwidget.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 "zoneblocklistwidget.hpp" +#include +#include +#include +#include "../../theme/theme.hpp" +#include "../../network/object.hpp" +#include "../../network/connection.hpp" +#include "../../network/tablemodel.hpp" +#include "../../board/blockhighlight.hpp" +#include "../../widget/tablewidget.hpp" +#include "../../mainwindow.hpp" + +ZoneBlockListWidget::ZoneBlockListWidget(const ObjectPtr& object, QWidget* parent) + : ObjectListWidget(object, parent) +{ + m_tableWidget->setFetchAll(true); + toolbar()->addSeparator(); + m_actionHighlight = toolbar()->addAction(Theme::getIcon("highlight_zone"), Locale::tr("list.zone:highlight_zone"), + [this]() + { + updateHighlight(); + }); + m_actionHighlight->setCheckable(true); +} + +ZoneBlockListWidget::~ZoneBlockListWidget() +{ + if(m_actionHighlight->isChecked()) + { + m_actionHighlight->setChecked(false); + updateHighlight(); + } +} + +void ZoneBlockListWidget::setTableModel(const TableModelPtr& tableModel) +{ + ObjectListWidget::setTableModel(tableModel); + connect(tableModel.get(), &TableModel::modelReset, this, + [this]() + { + if(m_actionHighlight->isChecked()) + { + updateHighlight(); + } + }); +} + +void ZoneBlockListWidget::updateHighlight() +{ + const auto highlight = m_actionHighlight->isChecked() ? m_tableWidget->getObjectIds() : QStringList(); + + if(!highlight.isEmpty() && m_highlightColor == Color::None) + { + m_highlightColor = MainWindow::instance->blockHighlight().colorPool.aquire(); + } + + for(const auto& blockId : m_highlight) + { + if(!highlight.contains(blockId)) + { + MainWindow::instance->blockHighlight().remove(blockId, m_highlightColor); + } + } + + for(const auto& blockId : highlight) + { + if(!m_highlight.contains(blockId)) + { + MainWindow::instance->blockHighlight().add(blockId, m_highlightColor); + } + } + + m_highlight = highlight; + + if(m_highlight.isEmpty() && m_highlightColor != Color::None) + { + MainWindow::instance->blockHighlight().colorPool.release(m_highlightColor); + m_highlightColor = Color::None; + } +} diff --git a/client/src/widget/objectlist/zoneblocklistwidget.hpp b/client/src/widget/objectlist/zoneblocklistwidget.hpp new file mode 100644 index 00000000..f00c53fa --- /dev/null +++ b/client/src/widget/objectlist/zoneblocklistwidget.hpp @@ -0,0 +1,46 @@ +/** + * client/src/widget/objectlist/zoneblocklistwidget.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_OBJECTLIST_ZONEBLOCKLISTWIDGET_HPP +#define TRAINTASTIC_CLIENT_WIDGET_OBJECTLIST_ZONEBLOCKLISTWIDGET_HPP + +#include "objectlistwidget.hpp" +#include + +class ZoneBlockListWidget : public ObjectListWidget +{ +private: + Color m_highlightColor = Color::None; + QStringList m_highlight; + QAction* m_actionHighlight; + + void updateHighlight(); + +protected: + void setTableModel(const TableModelPtr& tableModel) override; + +public: + explicit ZoneBlockListWidget(const ObjectPtr& object, QWidget* parent = nullptr); + ~ZoneBlockListWidget() override; +}; + +#endif diff --git a/client/src/widget/tablewidget.cpp b/client/src/widget/tablewidget.cpp index b67da43b..bcaa64c8 100644 --- a/client/src/widget/tablewidget.cpp +++ b/client/src/widget/tablewidget.cpp @@ -54,6 +54,21 @@ QString TableWidget::getRowObjectId(int row) const return m_model ? m_model->getRowObjectId(row) : ""; } +QStringList TableWidget::getObjectIds() const +{ + assert(m_fetchAll); + QStringList ids; + if(m_model) + { + ids.reserve(m_model->rowCount()); + for(int row = 0; row < m_model->rowCount(); ++row) + { + ids.append(m_model->getRowObjectId(row)); + } + } + return ids; +} + void TableWidget::setTableModel(const TableModelPtr& model) { Q_ASSERT(!m_model); @@ -88,14 +103,34 @@ void TableWidget::setTableModel(const TableModelPtr& model) updateRegion(); } +void TableWidget::setFetchAll(bool value) +{ + if(m_fetchAll != value) + { + m_fetchAll = value; + if(m_model) + { + updateRegion(); + } + } +} + void TableWidget::updateRegion() { + assert(m_model); + const int columnCount = m_model->columnCount(); const int rowCount = m_model->rowCount(); if(columnCount == 0 || rowCount == 0) return; + if(m_fetchAll) + { + m_model->setRegion(0, columnCount - 1, 0, rowCount - 1); + return; + } + const QRect r = viewport()->rect(); const QModelIndex topLeft = indexAt(r.topLeft()); diff --git a/client/src/widget/tablewidget.hpp b/client/src/widget/tablewidget.hpp index d34636c1..f7ea5673 100644 --- a/client/src/widget/tablewidget.hpp +++ b/client/src/widget/tablewidget.hpp @@ -34,6 +34,7 @@ class TableWidget : public QTableView TableModelPtr m_model; int m_selectedRow = -1; QPoint m_dragStartPosition; + bool m_fetchAll = false; void mouseMoveEvent(QMouseEvent* event) override; void mousePressEvent(QMouseEvent* event) override; @@ -46,8 +47,10 @@ class TableWidget : public QTableView ~TableWidget() override; QString getRowObjectId(int row) const; + QStringList getObjectIds() const; void setTableModel(const TableModelPtr& model); + void setFetchAll(bool value); signals: void rowDragged(int row); diff --git a/shared/translations/en-us.json b/shared/translations/en-us.json index e5b1881b..3efc000c 100644 --- a/shared/translations/en-us.json +++ b/shared/translations/en-us.json @@ -6432,6 +6432,10 @@ "term": "zone:no_smoke", "definition": "No smoke" }, + { + "term": "list.zone:highlight_zone", + "definition": "Highlight zone" + }, { "term": "message:I9003", "definition": "Cleared persistent variables" From 12f16a1a50141233caac0311c00d832e3c705e31 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 3 Nov 2024 23:45:57 +0100 Subject: [PATCH 10/37] zone: added speed limit property see #144 --- server/src/core/speedlimitproperty.hpp | 5 +++++ server/src/train/train.cpp | 27 ++++++++++++++++++++++++++ server/src/train/train.hpp | 2 ++ server/src/train/traintracking.cpp | 4 ++++ server/src/zone/zone.cpp | 13 ++++++++++++- server/src/zone/zone.hpp | 2 ++ shared/translations/en-us.json | 4 ++++ 7 files changed, 56 insertions(+), 1 deletion(-) diff --git a/server/src/core/speedlimitproperty.hpp b/server/src/core/speedlimitproperty.hpp index 1a496eb5..d1041e56 100644 --- a/server/src/core/speedlimitproperty.hpp +++ b/server/src/core/speedlimitproperty.hpp @@ -39,6 +39,11 @@ public: 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); + + inline bool hasLimit() const + { + return (m_value < noLimitValue); + } }; #endif diff --git a/server/src/train/train.cpp b/server/src/train/train.cpp index c79db2f4..a57ae7d1 100644 --- a/server/src/train/train.cpp +++ b/server/src/train/train.cpp @@ -77,6 +77,7 @@ Train::Train(World& world, std::string_view _id) : isStopped{this, "is_stopped", true, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}, speed{*this, "speed", 0, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadOnly | PropertyFlags::NoStore}, speedMax{*this, "speed_max", 0, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadWrite | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}, + speedLimit{*this, "speed_limit", SpeedLimitProperty::noLimitValue, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}, throttleSpeed{*this, "throttle_speed", 0, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadWrite | PropertyFlags::StoreState, [this](double value, SpeedUnit unit) { @@ -182,6 +183,10 @@ Train::Train(World& world, std::string_view _id) : Attributes::addMinMax(speed, 0., 0., SpeedUnit::KiloMeterPerHour); m_interfaceItems.add(speed); m_interfaceItems.add(speedMax); + + Attributes::addObjectEditor(speedLimit, false); + m_interfaceItems.add(speedLimit); + Attributes::addMinMax(throttleSpeed, 0., 0., SpeedUnit::KiloMeterPerHour); Attributes::addEnabled(throttleSpeed, false); Attributes::addObjectEditor(throttleSpeed, false); @@ -257,6 +262,7 @@ void Train::updateMute() if(value != mute) { mute.setValueInternal(value); + //! \todo Apply to train } } @@ -277,6 +283,27 @@ void Train::updateNoSmoke() if(value != noSmoke) { noSmoke.setValueInternal(value); + //! \todo Apply to train + } +} + +void Train::updateSpeedLimit() +{ + double value = SpeedLimitProperty::noLimitValue; + SpeedUnit unit = speedLimit.unit(); + for(const auto& zoneStatus : zones) + { + const auto& zoneSpeedLimit = zoneStatus->zone->speedLimit; + if(zoneSpeedLimit.getValue(unit) < value) + { + unit = zoneSpeedLimit.unit(); + value = zoneSpeedLimit.getValue(unit); + } + } + if(value != speedLimit.getValue(unit)) + { + speedLimit.setValueInternal(value, unit); + //! \todo Apply to train } } diff --git a/server/src/train/train.hpp b/server/src/train/train.hpp index 5c42b8ee..82838ea3 100644 --- a/server/src/train/train.hpp +++ b/server/src/train/train.hpp @@ -100,6 +100,7 @@ class Train : public IdObject Property isStopped; SpeedProperty speed; SpeedProperty speedMax; + SpeedProperty speedLimit; SpeedProperty throttleSpeed; Method stop; Property emergencyStop; @@ -134,6 +135,7 @@ class Train : public IdObject void updateMute(); void updateNoSmoke(); + void updateSpeedLimit(); void fireBlockAssigned(const std::shared_ptr& block); void fireBlockRemoved(const std::shared_ptr& block); diff --git a/server/src/train/traintracking.cpp b/server/src/train/traintracking.cpp index 877b5056..dd4435ae 100644 --- a/server/src/train/traintracking.cpp +++ b/server/src/train/traintracking.cpp @@ -325,4 +325,8 @@ void TrainTracking::trainEnteredOrLeftZone(Train& train, Zone& zone) { train.updateNoSmoke(); } + if(zone.speedLimit.hasLimit()) + { + train.updateSpeedLimit(); + } } diff --git a/server/src/zone/zone.cpp b/server/src/zone/zone.cpp index 7fa7dda1..e3f458a4 100644 --- a/server/src/zone/zone.cpp +++ b/server/src/zone/zone.cpp @@ -52,6 +52,14 @@ Zone::Zone(World& world, std::string_view id_) status->train->updateNoSmoke(); } }} + , speedLimit{*this, "speed_limit", SpeedLimitProperty::noLimitValue, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly, + [this](double /*value*/, SpeedUnit /*unit*/) + { + for(auto& status : trains) + { + status->train->updateSpeedLimit(); + } + }} , blocks{this, "blocks", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject} , trains{*this, "trains", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly} , onTrainAssigned{*this, "on_train_assigned", EventFlags::Scriptable} @@ -75,6 +83,9 @@ Zone::Zone(World& world, std::string_view id_) Attributes::addEnabled(noSmoke, editable); m_interfaceItems.add(noSmoke); + Attributes::addEnabled(speedLimit, editable); + m_interfaceItems.add(speedLimit); + m_interfaceItems.add(blocks); Attributes::addObjectEditor(trains, false); @@ -105,7 +116,7 @@ void Zone::worldEvent(WorldState worldState, WorldEvent worldEvent) const bool editable = contains(worldState, WorldState::Edit); - Attributes::setEnabled({name, mute, noSmoke}, editable); + Attributes::setEnabled({name, mute, noSmoke, speedLimit}, editable); } void Zone::addToWorld() diff --git a/server/src/zone/zone.hpp b/server/src/zone/zone.hpp index 0894e65a..44e2ef59 100644 --- a/server/src/zone/zone.hpp +++ b/server/src/zone/zone.hpp @@ -27,6 +27,7 @@ #include "../core/event.hpp" #include "../core/objectproperty.hpp" #include "../core/objectvectorproperty.hpp" +#include "../core/speedlimitproperty.hpp" #include "../train/trainzonestatus.hpp" class ZoneBlockList; @@ -59,6 +60,7 @@ public: Property name; Property mute; Property noSmoke; + SpeedLimitProperty speedLimit; ObjectProperty blocks; ObjectVectorProperty trains; Event&, const std::shared_ptr&> onTrainAssigned; diff --git a/shared/translations/en-us.json b/shared/translations/en-us.json index 8326f232..aebc2ecb 100644 --- a/shared/translations/en-us.json +++ b/shared/translations/en-us.json @@ -6436,6 +6436,10 @@ "term": "list.zone:highlight_zone", "definition": "Highlight zone" }, + { + "term": "zone:speed_limit", + "definition": "Speed limit" + }, { "term": "message:I9003", "definition": "Cleared persistent variables" From 46d822fe6ea900f442cec14b5c287917121db6a1 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Mon, 4 Nov 2024 23:17:45 +0100 Subject: [PATCH 11/37] added some test to verify zone related object delete see #144 --- server/test/objectcreatedestroy.cpp | 196 ++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/server/test/objectcreatedestroy.cpp b/server/test/objectcreatedestroy.cpp index c967158d..79aba875 100644 --- a/server/test/objectcreatedestroy.cpp +++ b/server/test/objectcreatedestroy.cpp @@ -26,6 +26,7 @@ #include "../src/core/objectproperty.tpp" #include "../src/board/board.hpp" #include "../src/board/boardlist.hpp" +#include "../src/board/tile/rail/blockrailtile.hpp" #include "../src/hardware/decoder/list/decoderlist.hpp" #include "../src/hardware/interface/interfacelist.hpp" #include "../src/hardware/input/input.hpp" @@ -36,6 +37,10 @@ #include "vehicle/rail/railvehicles.hpp" #include "../src/train/trainlist.hpp" #include "../src/train/train.hpp" +#include "../src/zone/zonelist.hpp" +#include "../src/zone/zone.hpp" +#include "../src/zone/zoneblocklist.hpp" +#include "../src/zone/blockzonelist.hpp" TEST_CASE("Create world => destroy world", "[object-create-destroy]") { @@ -445,3 +450,194 @@ TEST_CASE("Create world and train => destroy train", "[object-create-destroy]") world.reset(); REQUIRE(worldWeak.expired()); } + +TEST_CASE("Create world and zone => destroy world", "[object-create-destroy]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + REQUIRE(zoneWeak.lock()->getClassId() == Zone::classId); + + world.reset(); + REQUIRE(zoneWeak.expired()); + REQUIRE(worldWeak.expired()); +} + +TEST_CASE("Create world and zone => destroy zone", "[object-create-destroy]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + REQUIRE(worldWeak.lock()->zones->length == 0); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + REQUIRE(worldWeak.lock()->zones->length == 1); + + world->zones->delete_(zoneWeak.lock()); + REQUIRE(zoneWeak.expired()); + REQUIRE(worldWeak.lock()->zones->length == 0); + + world.reset(); + REQUIRE(worldWeak.expired()); +} + +TEST_CASE("Create world, board, block and zone => destroy world", "[object-create-destroy]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + REQUIRE(worldWeak.lock()->boards->length == 0); + REQUIRE(worldWeak.lock()->zones->length == 0); + + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + REQUIRE(worldWeak.lock()->boards->length == 1); + + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg0, BlockRailTile::classId, false)); + std::weak_ptr blockWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(blockWeak.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + REQUIRE(worldWeak.lock()->zones->length == 1); + + REQUIRE(blockWeak.lock()->zones->length == 0); + REQUIRE(zoneWeak.lock()->blocks->length == 0); + zoneWeak.lock()->blocks->add(blockWeak.lock()); + REQUIRE(blockWeak.lock()->zones->length == 1); + REQUIRE(blockWeak.lock()->zones->front() == zoneWeak.lock()); + REQUIRE(zoneWeak.lock()->blocks->length == 1); + REQUIRE(zoneWeak.lock()->blocks->front() == blockWeak.lock()); + + world.reset(); + REQUIRE(blockWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(zoneWeak.expired()); + REQUIRE(worldWeak.expired()); +} + +TEST_CASE("Create world, board, block and zone => destroy board", "[object-create-destroy]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + REQUIRE(worldWeak.lock()->boards->length == 0); + REQUIRE(worldWeak.lock()->zones->length == 0); + + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + REQUIRE(worldWeak.lock()->boards->length == 1); + + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg0, BlockRailTile::classId, false)); + std::weak_ptr blockWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(blockWeak.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + REQUIRE(worldWeak.lock()->zones->length == 1); + + REQUIRE(blockWeak.lock()->zones->length == 0); + REQUIRE(zoneWeak.lock()->blocks->length == 0); + zoneWeak.lock()->blocks->add(blockWeak.lock()); + REQUIRE(blockWeak.lock()->zones->length == 1); + REQUIRE(blockWeak.lock()->zones->front() == zoneWeak.lock()); + REQUIRE(zoneWeak.lock()->blocks->length == 1); + REQUIRE(zoneWeak.lock()->blocks->front() == blockWeak.lock()); + + world->boards->delete_(boardWeak.lock()); + REQUIRE(blockWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE_FALSE(zoneWeak.expired()); + REQUIRE(zoneWeak.lock()->blocks->length == 0); + REQUIRE_FALSE(worldWeak.expired()); + + world.reset(); + REQUIRE(zoneWeak.expired()); + REQUIRE(worldWeak.expired()); +} + +TEST_CASE("Create world, board, block and zone => destroy block", "[object-create-destroy]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + REQUIRE(worldWeak.lock()->boards->length == 0); + REQUIRE(worldWeak.lock()->zones->length == 0); + + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + REQUIRE(worldWeak.lock()->boards->length == 1); + + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg0, BlockRailTile::classId, false)); + std::weak_ptr blockWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(blockWeak.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + REQUIRE(worldWeak.lock()->zones->length == 1); + + REQUIRE(blockWeak.lock()->zones->length == 0); + REQUIRE(zoneWeak.lock()->blocks->length == 0); + zoneWeak.lock()->blocks->add(blockWeak.lock()); + REQUIRE(blockWeak.lock()->zones->length == 1); + REQUIRE(blockWeak.lock()->zones->front() == zoneWeak.lock()); + REQUIRE(zoneWeak.lock()->blocks->length == 1); + REQUIRE(zoneWeak.lock()->blocks->front() == blockWeak.lock()); + + REQUIRE(boardWeak.lock()->deleteTile(0, 0)); + REQUIRE(blockWeak.expired()); + REQUIRE_FALSE(boardWeak.expired()); + REQUIRE_FALSE(zoneWeak.expired()); + REQUIRE(zoneWeak.lock()->blocks->length == 0); + REQUIRE_FALSE(worldWeak.expired()); + + world.reset(); + REQUIRE(boardWeak.expired()); + REQUIRE(zoneWeak.expired()); + REQUIRE(worldWeak.expired()); +} + +TEST_CASE("Create world, board, block and zone => destroy zone", "[object-create-destroy]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + REQUIRE(worldWeak.lock()->boards->length == 0); + REQUIRE(worldWeak.lock()->zones->length == 0); + + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + REQUIRE(worldWeak.lock()->boards->length == 1); + + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg0, BlockRailTile::classId, false)); + std::weak_ptr blockWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(blockWeak.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + REQUIRE(worldWeak.lock()->zones->length == 1); + + REQUIRE(blockWeak.lock()->zones->length == 0); + REQUIRE(zoneWeak.lock()->blocks->length == 0); + zoneWeak.lock()->blocks->add(blockWeak.lock()); + REQUIRE(blockWeak.lock()->zones->length == 1); + REQUIRE(blockWeak.lock()->zones->front() == zoneWeak.lock()); + REQUIRE(zoneWeak.lock()->blocks->length == 1); + REQUIRE(zoneWeak.lock()->blocks->front() == blockWeak.lock()); + + world->zones->delete_(zoneWeak.lock()); + REQUIRE(zoneWeak.expired()); + REQUIRE_FALSE(blockWeak.expired()); + REQUIRE(blockWeak.lock()->zones->length == 0); + REQUIRE_FALSE(boardWeak.expired()); + REQUIRE_FALSE(worldWeak.expired()); + + world.reset(); + REQUIRE(blockWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(worldWeak.expired()); +} From c64aab057dcc93a8bfe0e70f898f6f1d490538c0 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Mon, 4 Nov 2024 23:19:25 +0100 Subject: [PATCH 12/37] fixes for proper zone cleanup see #144 --- server/src/board/tile/rail/blockrailtile.cpp | 5 +++++ server/src/zone/zone.cpp | 12 ++++++++++++ server/src/zone/zone.hpp | 1 + 3 files changed, 18 insertions(+) diff --git a/server/src/board/tile/rail/blockrailtile.cpp b/server/src/board/tile/rail/blockrailtile.cpp index 8f59571e..cfe9c7c6 100644 --- a/server/src/board/tile/rail/blockrailtile.cpp +++ b/server/src/board/tile/rail/blockrailtile.cpp @@ -33,6 +33,7 @@ #include "../../../train/traintracking.hpp" #include "../../../utils/displayname.hpp" #include "../../../zone/blockzonelist.hpp" +#include "../../../zone/zoneblocklist.hpp" #include "../../list/blockrailtilelist.hpp" #include "../../list/blockrailtilelisttablemodel.hpp" #include "../../map/blockpath.hpp" @@ -569,6 +570,10 @@ void BlockRailTile::destroying() { trains.back()->destroy(); } + for(const auto& zone : *zones) + { + zone->blocks->remove(self); + } m_world.blockRailTiles->removeObject(self); RailTile::destroying(); } diff --git a/server/src/zone/zone.cpp b/server/src/zone/zone.cpp index e3f458a4..bb8c821c 100644 --- a/server/src/zone/zone.cpp +++ b/server/src/zone/zone.cpp @@ -24,6 +24,7 @@ #include "zoneblocklist.hpp" #include "zonelist.hpp" #include "zonelisttablemodel.hpp" +#include "blockzonelist.hpp" #include "../core/attributes.hpp" #include "../core/objectproperty.tpp" #include "../core/objectvectorproperty.tpp" @@ -126,6 +127,17 @@ void Zone::addToWorld() m_world.zones->addObject(shared_ptr()); } +void Zone::destroying() +{ + auto self = shared_ptr(); + for(const auto& block : *blocks) + { + block->zones->remove(self); + } + m_world.zones->removeObject(self); + IdObject::destroying(); +} + void Zone::fireTrainAssigned(const std::shared_ptr& train) { fireEvent(onTrainAssigned, train, shared_ptr()); diff --git a/server/src/zone/zone.hpp b/server/src/zone/zone.hpp index 44e2ef59..280d9773 100644 --- a/server/src/zone/zone.hpp +++ b/server/src/zone/zone.hpp @@ -44,6 +44,7 @@ class Zone : public IdObject protected: void worldEvent(WorldState worldState, WorldEvent worldEvent) override; void addToWorld() override; + void destroying() override; void fireTrainAssigned(const std::shared_ptr& train); void fireTrainEntering(const std::shared_ptr& train); From 11f96ed040acb83a5ac54080c90c662749713340 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Mon, 4 Nov 2024 23:57:54 +0100 Subject: [PATCH 13/37] fix: missing include for Release build --- server/src/zone/zone.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/zone/zone.cpp b/server/src/zone/zone.cpp index bb8c821c..3b9e6348 100644 --- a/server/src/zone/zone.cpp +++ b/server/src/zone/zone.cpp @@ -26,6 +26,7 @@ #include "zonelisttablemodel.hpp" #include "blockzonelist.hpp" #include "../core/attributes.hpp" +#include "../core/method.tpp" #include "../core/objectproperty.tpp" #include "../core/objectvectorproperty.tpp" #include "../train/train.hpp" From b44ee29b0e340fe05b72131b16fc3660c2e2ee22 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Mon, 7 Apr 2025 23:05:39 +0200 Subject: [PATCH 14/37] [train] eval mute/noSmoke on world events --- server/src/train/train.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/server/src/train/train.cpp b/server/src/train/train.cpp index 48d3d950..105d1299 100644 --- a/server/src/train/train.cpp +++ b/server/src/train/train.cpp @@ -346,7 +346,26 @@ void Train::worldEvent(WorldState state, WorldEvent event) { IdObject::worldEvent(state, event); - updateEnabled(); + switch(event) + { + case WorldEvent::EditEnabled: + case WorldEvent::EditDisabled: + updateEnabled(); + break; + + case WorldEvent::Mute: + case WorldEvent::Unmute: + updateMute(); + break; + + case WorldEvent::NoSmoke: + case WorldEvent::Smoke: + updateNoSmoke(); + break; + + default: + break; + } } void Train::setSpeed(const double kmph) From 06996642ddab485029f778202943b17602228ddd Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Mon, 7 Apr 2025 23:07:04 +0200 Subject: [PATCH 15/37] [railvehicle] added mute/noSmoke properties --- server/src/train/train.cpp | 16 +++++- server/src/vehicle/rail/railvehicle.cpp | 69 ++++++++++++++++++++++--- server/src/vehicle/rail/railvehicle.hpp | 5 ++ 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/server/src/train/train.cpp b/server/src/train/train.cpp index 105d1299..2d2bf327 100644 --- a/server/src/train/train.cpp +++ b/server/src/train/train.cpp @@ -268,7 +268,13 @@ void Train::updateMute() if(value != mute) { mute.setValueInternal(value); - //! \todo Apply to train + if(active) + { + for(const auto& vehicle : *vehicles) + { + vehicle->updateMute(); + } + } } } @@ -289,7 +295,13 @@ void Train::updateNoSmoke() if(value != noSmoke) { noSmoke.setValueInternal(value); - //! \todo Apply to train + if(active) + { + for(const auto& vehicle : *vehicles) + { + vehicle->updateNoSmoke(); + } + } } } diff --git a/server/src/vehicle/rail/railvehicle.cpp b/server/src/vehicle/rail/railvehicle.cpp index 9ee33a01..6ee549f4 100644 --- a/server/src/vehicle/rail/railvehicle.cpp +++ b/server/src/vehicle/rail/railvehicle.cpp @@ -39,8 +39,10 @@ RailVehicle::RailVehicle(World& world, std::string_view _id) : lob{*this, "lob", 0, LengthUnit::MilliMeter, PropertyFlags::ReadWrite | PropertyFlags::Store}, speedMax{*this, "speed_max", 0, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadWrite | PropertyFlags::Store}, weight{*this, "weight", 0, WeightUnit::Ton, PropertyFlags::ReadWrite | PropertyFlags::Store, [this](double /*value*/, WeightUnit /*unit*/){ updateTotalWeight(); }}, - totalWeight{*this, "total_weight", 0, WeightUnit::Ton, PropertyFlags::ReadOnly | PropertyFlags::NoStore}, - activeTrain{this, "active_train", nullptr, PropertyFlags::ReadOnly | PropertyFlags::ScriptReadOnly | PropertyFlags::StoreState} + totalWeight{*this, "total_weight", 0, WeightUnit::Ton, PropertyFlags::ReadOnly | PropertyFlags::NoStore} + , mute{this, "mute", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly} + , noSmoke{this, "no_smoke", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly} + , activeTrain{this, "active_train", nullptr, PropertyFlags::ReadOnly | PropertyFlags::ScriptReadOnly | PropertyFlags::StoreState} , trains{*this, "trains", {}, PropertyFlags::ReadOnly | PropertyFlags::ScriptReadOnly | PropertyFlags::NoStore} { const bool editable = contains(m_world.state.value(), WorldState::Edit); @@ -66,6 +68,12 @@ RailVehicle::RailVehicle(World& world, std::string_view _id) : Attributes::addDisplayName(totalWeight, DisplayName::Vehicle::Rail::totalWeight); m_interfaceItems.insertBefore(totalWeight, notes); + Attributes::addObjectEditor(mute, false); + m_interfaceItems.add(mute); + + Attributes::addObjectEditor(noSmoke, false); + m_interfaceItems.add(noSmoke); + Attributes::addDisplayName(activeTrain, DisplayName::Vehicle::Rail::train); //TODO: "Active" Attributes::addEnabled(activeTrain, true); m_interfaceItems.insertBefore(activeTrain, notes); @@ -74,6 +82,34 @@ RailVehicle::RailVehicle(World& world, std::string_view _id) : m_interfaceItems.insertBefore(trains, notes); } +void RailVehicle::updateMute() +{ + bool value = contains(m_world.state, WorldState::NoSmoke); + if(!value && activeTrain) + { + value |= activeTrain->mute; + } + if(value != mute) + { + mute.setValueInternal(value); + //! \todo update decoder + } +} + +void RailVehicle::updateNoSmoke() +{ + bool value = contains(m_world.state, WorldState::NoSmoke); + if(!value && activeTrain) + { + value |= activeTrain->noSmoke; + } + if(value != noSmoke) + { + noSmoke.setValueInternal(value); + //! \todo update decoder + } +} + void RailVehicle::addToWorld() { Vehicle::addToWorld(); @@ -104,12 +140,31 @@ void RailVehicle::worldEvent(WorldState state, WorldEvent event) { Vehicle::worldEvent(state, event); - const bool editable = contains(state, WorldState::Edit); + switch(event) + { + case WorldEvent::EditEnabled: + case WorldEvent::EditDisabled: + { + const bool editable = contains(state, WorldState::Edit); + Attributes::setEnabled(decoder, editable); + Attributes::setEnabled(lob, editable); + Attributes::setEnabled(speedMax, editable); + Attributes::setEnabled(weight, editable); + break; + } + case WorldEvent::Mute: + case WorldEvent::Unmute: + updateMute(); + break; - Attributes::setEnabled(decoder, editable); - Attributes::setEnabled(lob, editable); - Attributes::setEnabled(speedMax, editable); - Attributes::setEnabled(weight, editable); + case WorldEvent::NoSmoke: + case WorldEvent::Smoke: + updateNoSmoke(); + break; + + default: + break; + } } double RailVehicle::calcTotalWeight(WeightUnit unit) const diff --git a/server/src/vehicle/rail/railvehicle.hpp b/server/src/vehicle/rail/railvehicle.hpp index d296b364..f13cc9b9 100644 --- a/server/src/vehicle/rail/railvehicle.hpp +++ b/server/src/vehicle/rail/railvehicle.hpp @@ -52,9 +52,14 @@ class RailVehicle : public Vehicle SpeedProperty speedMax; WeightProperty weight; WeightProperty totalWeight; + Property mute; + Property noSmoke; ObjectProperty activeTrain; ObjectVectorProperty trains; + + void updateMute(); + void updateNoSmoke(); }; #endif From c1f2690c2c5f787b65903bd3bcee15763d67570b Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Mon, 7 Apr 2025 23:11:19 +0200 Subject: [PATCH 16/37] [luadoc] added missing railvehicle properties --- manual/luadoc/object/railvehicle.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manual/luadoc/object/railvehicle.json b/manual/luadoc/object/railvehicle.json index 0214173e..4c17cdac 100644 --- a/manual/luadoc/object/railvehicle.json +++ b/manual/luadoc/object/railvehicle.json @@ -1,4 +1,6 @@ { "active_train": {}, + "mute": {}, + "no_smoke": {}, "trains": {} } From 3382f6a0760c6d868aaf19afff8513b959370a60 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Tue, 8 Apr 2025 23:43:24 +0200 Subject: [PATCH 17/37] [test] Added assign/remove train to/from muted and no smoke zone test --- server/CMakeLists.txt | 4 +- server/test/main.cpp | 21 -------- server/test/zone.cpp | 115 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 22 deletions(-) delete mode 100644 server/test/main.cpp create mode 100644 server/test/zone.cpp diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 5cf7ba3b..d535503f 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -40,7 +40,9 @@ if(BUILD_TESTING) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") target_compile_options(Catch2 PRIVATE -Wno-restrict) # workaround GCC bug endif() - add_executable(traintastic-server-test test/main.cpp) + add_executable(traintastic-server-test + test/zone.cpp + ) add_dependencies(traintastic-server-test traintastic-lang) target_compile_definitions(traintastic-server-test PRIVATE -DTRAINTASTIC_TEST) set_target_properties(traintastic-server-test PROPERTIES diff --git a/server/test/main.cpp b/server/test/main.cpp deleted file mode 100644 index fc407f23..00000000 --- a/server/test/main.cpp +++ /dev/null @@ -1,21 +0,0 @@ -/** - * server/test/main.cpp - * - * This file is part of the traintastic test suite. - * - * Copyright (C) 2021 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. - */ diff --git a/server/test/zone.cpp b/server/test/zone.cpp new file mode 100644 index 00000000..41595e18 --- /dev/null +++ b/server/test/zone.cpp @@ -0,0 +1,115 @@ +/** + * This file is part of Traintastic, + * see . + * + * Copyright (C) 2025 Reinder Feenstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include "../src/board/board.hpp" +#include "../src/board/boardlist.hpp" +#include "../src/board/tile/rail/blockrailtile.hpp" +#include "../src/world/world.hpp" +#include "../src/vehicle/rail/railvehiclelist.hpp" +#include "../src/vehicle/rail/railvehiclelisttablemodel.hpp" +#include "../src/vehicle/rail/locomotive.hpp" +#include "../src/train/trainlist.hpp" +#include "../src/train/train.hpp" +#include "../src/train/trainvehiclelist.hpp" +#include "../src/hardware/decoder/decoder.hpp" +#include "../src/zone/zonelist.hpp" +#include "../src/zone/zone.hpp" +#include "../src/zone/zoneblocklist.hpp" + +TEST_CASE("Zone: Assign/remove train to/from muted and no smoke zone", "[zone]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + + REQUIRE(world->railVehicles->length == 0); + std::weak_ptr locomotiveWeak = world->railVehicles->create(Locomotive::classId); + REQUIRE_FALSE(locomotiveWeak.expired()); + REQUIRE(world->railVehicles->length == 1); + + REQUIRE(world->trains->length == 0); + std::weak_ptr trainWeak = world->trains->create(); + REQUIRE_FALSE(trainWeak.expired()); + REQUIRE(world->trains->length == 1); + REQUIRE(trainWeak.lock()->vehicles->length == 0); + trainWeak.lock()->vehicles->add(locomotiveWeak.lock()); + REQUIRE(trainWeak.lock()->vehicles->length == 1); + + REQUIRE(world->boards->length == 0); + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + REQUIRE(world->boards->length == 1); + + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg90, BlockRailTile::classId, false)); + std::weak_ptr blockWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(blockWeak.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + REQUIRE_FALSE(zoneWeak.lock()->mute.value()); + REQUIRE_FALSE(zoneWeak.lock()->noSmoke.value()); + zoneWeak.lock()->mute = true; + zoneWeak.lock()->noSmoke = true; + REQUIRE(zoneWeak.lock()->mute); + REQUIRE(zoneWeak.lock()->noSmoke); + REQUIRE(zoneWeak.lock()->blocks->length == 0); + zoneWeak.lock()->blocks->add(blockWeak.lock()); + REQUIRE(zoneWeak.lock()->blocks->length == 1); + REQUIRE(zoneWeak.lock()->trains.size() == 0); + + // Assign train to block in muted and no smoke zone: + REQUIRE_FALSE(trainWeak.lock()->active); + REQUIRE_FALSE(trainWeak.lock()->mute); + REQUIRE_FALSE(trainWeak.lock()->noSmoke); + REQUIRE_FALSE(locomotiveWeak.lock()->mute); + REQUIRE_FALSE(locomotiveWeak.lock()->noSmoke); + blockWeak.lock()->assignTrain(trainWeak.lock()); + REQUIRE(trainWeak.lock()->active); + REQUIRE(trainWeak.lock()->blocks.size() == 1); + REQUIRE(trainWeak.lock()->zones.size() == 1); + REQUIRE(blockWeak.lock()->trains.size() == 1); + REQUIRE(zoneWeak.lock()->trains.size() == 1); + REQUIRE(trainWeak.lock()->mute); + REQUIRE(trainWeak.lock()->noSmoke); + REQUIRE(locomotiveWeak.lock()->mute); + REQUIRE(locomotiveWeak.lock()->noSmoke); + + // Remove train from block in muted and no smoke zone: + blockWeak.lock()->removeTrain(trainWeak.lock()); + REQUIRE_FALSE(trainWeak.lock()->active); + REQUIRE(trainWeak.lock()->blocks.size() == 0); + REQUIRE(trainWeak.lock()->zones.size() == 0); + REQUIRE(blockWeak.lock()->trains.size() == 0); + REQUIRE(zoneWeak.lock()->trains.size() == 0); + REQUIRE_FALSE(trainWeak.lock()->mute); + REQUIRE_FALSE(trainWeak.lock()->noSmoke); + REQUIRE_FALSE(locomotiveWeak.lock()->mute); + REQUIRE_FALSE(locomotiveWeak.lock()->noSmoke); + + world.reset(); + REQUIRE(worldWeak.expired()); + REQUIRE(locomotiveWeak.expired()); + REQUIRE(trainWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(blockWeak.expired()); + REQUIRE(zoneWeak.expired()); +} From 3123e926e2d2bebc8ca83cd04323f0f2124c7b3b Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Tue, 8 Apr 2025 23:44:57 +0200 Subject: [PATCH 18/37] [railvehicle] fix: mute/noSmoke wasn't updated if active train changed --- server/src/train/train.cpp | 4 ++-- server/src/train/trainvehiclelist.cpp | 6 +++--- server/src/vehicle/rail/railvehicle.cpp | 7 +++++++ server/src/vehicle/rail/railvehicle.hpp | 2 ++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/server/src/train/train.cpp b/server/src/train/train.cpp index 2d2bf327..70a18d2c 100644 --- a/server/src/train/train.cpp +++ b/server/src/train/train.cpp @@ -551,7 +551,7 @@ bool Train::setTrainActive(bool val) //Register this train as activeTrain for(const auto& vehicle : *vehicles) { - vehicle->activeTrain.setValueInternal(self); + vehicle->setActiveTrain(self); } //Sync Emergency Stop state @@ -569,7 +569,7 @@ bool Train::setTrainActive(bool val) for(const auto& vehicle : *vehicles) { assert(vehicle->activeTrain.value() == self); - vehicle->activeTrain.setValueInternal(nullptr); + vehicle->setActiveTrain(nullptr); } } diff --git a/server/src/train/trainvehiclelist.cpp b/server/src/train/trainvehiclelist.cpp index 49af221b..7799a0b7 100644 --- a/server/src/train/trainvehiclelist.cpp +++ b/server/src/train/trainvehiclelist.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2023-2024 Reinder Feenstra + * Copyright (C) 2023-2025 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -47,13 +47,13 @@ TrainVehicleList::TrainVehicleList(Train& train_, std::string_view parentPropert vehicle->trains.appendInternal(parent().shared_ptr()); if(train().active) - vehicle->activeTrain.setValueInternal(parent().shared_ptr()); + vehicle->setActiveTrain(parent().shared_ptr()); }} , remove{*this, "remove", [this](const std::shared_ptr& vehicle) { if(vehicle->activeTrain.value() == train().shared_ptr()) - vehicle->activeTrain.setValueInternal(nullptr); + vehicle->setActiveTrain(nullptr); vehicle->trains.removeInternal(parent().shared_ptr()); removeObject(vehicle); diff --git a/server/src/vehicle/rail/railvehicle.cpp b/server/src/vehicle/rail/railvehicle.cpp index 6ee549f4..3f733d73 100644 --- a/server/src/vehicle/rail/railvehicle.cpp +++ b/server/src/vehicle/rail/railvehicle.cpp @@ -82,6 +82,13 @@ RailVehicle::RailVehicle(World& world, std::string_view _id) : m_interfaceItems.insertBefore(trains, notes); } +void RailVehicle::setActiveTrain(const std::shared_ptr& train) +{ + activeTrain.setValueInternal(train); + updateMute(); + updateNoSmoke(); +} + void RailVehicle::updateMute() { bool value = contains(m_world.state, WorldState::NoSmoke); diff --git a/server/src/vehicle/rail/railvehicle.hpp b/server/src/vehicle/rail/railvehicle.hpp index f13cc9b9..cf85f569 100644 --- a/server/src/vehicle/rail/railvehicle.hpp +++ b/server/src/vehicle/rail/railvehicle.hpp @@ -58,6 +58,8 @@ class RailVehicle : public Vehicle ObjectProperty activeTrain; ObjectVectorProperty trains; + void setActiveTrain(const std::shared_ptr& train); + void updateMute(); void updateNoSmoke(); }; From 92a4c9b8532db07cc1b16a7da5304bb44323e411 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Tue, 8 Apr 2025 23:46:49 +0200 Subject: [PATCH 19/37] [traintracking] fix: zone limits wern't updated if train was assigned/removed from a zone block. --- server/src/train/traintracking.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/src/train/traintracking.cpp b/server/src/train/traintracking.cpp index dd4435ae..3644eb27 100644 --- a/server/src/train/traintracking.cpp +++ b/server/src/train/traintracking.cpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2024 Reinder Feenstra + * Copyright (C) 2024-2025 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,6 +27,7 @@ #include "../board/map/blockpath.hpp" #include "../core/objectproperty.tpp" #include "../zone/blockzonelist.hpp" +#include "../world/world.hpp" void TrainTracking::assigned(const std::shared_ptr& train, const std::shared_ptr& block) { @@ -36,7 +37,6 @@ void TrainTracking::assigned(const std::shared_ptr& train, const std::sha train->fireBlockAssigned(block); block->fireTrainAssigned(train); } -#include "../world/world.hpp" void TrainTracking::reserve(const std::shared_ptr& train, const std::shared_ptr& block, BlockTrainDirection direction) { @@ -170,6 +170,8 @@ void TrainTracking::checkZoneAssigned(const std::shared_ptr& train, const zone->trains.appendInternal(zoneStatus); train->zones.appendInternal(zoneStatus); + trainEnteredOrLeftZone(*train, *zone); + train->fireZoneAssigned(zone); zone->fireTrainAssigned(train); } @@ -280,6 +282,8 @@ void TrainTracking::checkZoneRemoved(const std::shared_ptr& train, const { if(removeTrainIfNotInZone(train, zone)) { + trainEnteredOrLeftZone(*train, *zone); + train->fireZoneRemoved(zone); zone->fireTrainRemoved(train); } From 6c7e505f1769dad385fa84a9634901b93c905a63 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Wed, 9 Apr 2025 00:04:48 +0200 Subject: [PATCH 20/37] fix: missing include for Release build --- server/test/zone.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/test/zone.cpp b/server/test/zone.cpp index 41595e18..ae0070a2 100644 --- a/server/test/zone.cpp +++ b/server/test/zone.cpp @@ -20,6 +20,8 @@ */ #include +#include "../src/core/method.tpp" +#include "../src/core/objectproperty.tpp" #include "../src/board/board.hpp" #include "../src/board/boardlist.hpp" #include "../src/board/tile/rail/blockrailtile.hpp" From e4f75b08e796450a23b94b2d9663c285cbb0634a Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Wed, 9 Apr 2025 18:36:00 +0200 Subject: [PATCH 21/37] [test] Added assign/remove zone event test, see #144 --- server/test/zone.cpp | 199 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/server/test/zone.cpp b/server/test/zone.cpp index ae0070a2..d275430c 100644 --- a/server/test/zone.cpp +++ b/server/test/zone.cpp @@ -115,3 +115,202 @@ TEST_CASE("Zone: Assign/remove train to/from muted and no smoke zone", "[zone]") REQUIRE(blockWeak.expired()); REQUIRE(zoneWeak.expired()); } + +TEST_CASE("Zone: Assign/remove events", "[zone]") +{ + size_t trainZoneAssignedEventCount = 0; + size_t trainZoneEnteringEventCount = 0; + size_t trainZoneEnteredEventCount = 0; + size_t trainZoneLeavingEventCount = 0; + size_t trainZoneLeftEventCount = 0; + size_t trainZoneRemovedEventCount = 0; + size_t zoneTrainAssignedEventCount = 0; + size_t zoneTrainEnteringEventCount = 0; + size_t zoneTrainEnteredEventCount = 0; + size_t zoneTrainLeavingEventCount = 0; + size_t zoneTrainLeftEventCount = 0; + size_t zoneTrainRemovedEventCount = 0; + + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + + REQUIRE(world->railVehicles->length == 0); + std::weak_ptr locomotiveWeak = world->railVehicles->create(Locomotive::classId); + REQUIRE_FALSE(locomotiveWeak.expired()); + REQUIRE(world->railVehicles->length == 1); + + REQUIRE(world->trains->length == 0); + std::weak_ptr trainWeak = world->trains->create(); + REQUIRE_FALSE(trainWeak.expired()); + REQUIRE(world->trains->length == 1); + REQUIRE(trainWeak.lock()->vehicles->length == 0); + trainWeak.lock()->vehicles->add(locomotiveWeak.lock()); + REQUIRE(trainWeak.lock()->vehicles->length == 1); + + REQUIRE(world->boards->length == 0); + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + REQUIRE(world->boards->length == 1); + + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg90, BlockRailTile::classId, false)); + std::weak_ptr blockWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(blockWeak.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + REQUIRE(zoneWeak.lock()->blocks->length == 0); + zoneWeak.lock()->blocks->add(blockWeak.lock()); + REQUIRE(zoneWeak.lock()->blocks->length == 1); + REQUIRE(zoneWeak.lock()->trains.size() == 0); + + // Setup events: + trainWeak.lock()->onZoneAssigned.connect( + [&](const std::shared_ptr& train, const std::shared_ptr& zone) + { + trainZoneAssignedEventCount++; + REQUIRE(train == trainWeak.lock()); + REQUIRE(zone == zoneWeak.lock()); + }); + trainWeak.lock()->onZoneEntering.connect( + [&](const std::shared_ptr& train, const std::shared_ptr& zone) + { + trainZoneEnteringEventCount++; + REQUIRE(train == trainWeak.lock()); + REQUIRE(zone == zoneWeak.lock()); + }); + trainWeak.lock()->onZoneEntered.connect( + [&](const std::shared_ptr& train, const std::shared_ptr& zone) + { + trainZoneEnteredEventCount++; + REQUIRE(train == trainWeak.lock()); + REQUIRE(zone == zoneWeak.lock()); + }); + trainWeak.lock()->onZoneLeaving.connect( + [&](const std::shared_ptr& train, const std::shared_ptr& zone) + { + trainZoneLeavingEventCount++; + REQUIRE(train == trainWeak.lock()); + REQUIRE(zone == zoneWeak.lock()); + }); + trainWeak.lock()->onZoneLeft.connect( + [&](const std::shared_ptr& train, const std::shared_ptr& zone) + { + trainZoneLeftEventCount++; + REQUIRE(train == trainWeak.lock()); + REQUIRE(zone == zoneWeak.lock()); + }); + trainWeak.lock()->onZoneRemoved.connect( + [&](const std::shared_ptr& train, const std::shared_ptr& zone) + { + trainZoneRemovedEventCount++; + REQUIRE(train == trainWeak.lock()); + REQUIRE(zone == zoneWeak.lock()); + }); + zoneWeak.lock()->onTrainAssigned.connect( + [&](const std::shared_ptr& train, const std::shared_ptr& zone) + { + zoneTrainAssignedEventCount++; + REQUIRE(train == trainWeak.lock()); + REQUIRE(zone == zoneWeak.lock()); + }); + zoneWeak.lock()->onTrainEntering.connect( + [&](const std::shared_ptr& train, const std::shared_ptr& zone) + { + zoneTrainEnteringEventCount++; + REQUIRE(train == trainWeak.lock()); + REQUIRE(zone == zoneWeak.lock()); + }); + zoneWeak.lock()->onTrainEntered.connect( + [&](const std::shared_ptr& train, const std::shared_ptr& zone) + { + zoneTrainEnteredEventCount++; + REQUIRE(train == trainWeak.lock()); + REQUIRE(zone == zoneWeak.lock()); + }); + zoneWeak.lock()->onTrainLeaving.connect( + [&](const std::shared_ptr& train, const std::shared_ptr& zone) + { + zoneTrainLeavingEventCount++; + REQUIRE(train == trainWeak.lock()); + REQUIRE(zone == zoneWeak.lock()); + }); + zoneWeak.lock()->onTrainLeft.connect( + [&](const std::shared_ptr& train, const std::shared_ptr& zone) + { + zoneTrainLeftEventCount++; + REQUIRE(train == trainWeak.lock()); + REQUIRE(zone == zoneWeak.lock()); + }); + zoneWeak.lock()->onTrainRemoved.connect( + [&](const std::shared_ptr& train, const std::shared_ptr& zone) + { + zoneTrainRemovedEventCount++; + REQUIRE(train == trainWeak.lock()); + REQUIRE(zone == zoneWeak.lock()); + }); + + REQUIRE(trainZoneAssignedEventCount == 0); + REQUIRE(trainZoneEnteringEventCount == 0); + REQUIRE(trainZoneEnteredEventCount == 0); + REQUIRE(trainZoneLeavingEventCount == 0); + REQUIRE(trainZoneLeftEventCount == 0); + REQUIRE(trainZoneRemovedEventCount == 0); + REQUIRE(zoneTrainAssignedEventCount == 0); + REQUIRE(zoneTrainEnteringEventCount == 0); + REQUIRE(zoneTrainEnteredEventCount == 0); + REQUIRE(zoneTrainLeavingEventCount == 0); + REQUIRE(zoneTrainLeftEventCount == 0); + REQUIRE(zoneTrainRemovedEventCount == 0); + + blockWeak.lock()->assignTrain(trainWeak.lock()); + + REQUIRE(trainZoneAssignedEventCount == 1); + REQUIRE(trainZoneEnteringEventCount == 0); + REQUIRE(trainZoneEnteredEventCount == 0); + REQUIRE(trainZoneLeavingEventCount == 0); + REQUIRE(trainZoneLeftEventCount == 0); + REQUIRE(trainZoneRemovedEventCount == 0); + REQUIRE(zoneTrainAssignedEventCount == 1); + REQUIRE(zoneTrainEnteringEventCount == 0); + REQUIRE(zoneTrainEnteredEventCount == 0); + REQUIRE(zoneTrainLeavingEventCount == 0); + REQUIRE(zoneTrainLeftEventCount == 0); + REQUIRE(zoneTrainRemovedEventCount == 0); + + blockWeak.lock()->removeTrain(trainWeak.lock()); + + REQUIRE(trainZoneAssignedEventCount == 1); + REQUIRE(trainZoneEnteringEventCount == 0); + REQUIRE(trainZoneEnteredEventCount == 0); + REQUIRE(trainZoneLeavingEventCount == 0); + REQUIRE(trainZoneLeftEventCount == 0); + REQUIRE(trainZoneRemovedEventCount == 1); + REQUIRE(zoneTrainAssignedEventCount == 1); + REQUIRE(zoneTrainEnteringEventCount == 0); + REQUIRE(zoneTrainEnteredEventCount == 0); + REQUIRE(zoneTrainLeavingEventCount == 0); + REQUIRE(zoneTrainLeftEventCount == 0); + REQUIRE(zoneTrainRemovedEventCount == 1); + + world.reset(); + + REQUIRE(worldWeak.expired()); + REQUIRE(locomotiveWeak.expired()); + REQUIRE(trainWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(blockWeak.expired()); + REQUIRE(zoneWeak.expired()); + REQUIRE(trainZoneAssignedEventCount == 1); + REQUIRE(trainZoneEnteringEventCount == 0); + REQUIRE(trainZoneEnteredEventCount == 0); + REQUIRE(trainZoneLeavingEventCount == 0); + REQUIRE(trainZoneLeftEventCount == 0); + REQUIRE(trainZoneRemovedEventCount == 1); + REQUIRE(zoneTrainAssignedEventCount == 1); + REQUIRE(zoneTrainEnteringEventCount == 0); + REQUIRE(zoneTrainEnteredEventCount == 0); + REQUIRE(zoneTrainLeavingEventCount == 0); + REQUIRE(zoneTrainLeftEventCount == 0); + REQUIRE(zoneTrainRemovedEventCount == 1); +} From 78a76902d0c52b10e8813449d86219cf869c7739 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Wed, 9 Apr 2025 22:48:05 +0200 Subject: [PATCH 22/37] [test] Toggle mute/noSmoke with train in zone test, see #144 --- server/test/zone.cpp | 85 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/server/test/zone.cpp b/server/test/zone.cpp index d275430c..5298a77b 100644 --- a/server/test/zone.cpp +++ b/server/test/zone.cpp @@ -314,3 +314,88 @@ TEST_CASE("Zone: Assign/remove events", "[zone]") REQUIRE(zoneTrainLeftEventCount == 0); REQUIRE(zoneTrainRemovedEventCount == 1); } + +TEST_CASE("Zone: Toggle mute/noSmoke with train in zone", "[zone]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + + REQUIRE(world->railVehicles->length == 0); + std::weak_ptr locomotiveWeak = world->railVehicles->create(Locomotive::classId); + REQUIRE_FALSE(locomotiveWeak.expired()); + REQUIRE(world->railVehicles->length == 1); + + REQUIRE(world->trains->length == 0); + std::weak_ptr trainWeak = world->trains->create(); + REQUIRE_FALSE(trainWeak.expired()); + REQUIRE(world->trains->length == 1); + REQUIRE(trainWeak.lock()->vehicles->length == 0); + trainWeak.lock()->vehicles->add(locomotiveWeak.lock()); + REQUIRE(trainWeak.lock()->vehicles->length == 1); + + REQUIRE(world->boards->length == 0); + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + REQUIRE(world->boards->length == 1); + + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg90, BlockRailTile::classId, false)); + std::weak_ptr blockWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(blockWeak.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + REQUIRE(zoneWeak.lock()->blocks->length == 0); + zoneWeak.lock()->blocks->add(blockWeak.lock()); + REQUIRE(zoneWeak.lock()->blocks->length == 1); + REQUIRE(zoneWeak.lock()->trains.size() == 0); + + REQUIRE_FALSE(trainWeak.lock()->active); + blockWeak.lock()->assignTrain(trainWeak.lock()); + REQUIRE(trainWeak.lock()->active); + REQUIRE(trainWeak.lock()->blocks.size() == 1); + REQUIRE(trainWeak.lock()->zones.size() == 1); + REQUIRE(blockWeak.lock()->trains.size() == 1); + REQUIRE(zoneWeak.lock()->trains.size() == 1); + + REQUIRE_FALSE(trainWeak.lock()->mute); + REQUIRE_FALSE(trainWeak.lock()->noSmoke); + REQUIRE_FALSE(locomotiveWeak.lock()->mute); + REQUIRE_FALSE(locomotiveWeak.lock()->noSmoke); + + zoneWeak.lock()->mute = true; + REQUIRE(trainWeak.lock()->mute); + REQUIRE(locomotiveWeak.lock()->mute); + + zoneWeak.lock()->mute = false; + REQUIRE_FALSE(trainWeak.lock()->mute); + REQUIRE_FALSE(locomotiveWeak.lock()->mute); + + zoneWeak.lock()->noSmoke = true; + REQUIRE(trainWeak.lock()->noSmoke); + REQUIRE(locomotiveWeak.lock()->noSmoke); + + zoneWeak.lock()->noSmoke = false; + REQUIRE_FALSE(trainWeak.lock()->noSmoke); + REQUIRE_FALSE(locomotiveWeak.lock()->noSmoke); + + zoneWeak.lock()->mute = true; + zoneWeak.lock()->noSmoke = true; + + blockWeak.lock()->removeTrain(trainWeak.lock()); + + REQUIRE_FALSE(trainWeak.lock()->mute); + REQUIRE_FALSE(locomotiveWeak.lock()->mute); + REQUIRE_FALSE(trainWeak.lock()->noSmoke); + REQUIRE_FALSE(locomotiveWeak.lock()->noSmoke); + + world.reset(); + + REQUIRE(worldWeak.expired()); + REQUIRE(locomotiveWeak.expired()); + REQUIRE(trainWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(blockWeak.expired()); + REQUIRE(zoneWeak.expired()); +} + From 5af2cb2a15eab292ca022c0ff81f6f0efa5ad525 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Wed, 9 Apr 2025 23:19:18 +0200 Subject: [PATCH 23/37] [zone] fix: duplicate class id, see #144 --- server/src/zone/blockzonelist.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/zone/blockzonelist.hpp b/server/src/zone/blockzonelist.hpp index 1106a01e..895d8254 100644 --- a/server/src/zone/blockzonelist.hpp +++ b/server/src/zone/blockzonelist.hpp @@ -32,7 +32,7 @@ class Zone; class BlockZoneList : public ObjectList { - CLASS_ID("list.zone_block") + CLASS_ID("list.block_zone") private: inline BlockRailTile& block(); From 7514c06649b2d9a2d191db4cd144592485cc8612 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Wed, 9 Apr 2025 23:20:30 +0200 Subject: [PATCH 24/37] [test] Added test to check zone related class id's, see #144 --- server/test/zone.cpp | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/server/test/zone.cpp b/server/test/zone.cpp index 5298a77b..1d8826ab 100644 --- a/server/test/zone.cpp +++ b/server/test/zone.cpp @@ -33,7 +33,9 @@ #include "../src/train/train.hpp" #include "../src/train/trainvehiclelist.hpp" #include "../src/hardware/decoder/decoder.hpp" +#include "../src/zone/blockzonelist.hpp" #include "../src/zone/zonelist.hpp" +#include "../src/zone/zonelisttablemodel.hpp" #include "../src/zone/zone.hpp" #include "../src/zone/zoneblocklist.hpp" @@ -399,3 +401,38 @@ TEST_CASE("Zone: Toggle mute/noSmoke with train in zone", "[zone]") REQUIRE(zoneWeak.expired()); } +TEST_CASE("Zone: Check class id's", "[zone]") +{ + // class id's may NOT be changed, it will break saved worlds and might break client stuff. + + REQUIRE(BlockZoneList::classId == "list.block_zone"); + REQUIRE(Zone::classId == "zone"); + REQUIRE(ZoneBlockList::classId == "list.zone_block"); + REQUIRE(ZoneList::classId == "list.zone"); + REQUIRE(ZoneListTableModel::classId == "zone_list_table_model"); + + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + REQUIRE(world->zones->getClassId() == "list.zone"); + + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg90, BlockRailTile::classId, false)); + std::weak_ptr blockWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(blockWeak.expired()); + REQUIRE(blockWeak.lock()->zones->getClassId() == "list.block_zone"); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + REQUIRE(zoneWeak.lock()->getClassId() == "zone"); + REQUIRE(zoneWeak.lock()->blocks->getClassId() == "list.zone_block"); + + world.reset(); + + REQUIRE(worldWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(blockWeak.expired()); + REQUIRE(zoneWeak.expired()); +} From c163e2288390c99f769bebc0cc49c62e3266c3a2 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Wed, 9 Apr 2025 23:44:34 +0200 Subject: [PATCH 25/37] [test] Added test to check zone enabled attributes, see #144 --- server/test/zone.cpp | 62 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/server/test/zone.cpp b/server/test/zone.cpp index 1d8826ab..ffea669e 100644 --- a/server/test/zone.cpp +++ b/server/test/zone.cpp @@ -20,6 +20,7 @@ */ #include +#include "../src/core/attributes.hpp" #include "../src/core/method.tpp" #include "../src/core/objectproperty.tpp" #include "../src/board/board.hpp" @@ -436,3 +437,64 @@ TEST_CASE("Zone: Check class id's", "[zone]") REQUIRE(blockWeak.expired()); REQUIRE(zoneWeak.expired()); } + +TEST_CASE("Zone: Check enabled attribute", "[zone]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + + REQUIRE(world->state.value() == static_cast(0)); + REQUIRE_FALSE(world->edit); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->id)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->name)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->mute)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->noSmoke)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->speedLimit)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->blocks->add)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->blocks->remove)); + + world->edit = true; + + REQUIRE(world->state.value() == WorldState::Edit); + REQUIRE(world->edit); + REQUIRE(Attributes::getEnabled(zoneWeak.lock()->id)); + REQUIRE(Attributes::getEnabled(zoneWeak.lock()->name)); + REQUIRE(Attributes::getEnabled(zoneWeak.lock()->mute)); + REQUIRE(Attributes::getEnabled(zoneWeak.lock()->noSmoke)); + REQUIRE(Attributes::getEnabled(zoneWeak.lock()->speedLimit)); + REQUIRE(Attributes::getEnabled(zoneWeak.lock()->blocks->add)); + REQUIRE(Attributes::getEnabled(zoneWeak.lock()->blocks->remove)); + + world->run(); + + REQUIRE(world->state.value() == (WorldState::Edit | WorldState::PowerOn | WorldState::Run)); + REQUIRE(world->edit); + REQUIRE(Attributes::getEnabled(zoneWeak.lock()->id)); + REQUIRE(Attributes::getEnabled(zoneWeak.lock()->name)); + REQUIRE(Attributes::getEnabled(zoneWeak.lock()->mute)); + REQUIRE(Attributes::getEnabled(zoneWeak.lock()->noSmoke)); + REQUIRE(Attributes::getEnabled(zoneWeak.lock()->speedLimit)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->blocks->add)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->blocks->remove)); + + world->edit = false; + + REQUIRE(world->state.value() == (WorldState::PowerOn | WorldState::Run)); + REQUIRE_FALSE(world->edit); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->id)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->name)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->mute)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->noSmoke)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->speedLimit)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->blocks->add)); + REQUIRE_FALSE(Attributes::getEnabled(zoneWeak.lock()->blocks->remove)); + + world.reset(); + + REQUIRE(worldWeak.expired()); + REQUIRE(zoneWeak.expired()); +} From 98fb8b38e142c51fb133ff249ae2ae9d9f3c6a63 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Fri, 11 Apr 2025 23:06:19 +0200 Subject: [PATCH 26/37] [test] Added testing zone speed limit property, see #144 --- server/test/zone.cpp | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/server/test/zone.cpp b/server/test/zone.cpp index ffea669e..d9395995 100644 --- a/server/test/zone.cpp +++ b/server/test/zone.cpp @@ -20,6 +20,7 @@ */ #include +#include #include "../src/core/attributes.hpp" #include "../src/core/method.tpp" #include "../src/core/objectproperty.tpp" @@ -40,7 +41,7 @@ #include "../src/zone/zone.hpp" #include "../src/zone/zoneblocklist.hpp" -TEST_CASE("Zone: Assign/remove train to/from muted and no smoke zone", "[zone]") +TEST_CASE("Zone: Assign/remove train to/from muted, no smoke and speed limited zone", "[zone]") { auto world = World::create(); std::weak_ptr worldWeak = world; @@ -74,17 +75,20 @@ TEST_CASE("Zone: Assign/remove train to/from muted and no smoke zone", "[zone]") REQUIRE_FALSE(zoneWeak.lock()->noSmoke.value()); zoneWeak.lock()->mute = true; zoneWeak.lock()->noSmoke = true; + zoneWeak.lock()->speedLimit.setValue(100.0); REQUIRE(zoneWeak.lock()->mute); REQUIRE(zoneWeak.lock()->noSmoke); + REQUIRE(zoneWeak.lock()->speedLimit.value() == Catch::Approx(100.0)); REQUIRE(zoneWeak.lock()->blocks->length == 0); zoneWeak.lock()->blocks->add(blockWeak.lock()); REQUIRE(zoneWeak.lock()->blocks->length == 1); REQUIRE(zoneWeak.lock()->trains.size() == 0); - // Assign train to block in muted and no smoke zone: + // Assign train to block in muted, no smoke and speed limited zone: REQUIRE_FALSE(trainWeak.lock()->active); REQUIRE_FALSE(trainWeak.lock()->mute); REQUIRE_FALSE(trainWeak.lock()->noSmoke); + REQUIRE(std::isinf(trainWeak.lock()->speedLimit.value())); REQUIRE_FALSE(locomotiveWeak.lock()->mute); REQUIRE_FALSE(locomotiveWeak.lock()->noSmoke); blockWeak.lock()->assignTrain(trainWeak.lock()); @@ -95,10 +99,11 @@ TEST_CASE("Zone: Assign/remove train to/from muted and no smoke zone", "[zone]") REQUIRE(zoneWeak.lock()->trains.size() == 1); REQUIRE(trainWeak.lock()->mute); REQUIRE(trainWeak.lock()->noSmoke); + REQUIRE(trainWeak.lock()->speedLimit.value() == Catch::Approx(100.0)); REQUIRE(locomotiveWeak.lock()->mute); REQUIRE(locomotiveWeak.lock()->noSmoke); - // Remove train from block in muted and no smoke zone: + // Remove train from block in muted, no smoke and speed limited zone: blockWeak.lock()->removeTrain(trainWeak.lock()); REQUIRE_FALSE(trainWeak.lock()->active); REQUIRE(trainWeak.lock()->blocks.size() == 0); @@ -107,6 +112,7 @@ TEST_CASE("Zone: Assign/remove train to/from muted and no smoke zone", "[zone]") REQUIRE(zoneWeak.lock()->trains.size() == 0); REQUIRE_FALSE(trainWeak.lock()->mute); REQUIRE_FALSE(trainWeak.lock()->noSmoke); + REQUIRE(std::isinf(trainWeak.lock()->speedLimit.value())); REQUIRE_FALSE(locomotiveWeak.lock()->mute); REQUIRE_FALSE(locomotiveWeak.lock()->noSmoke); @@ -318,7 +324,7 @@ TEST_CASE("Zone: Assign/remove events", "[zone]") REQUIRE(zoneTrainRemovedEventCount == 1); } -TEST_CASE("Zone: Toggle mute/noSmoke with train in zone", "[zone]") +TEST_CASE("Zone: Toggle mute/noSmoke/speedLimit with train in zone", "[zone]") { auto world = World::create(); std::weak_ptr worldWeak = world; @@ -382,8 +388,15 @@ TEST_CASE("Zone: Toggle mute/noSmoke with train in zone", "[zone]") REQUIRE_FALSE(trainWeak.lock()->noSmoke); REQUIRE_FALSE(locomotiveWeak.lock()->noSmoke); + zoneWeak.lock()->speedLimit.setValue(100.0); + REQUIRE(trainWeak.lock()->speedLimit.value() == Catch::Approx(100.0)); + + zoneWeak.lock()->speedLimit.setValue(std::numeric_limits::infinity()); + REQUIRE(std::isinf(trainWeak.lock()->speedLimit.value())); + zoneWeak.lock()->mute = true; zoneWeak.lock()->noSmoke = true; + zoneWeak.lock()->speedLimit.setValue(100.0); blockWeak.lock()->removeTrain(trainWeak.lock()); @@ -391,6 +404,7 @@ TEST_CASE("Zone: Toggle mute/noSmoke with train in zone", "[zone]") REQUIRE_FALSE(locomotiveWeak.lock()->mute); REQUIRE_FALSE(trainWeak.lock()->noSmoke); REQUIRE_FALSE(locomotiveWeak.lock()->noSmoke); + REQUIRE(std::isinf(trainWeak.lock()->speedLimit.value())); world.reset(); From 63ed47cd8505003023752f853131f761eab1cb63 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 13 Apr 2025 17:09:59 +0200 Subject: [PATCH 27/37] [test] Added testing zone list table model, see #144 --- server/test/zone.cpp | 48 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/server/test/zone.cpp b/server/test/zone.cpp index d9395995..2ddae419 100644 --- a/server/test/zone.cpp +++ b/server/test/zone.cpp @@ -512,3 +512,51 @@ TEST_CASE("Zone: Check enabled attribute", "[zone]") REQUIRE(worldWeak.expired()); REQUIRE(zoneWeak.expired()); } + +TEST_CASE("Zone: table model", "[zone]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + + auto zoneListModel = world->zones->getModel(); + REQUIRE(zoneListModel); + REQUIRE(zoneListModel->rowCount() == 0); + + std::weak_ptr zone1 = world->zones->create(); + REQUIRE_FALSE(zone1.expired()); + + REQUIRE(zoneListModel->rowCount() == 1); + REQUIRE(zoneListModel->getText(0, 0) == zone1.lock()->id.value()); + REQUIRE(zoneListModel->getText(1, 0) == zone1.lock()->name.value()); + + zone1.lock()->id = "zone_one"; + REQUIRE(zoneListModel->getText(0, 0) == "zone_one"); + zone1.lock()->name = "Zone One"; + REQUIRE(zoneListModel->getText(1, 0) == "Zone One"); + + std::weak_ptr zone2 = world->zones->create(); + REQUIRE_FALSE(zone2.expired()); + + REQUIRE(zoneListModel->rowCount() == 2); + REQUIRE(zoneListModel->getText(0, 1) == zone2.lock()->id.value()); + REQUIRE(zoneListModel->getText(1, 1) == zone2.lock()->name.value()); + + zone2.lock()->id = "zone_two"; + REQUIRE(zoneListModel->getText(0, 1) == "zone_two"); + zone2.lock()->name = "Zone Two"; + REQUIRE(zoneListModel->getText(1, 1) == "Zone Two"); + + world->zones->delete_(zone1.lock()); + REQUIRE(zone1.expired()); + + REQUIRE(zoneListModel->rowCount() == 1); + REQUIRE(zoneListModel->getText(0, 0) == "zone_two"); + REQUIRE(zoneListModel->getText(1, 0) == "Zone Two"); + REQUIRE(zoneListModel->getText(0, 1) == ""); + REQUIRE(zoneListModel->getText(1, 1) == ""); + + world.reset(); + REQUIRE(worldWeak.expired()); + REQUIRE(zone2.expired()); +} From 710473caf457f3d9c852f18ca125f0412755c797 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 13 Apr 2025 20:34:35 +0200 Subject: [PATCH 28/37] [test] fix: added classID check in zone list table model test, see #144 --- server/test/zone.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/test/zone.cpp b/server/test/zone.cpp index 2ddae419..c28f3d7b 100644 --- a/server/test/zone.cpp +++ b/server/test/zone.cpp @@ -513,7 +513,7 @@ TEST_CASE("Zone: Check enabled attribute", "[zone]") REQUIRE(zoneWeak.expired()); } -TEST_CASE("Zone: table model", "[zone]") +TEST_CASE("Zone: zone list table model", "[zone]") { auto world = World::create(); std::weak_ptr worldWeak = world; @@ -521,6 +521,7 @@ TEST_CASE("Zone: table model", "[zone]") auto zoneListModel = world->zones->getModel(); REQUIRE(zoneListModel); + REQUIRE(zoneListModel->getClassId() == "zone_list_table_model"); REQUIRE(zoneListModel->rowCount() == 0); std::weak_ptr zone1 = world->zones->create(); From 210d52e3d154857f5082118f9193b70870baad74 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 13 Apr 2025 20:35:09 +0200 Subject: [PATCH 29/37] [test] Added zone block list table model test, see #144 --- server/test/zone.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/server/test/zone.cpp b/server/test/zone.cpp index c28f3d7b..f76f7b79 100644 --- a/server/test/zone.cpp +++ b/server/test/zone.cpp @@ -561,3 +561,59 @@ TEST_CASE("Zone: zone list table model", "[zone]") REQUIRE(worldWeak.expired()); REQUIRE(zone2.expired()); } + +TEST_CASE("Zone: zone block list table model", "[zone]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg90, BlockRailTile::classId, false)); + std::weak_ptr block1 = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(block1.expired()); + + REQUIRE(boardWeak.lock()->addTile(1, 1, TileRotate::Deg0, BlockRailTile::classId, false)); + std::weak_ptr block2 = std::dynamic_pointer_cast(boardWeak.lock()->getTile({1, 1})); + REQUIRE_FALSE(block2.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + + auto zoneBlockList = zoneWeak.lock()->blocks->getModel(); + REQUIRE(zoneBlockList->rowCount() == 0); + + zoneWeak.lock()->blocks->add(block1.lock()); + REQUIRE(zoneBlockList->rowCount() == 1); + REQUIRE(zoneBlockList->getText(0, 0) == block1.lock()->id.value()); + REQUIRE(zoneBlockList->getText(1, 0) == block1.lock()->name.value()); + + block1.lock()->id = "block_one"; + block1.lock()->name = "Block One"; + REQUIRE(zoneBlockList->getText(0, 0) == "block_one"); + REQUIRE(zoneBlockList->getText(1, 0) == "Block One"); + + zoneWeak.lock()->blocks->add(block2.lock()); + REQUIRE(zoneBlockList->rowCount() == 2); + REQUIRE(zoneBlockList->getText(0, 1) == block2.lock()->id.value()); + REQUIRE(zoneBlockList->getText(1, 1) == block2.lock()->name.value()); + + block2.lock()->id = "block_two"; + block2.lock()->name = "Block Two"; + REQUIRE(zoneBlockList->getText(0, 1) == "block_two"); + REQUIRE(zoneBlockList->getText(1, 1) == "Block Two"); + + zoneWeak.lock()->blocks->remove(block1.lock()); + REQUIRE(zoneBlockList->rowCount() == 1); + REQUIRE(zoneBlockList->getText(0, 0) == "block_two"); + REQUIRE(zoneBlockList->getText(1, 0) == "Block Two"); + + world.reset(); + REQUIRE(worldWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(block1.expired()); + REQUIRE(block2.expired()); + REQUIRE(zoneWeak.expired()); +} From 09450099f63745d3075d6f82ec42fb63301a2f6c Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 13 Apr 2025 23:06:52 +0200 Subject: [PATCH 30/37] [zone] fix: use while loop, blocks/zones is modified during the loop, see #144 --- server/src/board/tile/rail/blockrailtile.cpp | 4 ++-- server/src/zone/zone.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/board/tile/rail/blockrailtile.cpp b/server/src/board/tile/rail/blockrailtile.cpp index 89fa3df5..6024107c 100644 --- a/server/src/board/tile/rail/blockrailtile.cpp +++ b/server/src/board/tile/rail/blockrailtile.cpp @@ -568,9 +568,9 @@ void BlockRailTile::destroying() { trains.back()->destroy(); } - for(const auto& zone : *zones) + while(!zones->empty()) { - zone->blocks->remove(self); + zones->back()->blocks->remove(self); } m_world.blockRailTiles->removeObject(self); RailTile::destroying(); diff --git a/server/src/zone/zone.cpp b/server/src/zone/zone.cpp index 3b9e6348..fa5b1718 100644 --- a/server/src/zone/zone.cpp +++ b/server/src/zone/zone.cpp @@ -131,9 +131,9 @@ void Zone::addToWorld() void Zone::destroying() { auto self = shared_ptr(); - for(const auto& block : *blocks) + while(!blocks->empty()) { - block->zones->remove(self); + blocks->back()->zones->remove(self); } m_world.zones->removeObject(self); IdObject::destroying(); From 2b57d3fec717a6dd731175c1c20164e7df9775ec Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 13 Apr 2025 23:07:57 +0200 Subject: [PATCH 31/37] [objectlist] added back() method --- server/src/core/objectlist.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/core/objectlist.hpp b/server/src/core/objectlist.hpp index da47b83b..59875247 100644 --- a/server/src/core/objectlist.hpp +++ b/server/src/core/objectlist.hpp @@ -3,7 +3,7 @@ * * This file is part of the traintastic source code. * - * Copyright (C) 2019-2024 Reinder Feenstra + * Copyright (C) 2019-2025 Reinder Feenstra * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -125,6 +125,8 @@ class ObjectList : public AbstractObjectList inline const_iterator end() const noexcept { return m_items.end(); } inline const std::shared_ptr& front() const noexcept { return m_items.front(); } inline std::shared_ptr& front() noexcept { return m_items.front(); } + inline const std::shared_ptr& back() const noexcept { return m_items.back(); } + inline std::shared_ptr& back() noexcept { return m_items.back(); } inline bool empty() const noexcept { return m_items.empty(); } ObjectPtr getObject(uint32_t index) final From 3e80ec734d1d8caf3919e80bf23eb0e2b588ea33 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 13 Apr 2025 23:09:57 +0200 Subject: [PATCH 32/37] [test] added zone/block/broad delete tests, see #144. --- server/test/zone.cpp | 155 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 123 insertions(+), 32 deletions(-) diff --git a/server/test/zone.cpp b/server/test/zone.cpp index f76f7b79..7235b2a0 100644 --- a/server/test/zone.cpp +++ b/server/test/zone.cpp @@ -562,7 +562,7 @@ TEST_CASE("Zone: zone list table model", "[zone]") REQUIRE(zone2.expired()); } -TEST_CASE("Zone: zone block list table model", "[zone]") +TEST_CASE("Zone: block zone list table model", "[zone]") { auto world = World::create(); std::weak_ptr worldWeak = world; @@ -572,48 +572,139 @@ TEST_CASE("Zone: zone block list table model", "[zone]") REQUIRE_FALSE(boardWeak.expired()); REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg90, BlockRailTile::classId, false)); - std::weak_ptr block1 = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); - REQUIRE_FALSE(block1.expired()); + std::weak_ptr blockWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(blockWeak.expired()); - REQUIRE(boardWeak.lock()->addTile(1, 1, TileRotate::Deg0, BlockRailTile::classId, false)); - std::weak_ptr block2 = std::dynamic_pointer_cast(boardWeak.lock()->getTile({1, 1})); - REQUIRE_FALSE(block2.expired()); + std::weak_ptr zone1 = world->zones->create(); + REQUIRE_FALSE(zone1.expired()); - std::weak_ptr zoneWeak = world->zones->create(); - REQUIRE_FALSE(zoneWeak.expired()); + std::weak_ptr zone2 = world->zones->create(); + REQUIRE_FALSE(zone2.expired()); - auto zoneBlockList = zoneWeak.lock()->blocks->getModel(); - REQUIRE(zoneBlockList->rowCount() == 0); + auto blockZoneList = blockWeak.lock()->zones->getModel(); + REQUIRE(blockZoneList->rowCount() == 0); - zoneWeak.lock()->blocks->add(block1.lock()); - REQUIRE(zoneBlockList->rowCount() == 1); - REQUIRE(zoneBlockList->getText(0, 0) == block1.lock()->id.value()); - REQUIRE(zoneBlockList->getText(1, 0) == block1.lock()->name.value()); + blockWeak.lock()->zones->add(zone1.lock()); + REQUIRE(blockZoneList->rowCount() == 1); + REQUIRE(blockZoneList->getText(0, 0) == zone1.lock()->id.value()); + REQUIRE(blockZoneList->getText(1, 0) == zone1.lock()->name.value()); - block1.lock()->id = "block_one"; - block1.lock()->name = "Block One"; - REQUIRE(zoneBlockList->getText(0, 0) == "block_one"); - REQUIRE(zoneBlockList->getText(1, 0) == "Block One"); + zone1.lock()->id = "zone_one"; + zone1.lock()->name = "Zone One"; + REQUIRE(blockZoneList->getText(0, 0) == "zone_one"); + REQUIRE(blockZoneList->getText(1, 0) == "Zone One"); - zoneWeak.lock()->blocks->add(block2.lock()); - REQUIRE(zoneBlockList->rowCount() == 2); - REQUIRE(zoneBlockList->getText(0, 1) == block2.lock()->id.value()); - REQUIRE(zoneBlockList->getText(1, 1) == block2.lock()->name.value()); + zone2.lock()->blocks->add(blockWeak.lock()); + REQUIRE(blockZoneList->rowCount() == 2); + REQUIRE(blockZoneList->getText(0, 1) == zone2.lock()->id.value()); + REQUIRE(blockZoneList->getText(1, 1) == zone2.lock()->name.value()); - block2.lock()->id = "block_two"; - block2.lock()->name = "Block Two"; - REQUIRE(zoneBlockList->getText(0, 1) == "block_two"); - REQUIRE(zoneBlockList->getText(1, 1) == "Block Two"); + zone2.lock()->id = "zone_two"; + zone2.lock()->name = "Zone Two"; + REQUIRE(blockZoneList->getText(0, 1) == "zone_two"); + REQUIRE(blockZoneList->getText(1, 1) == "Zone Two"); - zoneWeak.lock()->blocks->remove(block1.lock()); - REQUIRE(zoneBlockList->rowCount() == 1); - REQUIRE(zoneBlockList->getText(0, 0) == "block_two"); - REQUIRE(zoneBlockList->getText(1, 0) == "Block Two"); + blockWeak.lock()->zones->remove(zone1.lock()); + REQUIRE(blockZoneList->rowCount() == 1); + REQUIRE(blockZoneList->getText(0, 0) == "zone_two"); + REQUIRE(blockZoneList->getText(1, 0) == "Zone Two"); + + world.reset(); + REQUIRE(worldWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(blockWeak.expired()); + REQUIRE(zone1.expired()); + REQUIRE(zone2.expired()); +} + +TEST_CASE("!Zone: delete zone with block assigned", "[zone]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg90, BlockRailTile::classId, false)); + std::weak_ptr blockWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(blockWeak.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + + zoneWeak.lock()->blocks->add(blockWeak.lock()); + REQUIRE(zoneWeak.lock()->blocks->length.value() == 1); + REQUIRE(blockWeak.lock()->zones->length.value() == 1); + + world->zones->delete_(zoneWeak.lock()); + REQUIRE(zoneWeak.expired()); + REQUIRE(world->zones->length.value() == 0); + REQUIRE(blockWeak.lock()->zones->length.value() == 0); + + world.reset(); + REQUIRE(worldWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(blockWeak.expired()); +} + +TEST_CASE("!Zone: delete block with zone assigned", "[zone]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg90, BlockRailTile::classId, false)); + std::weak_ptr blockWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(blockWeak.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + + zoneWeak.lock()->blocks->add(blockWeak.lock()); + REQUIRE(zoneWeak.lock()->blocks->length.value() == 1); + REQUIRE(blockWeak.lock()->zones->length.value() == 1); + + boardWeak.lock()->deleteTile(0, 0); + REQUIRE(blockWeak.expired()); + REQUIRE(zoneWeak.lock()->blocks->length.value() == 0); + + world.reset(); + REQUIRE(worldWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(zoneWeak.expired()); +} + +TEST_CASE("!Zone: delete board with block with zone assigned", "[zone]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg90, BlockRailTile::classId, false)); + std::weak_ptr blockWeak = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(blockWeak.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + + zoneWeak.lock()->blocks->add(blockWeak.lock()); + REQUIRE(zoneWeak.lock()->blocks->length.value() == 1); + REQUIRE(blockWeak.lock()->zones->length.value() == 1); + + world->boards->delete_(boardWeak.lock()); + REQUIRE(boardWeak.expired()); + REQUIRE(blockWeak.expired()); + REQUIRE(zoneWeak.lock()->blocks->length.value() == 0); world.reset(); REQUIRE(worldWeak.expired()); REQUIRE(boardWeak.expired()); - REQUIRE(block1.expired()); - REQUIRE(block2.expired()); REQUIRE(zoneWeak.expired()); } From 2586e5a6a51b39fbfe1a9549ae032f372eb06cc6 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Sun, 13 Apr 2025 23:29:42 +0200 Subject: [PATCH 33/37] [test] fix: re-added deleted test, corrected test names --- server/test/zone.cpp | 62 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/server/test/zone.cpp b/server/test/zone.cpp index 7235b2a0..cb212815 100644 --- a/server/test/zone.cpp +++ b/server/test/zone.cpp @@ -617,7 +617,63 @@ TEST_CASE("Zone: block zone list table model", "[zone]") REQUIRE(zone2.expired()); } -TEST_CASE("!Zone: delete zone with block assigned", "[zone]") +TEST_CASE("Zone: zone block list table model", "[zone]") +{ + auto world = World::create(); + std::weak_ptr worldWeak = world; + REQUIRE_FALSE(worldWeak.expired()); + + std::weak_ptr boardWeak = world->boards->create(); + REQUIRE_FALSE(boardWeak.expired()); + REQUIRE(boardWeak.lock()->addTile(0, 0, TileRotate::Deg90, BlockRailTile::classId, false)); + + std::weak_ptr block1 = std::dynamic_pointer_cast(boardWeak.lock()->getTile({0, 0})); + REQUIRE_FALSE(block1.expired()); + REQUIRE(boardWeak.lock()->addTile(1, 1, TileRotate::Deg0, BlockRailTile::classId, false)); + + std::weak_ptr block2 = std::dynamic_pointer_cast(boardWeak.lock()->getTile({1, 1})); + REQUIRE_FALSE(block2.expired()); + + std::weak_ptr zoneWeak = world->zones->create(); + REQUIRE_FALSE(zoneWeak.expired()); + + auto zoneBlockList = zoneWeak.lock()->blocks->getModel(); + REQUIRE(zoneBlockList->rowCount() == 0); + + zoneWeak.lock()->blocks->add(block1.lock()); + REQUIRE(zoneBlockList->rowCount() == 1); + REQUIRE(zoneBlockList->getText(0, 0) == block1.lock()->id.value()); + REQUIRE(zoneBlockList->getText(1, 0) == block1.lock()->name.value()); + + block1.lock()->id = "block_one"; + block1.lock()->name = "Block One"; + REQUIRE(zoneBlockList->getText(0, 0) == "block_one"); + REQUIRE(zoneBlockList->getText(1, 0) == "Block One"); + + zoneWeak.lock()->blocks->add(block2.lock()); + REQUIRE(zoneBlockList->rowCount() == 2); + REQUIRE(zoneBlockList->getText(0, 1) == block2.lock()->id.value()); + REQUIRE(zoneBlockList->getText(1, 1) == block2.lock()->name.value()); + + block2.lock()->id = "block_two"; + block2.lock()->name = "Block Two"; + REQUIRE(zoneBlockList->getText(0, 1) == "block_two"); + REQUIRE(zoneBlockList->getText(1, 1) == "Block Two"); + + zoneWeak.lock()->blocks->remove(block1.lock()); + REQUIRE(zoneBlockList->rowCount() == 1); + REQUIRE(zoneBlockList->getText(0, 0) == "block_two"); + REQUIRE(zoneBlockList->getText(1, 0) == "Block Two"); + + world.reset(); + REQUIRE(worldWeak.expired()); + REQUIRE(boardWeak.expired()); + REQUIRE(block1.expired()); + REQUIRE(block2.expired()); + REQUIRE(zoneWeak.expired()); +} + +TEST_CASE("Zone: delete zone with block assigned", "[zone]") { auto world = World::create(); std::weak_ptr worldWeak = world; @@ -648,7 +704,7 @@ TEST_CASE("!Zone: delete zone with block assigned", "[zone]") REQUIRE(blockWeak.expired()); } -TEST_CASE("!Zone: delete block with zone assigned", "[zone]") +TEST_CASE("Zone: delete block with zone assigned", "[zone]") { auto world = World::create(); std::weak_ptr worldWeak = world; @@ -678,7 +734,7 @@ TEST_CASE("!Zone: delete block with zone assigned", "[zone]") REQUIRE(zoneWeak.expired()); } -TEST_CASE("!Zone: delete board with block with zone assigned", "[zone]") +TEST_CASE("Zone: delete board with block with zone assigned", "[zone]") { auto world = World::create(); std::weak_ptr worldWeak = world; From 469d59f4c26cd4fdd87f7e23465bfb6a8a507ce3 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Tue, 26 Aug 2025 20:06:35 +0200 Subject: [PATCH 34/37] [decoder] now aware of train/vehicle mute/noSmoke --- server/src/hardware/decoder/decoder.cpp | 95 +++++++++++++++++-------- server/src/hardware/decoder/decoder.hpp | 7 +- server/src/vehicle/rail/railvehicle.cpp | 10 ++- 3 files changed, 77 insertions(+), 35 deletions(-) diff --git a/server/src/hardware/decoder/decoder.cpp b/server/src/hardware/decoder/decoder.cpp index ecffcdc6..f8ba7f17 100644 --- a/server/src/hardware/decoder/decoder.cpp +++ b/server/src/hardware/decoder/decoder.cpp @@ -115,9 +115,6 @@ Decoder::Decoder(World& world, std::string_view _id) : { functions.setValueInternal(std::make_shared(*this, functions.name())); - m_worldMute = contains(m_world.state.value(), WorldState::Mute); - m_worldNoSmoke = contains(m_world.state.value(), WorldState::NoSmoke); - Attributes::addDisplayName(name, DisplayName::Object::name); Attributes::addEnabled(name, false); m_interfaceItems.add(name); @@ -169,6 +166,8 @@ Decoder::Decoder(World& world, std::string_view _id) : m_interfaceItems.add(notes); updateEditable(); + updateMute(); + updateNoSmoke(); } void Decoder::addToWorld() @@ -258,15 +257,15 @@ bool Decoder::getFunctionValue(const std::shared_ptr& fun assert(this == &function->decoder()); - // Apply mute/noSmoke world states: - if(m_worldMute) + // Apply mute/noSmoke: + if(m_mute) { if(function->function == DecoderFunctionFunction::Mute) return true; if(function->function == DecoderFunctionFunction::Sound && !getFunction(DecoderFunctionFunction::Mute)) return false; } - if(m_worldNoSmoke) + if(m_noSmoke) { if(function->function == DecoderFunctionFunction::Smoke) return false; @@ -307,6 +306,54 @@ void Decoder::release(Throttle& driver) assert(false); } +void Decoder::updateMute() +{ + bool value = contains(m_world.state, WorldState::Mute); + if(!value && vehicle) + { + value |= vehicle->mute; + } + if(value != m_mute) + { + m_mute = value; + + if(auto muteFn = getFunction(DecoderFunctionFunction::Mute)) + { + if(muteFn->value != m_mute) + { + changed(DecoderChangeFlags::FunctionValue, muteFn->number); + } + } + else if(auto soundFn = getFunction(DecoderFunctionFunction::Sound)) + { + if(soundFn->value == m_mute) + { + changed(DecoderChangeFlags::FunctionValue, soundFn->number); + } + } + } +} + +void Decoder::updateNoSmoke() +{ + bool value = contains(m_world.state, WorldState::NoSmoke); + if(!value && vehicle) + { + value |= vehicle->noSmoke; + } + if(value != m_noSmoke) + { + m_noSmoke = value; + if(auto smokeFn = getFunction(DecoderFunctionFunction::Smoke)) + { + if(smokeFn->value == m_noSmoke) + { + changed(DecoderChangeFlags::FunctionValue, smokeFn->number); + } + } + } +} + void Decoder::destroying() { if(m_driver) // release driver throttle @@ -325,34 +372,20 @@ void Decoder::worldEvent(WorldState state, WorldEvent event) IdObject::worldEvent(state, event); updateEditable(contains(state, WorldState::Edit)); - // Handle mute/noSmoke world states: - m_worldMute = contains(state, WorldState::Mute); - m_worldNoSmoke = contains(state, WorldState::NoSmoke); - - if(event == WorldEvent::Mute || event == WorldEvent::Unmute) + switch(event) { - bool hasMute = false; + case WorldEvent::Mute: + case WorldEvent::Unmute: + updateMute(); + break; - for(const auto& f : *functions) - if(f->function == DecoderFunctionFunction::Mute) - { - if(!f->value) - changed(DecoderChangeFlags::FunctionValue, f->number); - hasMute = true; - } + case WorldEvent::NoSmoke: + case WorldEvent::Smoke: + updateNoSmoke(); + break; - if(!hasMute) - { - for(const auto& f : *functions) - if(f->function == DecoderFunctionFunction::Sound && f->value) - changed(DecoderChangeFlags::FunctionValue, f->number); - } - } - else if(event == WorldEvent::NoSmoke || event == WorldEvent::Smoke) - { - for(const auto& f : *functions) - if(f->function == DecoderFunctionFunction::Smoke && f->value) - changed(DecoderChangeFlags::FunctionValue, f->number); + default: + break; } } diff --git a/server/src/hardware/decoder/decoder.hpp b/server/src/hardware/decoder/decoder.hpp index 5da7e0e3..09fdcb40 100644 --- a/server/src/hardware/decoder/decoder.hpp +++ b/server/src/hardware/decoder/decoder.hpp @@ -45,8 +45,8 @@ class Decoder : public IdObject friend class DecoderFunction; private: - bool m_worldMute; - bool m_worldNoSmoke; + bool m_mute = false; + bool m_noSmoke = false; std::shared_ptr m_driver; protected: @@ -131,6 +131,9 @@ class Decoder : public IdObject bool acquire(Throttle& driver, bool steal = false); void release(Throttle& driver); + + void updateMute(); + void updateNoSmoke(); }; #endif diff --git a/server/src/vehicle/rail/railvehicle.cpp b/server/src/vehicle/rail/railvehicle.cpp index 5b8fe81c..fcaf1aa3 100644 --- a/server/src/vehicle/rail/railvehicle.cpp +++ b/server/src/vehicle/rail/railvehicle.cpp @@ -115,7 +115,10 @@ void RailVehicle::updateMute() if(value != mute) { mute.setValueInternal(value); - //! \todo update decoder + if(decoder) + { + decoder->updateMute(); + } } } @@ -129,7 +132,10 @@ void RailVehicle::updateNoSmoke() if(value != noSmoke) { noSmoke.setValueInternal(value); - //! \todo update decoder + if(decoder) + { + decoder->updateNoSmoke(); + } } } From eb325fb1a2f7ec96d7852fc75369689a36642517 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Tue, 26 Aug 2025 23:45:06 +0200 Subject: [PATCH 35/37] [luadoc] Added missing terms for zone related documantation, see #144. --- manual/luadoc/terms/en-us.json | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/manual/luadoc/terms/en-us.json b/manual/luadoc/terms/en-us.json index 23bbc1cb..1ededd34 100644 --- a/manual/luadoc/terms/en-us.json +++ b/manual/luadoc/terms/en-us.json @@ -2266,5 +2266,41 @@ { "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." + }, + { + "term": "enum.zone_train_state:description", + "definition": "Enum value represeting the state of a {ref:object.train|train} related to a {ref:object.zone|zone}. For long trains and/or short zones it is possible that a train is `LEAVING` before it fully `ENTERED` the zone, in that case `LEAVING` takes preference over `ENTERING`." + }, + { + "term": "enum.zone_train_state.entered:description", + "definition": "The {ref:object.train|train} is in the {ref:object.zone|zone}, it does not occupy any {ref:object.blockrailtile|block} outside the zone." + }, + { + "term": "enum.zone_train_state.entering:description", + "definition": "The front of the {ref:object.train|train} is in or has reserved at least one {ref:object.blockrailtile|block} of the {ref:object.zone|zone}." + }, + { + "term": "enum.zone_train_state.leaving:description", + "definition": "The front of the {ref:object.train|train} is in or has reserved at least one {ref:object.blockrailtile|block} that is not of the {ref:object.zone|zone}." + }, + { + "term": "enum.zone_train_state.unknown:description", + "definition": "The state is unknown, this value isn't used, it can be used for initialization purposes." + }, + { + "term": "object.trainzonestatus:description", + "definition": "Object represeting the state of a train related to a zone." + }, + { + "term": "object.train.mute:description", + "definition": "`true` if the train is currently muted by the {ref:object.world|world} mute or a muted {ref:object.zone|zone}, `false` otherwise." + }, + { + "term": "object.train.no_smoke:description", + "definition": "`true` if the train's smoke generators are disabled by the {ref:object.world|world} no smoke or a non smoking {ref:object.zone|zone}, `false` otherwise." + }, + { + "term": "object.train.zones:description", + "definition": "List of {ref:object.trainzonestatus|train zone status} objects of all zones that the train is entering, has entered and/or is leaving." } ] From 8ca65046b145570d15c879bff38cd3f0364d5b62 Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Wed, 27 Aug 2025 18:52:52 +0200 Subject: [PATCH 36/37] [manual] Added basic documentation for zones, see #144. --- manual/traintasticmanual/en-us/zone.md | 40 +++++++++++++++++++ .../traintasticmanual/traintasticmanual.json | 4 ++ 2 files changed, 44 insertions(+) create mode 100644 manual/traintasticmanual/en-us/zone.md diff --git a/manual/traintasticmanual/en-us/zone.md b/manual/traintasticmanual/en-us/zone.md new file mode 100644 index 00000000..d2e3d180 --- /dev/null +++ b/manual/traintasticmanual/en-us/zone.md @@ -0,0 +1,40 @@ +# Zones {#zones} + +Zones allow you to apply common rules or restrictions to a group of blocks. +For example, you can create a **Staging zone** that mutes trains, or a **Shunting zone** with a speed limit. + +- A block can belong to multiple zones. +- A zone can include any blocks, even if they are not physically adjacent. + +## Zone list {#zone-list} + +Open the zone list from the main menu: *Objects* → *Zones*. +From here you can manage all zones in your world: + +- **Add a new zone** — Click the **+** button to create a new zone. +- **Remove a zone** — Select a zone and click the **–** button to delete it. +- **Edit a zone** — Double-click a zone in the list or use the **pencil** button to open its properties. + +## Zone properties {#zone-properties} + +Each zone has a set of properties that define its behavior. + +### Identification +- **Name** — A short description of the zone, e.g. *Shunting Area* or *Staging Area*. +- **ID** — Unique identifier of the zone. + - Must be unique among all objects. + - Used for accessing the zone from the [Lua scripting engine](lua.md). + - Must start with a letter (`a–z`). + - May only contain lowercase letters (`a–z`), digits (`0–9`), and underscores (`_`). + +### Block membership +- **Blocks** — List of blocks included in the zone. + Use the **+** and **–** buttons to add or remove blocks. + The **Highlight** button shows all blocks of the zone on opened boards. + +### Behavioral settings +- **Mute** — When enabled, all trains in or entering the zone are muted (default: off). +- **No smoke** — When enabled, all smoke generators of trains in or entering the zone are disabled (default: off). +- **Speed limit** — Defines the maximum speed for trains in the zone. + This does **not** affect manually controlled trains. + By default, there is no speed limit. diff --git a/manual/traintasticmanual/traintasticmanual.json b/manual/traintasticmanual/traintasticmanual.json index c967febc..ddc374c6 100644 --- a/manual/traintasticmanual/traintasticmanual.json +++ b/manual/traintasticmanual/traintasticmanual.json @@ -60,6 +60,10 @@ } ] }, + { + "type": "chapter", + "markdown": "zone.md" + }, { "type": "chapter", "markdown": "clock.md" From e70bc530c01f9ef7c4fe9416c8b7e60fdad2e14f Mon Sep 17 00:00:00 2001 From: Reinder Feenstra Date: Wed, 27 Aug 2025 19:01:22 +0200 Subject: [PATCH 37/37] fix: Missing string, see #144. --- shared/translations/en-us.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shared/translations/en-us.json b/shared/translations/en-us.json index f0aaeb29..23a78e3e 100644 --- a/shared/translations/en-us.json +++ b/shared/translations/en-us.json @@ -6809,5 +6809,9 @@ { "term": "throttle.controlled_by_x", "definition": "Controlled by %1" + }, + { + "term": "board_tile.rail.block:zones", + "definition": "Zones" } ] \ No newline at end of file