• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

traintastic / traintastic / 20435179075

22 Dec 2025 02:24PM UTC coverage: 27.527% (-0.02%) from 27.55%
20435179075

push

github

reinder
[loconet] improved handling of power commands

0 of 22 new or added lines in 2 files covered. (0.0%)

147 existing lines in 4 files now uncovered.

7843 of 28492 relevant lines covered (27.53%)

192.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.46
/server/src/train/train.cpp
1
/**
2
 * server/src/train/train.cpp
3
 *
4
 * This file is part of the traintastic source code.
5
 *
6
 * Copyright (C) 2019-2025 Reinder Feenstra
7
 *
8
 * This program is free software; you can redistribute it and/or
9
 * modify it under the terms of the GNU General Public License
10
 * as published by the Free Software Foundation; either version 2
11
 * of the License, or (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with this program; if not, write to the Free Software
20
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
21
 */
22

23
#include "train.hpp"
24
#include "trainlist.hpp"
25
#include "trainvehiclelist.hpp"
26
#include "../world/world.hpp"
27
#include "trainblockstatus.hpp"
28
#include "trainlisttablemodel.hpp"
29
#include "../core/attributes.hpp"
30
#include "../core/errorcode.hpp"
31
#include "../core/method.tpp"
32
#include "../core/objectproperty.tpp"
33
#include "../core/objectvectorproperty.tpp"
34
#include "../core/eventloop.hpp"
35
#include "../board/tile/rail/blockrailtile.hpp"
36
#include "../vehicle/rail/poweredrailvehicle.hpp"
37
#include "../hardware/decoder/decoder.hpp"
38
#include "../log/log.hpp"
39
#include "../throttle/throttle.hpp"
40
#include "../utils/almostzero.hpp"
41
#include "../utils/displayname.hpp"
42
#include "../zone/zone.hpp"
43

44
CREATE_IMPL(Train)
22✔
45

46
static inline bool isPowered(const RailVehicle& vehicle)
×
47
{
48
  return dynamic_cast<const PoweredRailVehicle*>(&vehicle);
×
49
}
50

51
Train::Train(World& world, std::string_view _id) :
22✔
52
  IdObject(world, _id),
53
  m_speedTimer{EventLoop::ioContext},
22✔
54
  name{this, "name", id, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly},
22✔
55
  length{*this, "length", 0, LengthUnit::MilliMeter, PropertyFlags::ReadWrite | PropertyFlags::Store},
22✔
56
  overrideLength{this, "override_length", false, PropertyFlags::ReadWrite | PropertyFlags::Store,
44✔
57
    [this](bool value)
22✔
58
    {
59
      Attributes::setEnabled(length, value);
×
60
      if(!value)
×
61
        updateLength();
×
62
    }},
×
63
  direction{this, "direction", Direction::Forward, PropertyFlags::ReadWrite | PropertyFlags::StoreState,
44✔
64
    [this](Direction value)
22✔
65
    {
66
      // update train direction from the block perspective:
67
      for(const auto& status : *blocks)
×
68
        status->direction.setValueInternal(!status->direction.value());
×
69
      blocks.reverseInternal(); // index 0 is head of train
×
70

71
      for(const auto& vehicle : m_poweredVehicles)
×
72
        vehicle->setDirection(value);
×
73
      updateEnabled();
×
74
    },
×
75
    [](Direction& value)
22✔
76
    {
77
      // only accept valid direction values, don't accept Unknown direction
78
      return value == Direction::Forward || value == Direction::Reverse;
×
79
    }},
80
  isStopped{this, "is_stopped", true, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
22✔
81
  speed{*this, "speed", 0, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadOnly | PropertyFlags::NoStore},
22✔
82
  speedMax{*this, "speed_max", 0, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
22✔
83
  speedLimit{*this, "speed_limit", SpeedLimitProperty::noLimitValue, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
22✔
84
  throttleSpeed{*this, "throttle_speed", 0, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadWrite | PropertyFlags::StoreState,
44✔
85
    [this](double value, SpeedUnit unit)
22✔
86
    {
87
      const double currentSpeed = speed.getValue(unit);
×
88

89
      emergencyStop.setValueInternal(false);
×
90

91
      if(value > currentSpeed) // Accelerate
×
92
      {
93
        if(m_speedState == SpeedState::Accelerate)
×
94
          return;
×
95

96
        m_speedTimer.cancel();
×
97
        m_speedState = SpeedState::Accelerate;
×
98
        updateSpeed();
×
99
      }
100
      else if(value < currentSpeed) // brake
×
101
      {
102
        if(m_speedState == SpeedState::Braking)
×
103
          return;
×
104

105
        m_speedTimer.cancel();
×
106
        m_speedState = SpeedState::Braking;
×
107
        updateSpeed();
×
108
      }
109
    }},
110
  stop{*this, "stop", MethodFlags::ScriptCallable,
44✔
111
    [this]()
22✔
112
    {
113
      throttleSpeed.setValue(0);
×
114
    }},
×
115
  emergencyStop{this, "emergency_stop", true, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadWrite,
44✔
116
    [this](bool value)
22✔
117
    {
118
      if(value)
×
119
      {
120
        m_speedState = SpeedState::Idle;
×
121
        m_speedTimer.cancel();
×
122
        throttleSpeed.setValueInternal(0);
×
123
        speed.setValueInternal(0);
×
124
        isStopped.setValueInternal(true);
×
125
        updateEnabled();
×
126
      }
127

128
      if(active)
×
129
      {
130
        //Propagate to all vehicles in this Train
131
        for(const auto& vehicle : m_poweredVehicles)
×
132
          vehicle->setEmergencyStop(value);
×
133
      }
134
    }},
×
135
  weight{*this, "weight", 0, WeightUnit::Ton, PropertyFlags::ReadWrite | PropertyFlags::Store},
22✔
136
  overrideWeight{this, "override_weight", false, PropertyFlags::ReadWrite | PropertyFlags::Store,
44✔
137
    [this](bool value)
22✔
138
    {
139
      Attributes::setEnabled(weight, value);
×
140
      if(!value)
×
141
        updateWeight();
×
142
    }},
×
143
  vehicles{this, "vehicles", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject},
22✔
144
  powered{this, "powered", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
22✔
145
  active{this, "active", false, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly,
44✔
146
    [this](bool value)
22✔
147
    {
148
      updateSpeed();
24✔
149
      if(!value && m_throttle)
24✔
150
      {
151
        m_throttle->release();
×
152
      }
153
    },
24✔
154
    std::bind(&Train::setTrainActive, this, std::placeholders::_1)}
44✔
155
  , mode{this, "mode", TrainMode::ManualUnprotected, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}
22✔
156
  , mute{this, "mute", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
22✔
157
  , noSmoke{this, "no_smoke", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
22✔
158
  , hasThrottle{this, "has_throttle", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
22✔
159
  , throttleName{this, "throttle_name", "", PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
44✔
160
  , blocks{*this, "blocks", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}
22✔
161
  , zones{*this, "zones", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}
22✔
162
  , notes{this, "notes", "", PropertyFlags::ReadWrite | PropertyFlags::Store}
44✔
163
  , onBlockAssigned{*this, "on_block_assigned", EventFlags::Scriptable}
22✔
164
  , onBlockReserved{*this, "on_block_reserved", EventFlags::Scriptable}
22✔
165
  , onBlockEntered{*this, "on_block_entered", EventFlags::Scriptable}
22✔
166
  , onBlockLeft{*this, "on_block_left", EventFlags::Scriptable}
22✔
167
  , onBlockRemoved{*this, "on_block_removed", EventFlags::Scriptable}
22✔
168
  , onZoneAssigned{*this, "on_zone_assigned", EventFlags::Scriptable}
22✔
169
  , onZoneEntering{*this, "on_zone_entering", EventFlags::Scriptable}
22✔
170
  , onZoneEntered{*this, "on_zone_entered", EventFlags::Scriptable}
22✔
171
  , onZoneLeaving{*this, "on_zone_leaving", EventFlags::Scriptable}
22✔
172
  , onZoneLeft{*this, "on_zone_left", EventFlags::Scriptable}
22✔
173
  , onZoneRemoved{*this, "on_zone_removed", EventFlags::Scriptable}
66✔
174
{
175
  vehicles.setValueInternal(std::make_shared<TrainVehicleList>(*this, vehicles.name()));
22✔
176

177
  Attributes::addDisplayName(name, DisplayName::Object::name);
22✔
178
  Attributes::addEnabled(name, false);
22✔
179
  m_interfaceItems.add(name);
22✔
180
  Attributes::addEnabled(length, overrideLength);
22✔
181
  m_interfaceItems.add(length);
22✔
182
  m_interfaceItems.add(overrideLength);
22✔
183
  Attributes::addEnabled(direction, false);
22✔
184
  Attributes::addValues(direction, DirectionValues);
22✔
185
  Attributes::addObjectEditor(direction, false);
22✔
186
  m_interfaceItems.add(direction);
22✔
187

188
  Attributes::addObjectEditor(isStopped, false);
22✔
189
  m_interfaceItems.add(isStopped);
22✔
190

191
  Attributes::addObjectEditor(speed, false);
22✔
192
  Attributes::addMinMax(speed, 0., 0., SpeedUnit::KiloMeterPerHour);
22✔
193
  m_interfaceItems.add(speed);
22✔
194
  m_interfaceItems.add(speedMax);
22✔
195

196
  Attributes::addObjectEditor(speedLimit, false);
22✔
197
  m_interfaceItems.add(speedLimit);
22✔
198

199
  Attributes::addMinMax(throttleSpeed, 0., 0., SpeedUnit::KiloMeterPerHour);
22✔
200
  Attributes::addEnabled(throttleSpeed, false);
22✔
201
  Attributes::addObjectEditor(throttleSpeed, false);
22✔
202
  m_interfaceItems.add(throttleSpeed);
22✔
203

204
  Attributes::addEnabled(stop, false);
22✔
205
  Attributes::addObjectEditor(stop, false);
22✔
206
  m_interfaceItems.add(stop);
22✔
207

208
  Attributes::addEnabled(emergencyStop, false);
22✔
209
  Attributes::addObjectEditor(emergencyStop, false);
22✔
210
  m_interfaceItems.add(emergencyStop);
22✔
211

212
  Attributes::addEnabled(weight, overrideWeight);
22✔
213
  m_interfaceItems.add(weight);
22✔
214
  m_interfaceItems.add(overrideWeight);
22✔
215
  m_interfaceItems.add(vehicles);
22✔
216

217
  Attributes::addEnabled(active, true);
22✔
218
  m_interfaceItems.add(active);
22✔
219

220
  Attributes::addValues(mode, trainModeValues);
22✔
221
  Attributes::addObjectEditor(mode, false);
22✔
222
  m_interfaceItems.add(mode);
22✔
223

224
  Attributes::addObjectEditor(mute, false);
22✔
225
  m_interfaceItems.add(mute);
22✔
226

227
  Attributes::addObjectEditor(noSmoke, false);
22✔
228
  m_interfaceItems.add(noSmoke);
22✔
229

230
  Attributes::addObjectEditor(hasThrottle, false);
22✔
231
  m_interfaceItems.add(hasThrottle);
22✔
232

233
  Attributes::addObjectEditor(throttleName, false);
22✔
234
  m_interfaceItems.add(throttleName);
22✔
235

236
  Attributes::addObjectEditor(blocks, false);
22✔
237
  m_interfaceItems.add(blocks);
22✔
238

239
  Attributes::addObjectEditor(zones, false);
22✔
240
  m_interfaceItems.add(zones);
22✔
241

242
  Attributes::addObjectEditor(powered, false);
22✔
243
  m_interfaceItems.add(powered);
22✔
244
  Attributes::addDisplayName(notes, DisplayName::Object::notes);
22✔
245
  m_interfaceItems.add(notes);
22✔
246

247
  m_interfaceItems.add(onBlockAssigned);
22✔
248
  m_interfaceItems.add(onBlockReserved);
22✔
249
  m_interfaceItems.add(onBlockEntered);
22✔
250
  m_interfaceItems.add(onBlockLeft);
22✔
251
  m_interfaceItems.add(onBlockRemoved);
22✔
252
  m_interfaceItems.add(onZoneAssigned);
22✔
253
  m_interfaceItems.add(onZoneEntering);
22✔
254
  m_interfaceItems.add(onZoneEntered);
22✔
255
  m_interfaceItems.add(onZoneLeaving);
22✔
256
  m_interfaceItems.add(onZoneLeft);
22✔
257
  m_interfaceItems.add(onZoneRemoved);
22✔
258

259
  updateEnabled();
22✔
260
  updateMute();
22✔
261
  updateNoSmoke();
22✔
262
}
22✔
263

264
void Train::updateMute()
29✔
265
{
266
  bool value = contains(m_world.state, WorldState::Mute);
29✔
267
  if(!value)
29✔
268
  {
269
    for(const auto& zoneStatus : zones)
30✔
270
    {
271
      if(zoneStatus->zone->mute)
4✔
272
      {
273
        value = true;
3✔
274
        break;
3✔
275
      }
276
    }
277
  }
278
  if(value != mute)
29✔
279
  {
280
    mute.setValueInternal(value);
6✔
281
    if(active)
6✔
282
    {
283
      for(const auto& vehicle : *vehicles)
8✔
284
      {
285
        vehicle->updateMute();
4✔
286
      }
287
    }
288
  }
289
}
29✔
290

291
void Train::updateNoSmoke()
29✔
292
{
293
  bool value = contains(m_world.state, WorldState::NoSmoke);
29✔
294
  if(!value)
29✔
295
  {
296
    for(const auto& zoneStatus : zones)
30✔
297
    {
298
      if(zoneStatus->zone->noSmoke)
4✔
299
      {
300
        value = true;
3✔
301
        break;
3✔
302
      }
303
    }
304
  }
305
  if(value != noSmoke)
29✔
306
  {
307
    noSmoke.setValueInternal(value);
6✔
308
    if(active)
6✔
309
    {
310
      for(const auto& vehicle : *vehicles)
8✔
311
      {
312
        vehicle->updateNoSmoke();
4✔
313
      }
314
    }
315
  }
316
}
29✔
317

318
void Train::updateSpeedLimit()
6✔
319
{
320
  double value = SpeedLimitProperty::noLimitValue;
6✔
321
  SpeedUnit unit = speedLimit.unit();
6✔
322
  for(const auto& zoneStatus : zones)
10✔
323
  {
324
    const auto& zoneSpeedLimit = zoneStatus->zone->speedLimit;
4✔
325
    if(zoneSpeedLimit.getValue(unit) < value)
4✔
326
    {
327
      unit = zoneSpeedLimit.unit();
3✔
328
      value = zoneSpeedLimit.getValue(unit);
3✔
329
    }
330
  }
331
  if(value != speedLimit.getValue(unit))
6✔
332
  {
333
    speedLimit.setValueInternal(value, unit);
6✔
334
    //! \todo Apply to train
335
  }
336
}
6✔
337

338
void Train::addToWorld()
22✔
339
{
340
  IdObject::addToWorld();
22✔
341
  m_world.trains->addObject(shared_ptr<Train>());
22✔
342
}
22✔
343

344
void Train::destroying()
22✔
345
{
346
  auto self = shared_ptr<Train>();
22✔
347
  for(const auto& vehicle : *vehicles)
37✔
348
  {
349
    vehicle->trains.removeInternal(self);
15✔
350
  }
351
  m_world.trains->removeObject(self);
22✔
352
  IdObject::destroying();
22✔
353
}
22✔
354

355
void Train::loaded()
1✔
356
{
357
  IdObject::loaded();
1✔
358

359
  Attributes::setEnabled(length, overrideLength);
1✔
360
  Attributes::setEnabled(weight, overrideWeight);
1✔
361

362
  auto self = shared_ptr<Train>();
1✔
363
  for(auto& vehicle : *vehicles)
2✔
364
  {
365
    vehicle->trains.appendInternal(self);
1✔
366
  }
367

368
  vehiclesChanged();
1✔
369
  updateMute();
1✔
370
  updateNoSmoke();
1✔
371

372
  if(active)
1✔
373
  {
UNCOV
374
    for(const auto& vehicle : m_poweredVehicles)
×
375
    {
UNCOV
376
      vehicle->setDirection(direction);
×
377
    }
378
  }
379
}
1✔
380

381
void Train::worldEvent(WorldState state, WorldEvent event)
7✔
382
{
383
  IdObject::worldEvent(state, event);
7✔
384

385
  switch(event)
7✔
386
  {
387
    case WorldEvent::EditEnabled:
×
388
    case WorldEvent::EditDisabled:
389
      updateEnabled();
×
UNCOV
390
      break;
×
391

392
    case WorldEvent::Mute:
×
393
    case WorldEvent::Unmute:
UNCOV
394
      updateMute();
×
UNCOV
395
      break;
×
396

UNCOV
397
    case WorldEvent::NoSmoke:
×
398
    case WorldEvent::Smoke:
399
      updateNoSmoke();
×
UNCOV
400
      break;
×
401

402
    default:
7✔
403
      break;
7✔
404
  }
405
}
7✔
406

UNCOV
407
void Train::setSpeed(const double kmph)
×
408
{
UNCOV
409
  for(const auto& vehicle : m_poweredVehicles)
×
UNCOV
410
    vehicle->setSpeed(kmph);
×
UNCOV
411
  speed.setValueInternal(convertUnit(kmph, SpeedUnit::KiloMeterPerHour, speed.unit()));
×
412
  updateEnabled();
×
413
}
×
414

415
void Train::updateSpeed()
24✔
416
{
417
  if(m_speedState == SpeedState::Idle)
24✔
418
    return;
24✔
419

UNCOV
420
  if(m_speedState == SpeedState::Accelerate && !active)
×
UNCOV
421
    return;
×
422

UNCOV
423
  const double targetSpeed = throttleSpeed.getValue(SpeedUnit::MeterPerSecond);
×
424
  double currentSpeed = speed.getValue(SpeedUnit::MeterPerSecond);
×
425

UNCOV
426
  double acceleration = 0;
×
427
  if(m_speedState == SpeedState::Accelerate)
×
428
  {
429
    //! \todo add realistic acceleration
430
    acceleration = 1; // m/s^2
×
431
  }
432
  else if(m_speedState == SpeedState::Braking)
×
433
  {
434
    //! \todo add realistic braking
435
    acceleration = -0.5; // m/s^2
×
436
  }
437
  else
438
    assert(false);
×
439

UNCOV
440
  currentSpeed += acceleration * 0.1; // x 100ms
×
441

UNCOV
442
  if((m_speedState == SpeedState::Accelerate && currentSpeed >= targetSpeed) ||
×
UNCOV
443
      (m_speedState == SpeedState::Braking && currentSpeed <= targetSpeed))
×
444
  {
445
    m_speedState = SpeedState::Idle;
×
446
    setSpeed(convertUnit(targetSpeed, SpeedUnit::MeterPerSecond, SpeedUnit::KiloMeterPerHour));
×
447
    currentSpeed = targetSpeed;
×
448
  }
449
  else
450
  {
451
    using namespace std::literals;
UNCOV
452
    setSpeed(convertUnit(currentSpeed, SpeedUnit::MeterPerSecond, SpeedUnit::KiloMeterPerHour));
×
UNCOV
453
    m_speedTimer.expires_after(100ms);
×
454
    m_speedTimer.async_wait(
×
455
      [this](const boost::system::error_code& ec)
×
456
      {
457
        if(!ec)
×
UNCOV
458
          updateSpeed();
×
UNCOV
459
      });
×
460
  }
461

UNCOV
462
  const bool currentValue = isStopped;
×
UNCOV
463
  isStopped.setValueInternal(m_speedState == SpeedState::Idle && almostZero(currentSpeed) && almostZero(targetSpeed));
×
UNCOV
464
  if(currentValue != isStopped)
×
UNCOV
465
    updateEnabled();
×
466
}
467

468
void Train::vehiclesChanged()
17✔
469
{
470
  updateLength();
17✔
471
  updateWeight();
17✔
472
  updatePowered();
17✔
473
  updateSpeedMax();
17✔
474
  updateEnabled();
17✔
475
}
17✔
476

477
void Train::updateLength()
17✔
478
{
479
  if(overrideLength)
17✔
UNCOV
480
    return;
×
481

482
  double mm = 0;
17✔
483
  for(const auto& vehicle : *vehicles)
33✔
484
    mm += vehicle->length.getValue(LengthUnit::MilliMeter);
16✔
485
  length.setValueInternal(convertUnit(mm, LengthUnit::MilliMeter, length.unit()));
17✔
486
}
487

488
void Train::updateWeight()
17✔
489
{
490
  if(overrideWeight)
17✔
UNCOV
491
    return;
×
492

493
  double ton = 0;
17✔
494
  for(const auto& vehicle : *vehicles)
33✔
495
    ton += vehicle->totalWeight.getValue(WeightUnit::Ton);
16✔
496
  weight.setValueInternal(convertUnit(ton, WeightUnit::Ton, weight.unit()));
17✔
497
}
498

499
void Train::updatePowered()
17✔
500
{
501
  m_poweredVehicles.clear();
17✔
502
  for(const auto& vehicle : *vehicles)
33✔
503
    if(auto poweredVehicle = std::dynamic_pointer_cast<PoweredRailVehicle>(vehicle))
16✔
504
      m_poweredVehicles.emplace_back(poweredVehicle);
16✔
505
  powered.setValueInternal(!m_poweredVehicles.empty());
17✔
506
}
17✔
507

508
void Train::updateSpeedMax()
17✔
509
{
510
  if(!vehicles->empty() && powered)
17✔
511
  {
512
    const auto itEnd = vehicles->end();
16✔
513
    auto it = vehicles->begin();
16✔
514
    double kmph = (*it)->speedMax.getValue(SpeedUnit::KiloMeterPerHour);
16✔
515
    for(; it != itEnd; ++it)
32✔
516
    {
517
      const double v = (*it)->speedMax.getValue(SpeedUnit::KiloMeterPerHour);
16✔
518
      if((v > 0 || isPowered(**it)) && v < kmph)
16✔
UNCOV
519
        kmph = v;
×
520
    }
521
    speedMax.setValueInternal(convertUnit(kmph, SpeedUnit::KiloMeterPerHour, speedMax.unit()));
16✔
522
  }
523
  else
524
    speedMax.setValueInternal(0);
1✔
525

526
  speed.setUnit(speedMax.unit());
17✔
527
  Attributes::setMax(speed, speedMax.value(), speedMax.unit());
17✔
528
  throttleSpeed.setUnit(speedMax.unit());
17✔
529
  Attributes::setMax(throttleSpeed, speedMax.value(), speedMax.unit());
17✔
530
}
17✔
531

532
void Train::updateEnabled()
39✔
533
{
534
  const bool stopped = isStopped;
39✔
535
  const bool editable = contains(m_world.state, WorldState::Edit);
39✔
536

537
  Attributes::setEnabled(name, stopped && editable);
39✔
538
  Attributes::setEnabled(direction, stopped && powered);
39✔
539
  Attributes::setEnabled(throttleSpeed, powered);
39✔
540
  Attributes::setEnabled(stop, powered);
39✔
541
  Attributes::setEnabled(emergencyStop, powered);
39✔
542
  Attributes::setEnabled(vehicles->add, stopped);
39✔
543
  Attributes::setEnabled(vehicles->remove, stopped);
39✔
544
  Attributes::setEnabled(vehicles->move, stopped);
39✔
545
  Attributes::setEnabled(vehicles->reverse, stopped);
39✔
546
}
39✔
547

548
bool Train::setTrainActive(bool val)
25✔
549
{
550
  auto self = this->shared_ptr<Train>();
25✔
551

552
  if(val)
25✔
553
  {
554
    if(vehicles->empty())
13✔
555
    {
556
      return false; // activating a train without vehicles is useless.
1✔
557
    }
558

559
    //To activate a train, ensure all vehicles are stopped and free
560
    for(const auto& vehicle : *vehicles)
24✔
561
    {
562
      assert(vehicle->activeTrain.value() != self);
12✔
563
      if(vehicle->activeTrain.value())
12✔
564
      {
UNCOV
565
        return false; //Not free
×
566
      }
567

568
      if(auto decoder = vehicle->decoder.value(); decoder && !almostZero(decoder->throttle.value()))
12✔
569
      {
UNCOV
570
        return false; //Already running
×
571
      }
12✔
572
    }
573

574
    //Now really activate
575
    //Register this train as activeTrain
576
    for(const auto& vehicle : *vehicles)
24✔
577
    {
578
      vehicle->setActiveTrain(self);
12✔
579
    }
580

581
    //Sync Emergency Stop state
582
    const bool stopValue = emergencyStop;
12✔
583
    for(const auto& vehicle : m_poweredVehicles)
24✔
584
      vehicle->setEmergencyStop(stopValue);
12✔
585
  }
586
  else
587
  {
588
    //To deactivate a Train it must be stopped first
589
    if(!isStopped)
12✔
UNCOV
590
      return false;
×
591

592
    //Deactivate all vehicles
593
    for(const auto& vehicle : *vehicles)
24✔
594
    {
595
      assert(vehicle->activeTrain.value() == self);
12✔
596
      vehicle->setActiveTrain(nullptr);
12✔
597
    }
598
  }
599

600
  return true;
24✔
601
}
25✔
602

603
std::error_code Train::acquire(Throttle& throttle, bool steal)
×
604
{
605
  if(m_throttle)
×
606
  {
UNCOV
607
    if(!steal)
×
608
    {
609
      return make_error_code(ErrorCode::AlreadyAcquired);
×
610
    }
611
    m_throttle->release();
×
612
  }
613
  if(!active)
×
614
  {
615
    try
616
    {
UNCOV
617
      active = true; // TODO: activate();
×
618
    }
619
    catch(...)
×
620
    {
621
    }
×
622
    if(!active)
×
623
    {
UNCOV
624
      return make_error_code(ErrorCode::CanNotActivateTrain);
×
625
    }
626
  }
UNCOV
627
  assert(!m_throttle);
×
628
  m_throttle = throttle.shared_ptr<Throttle>();
×
UNCOV
629
  hasThrottle.setValueInternal(true);
×
630
  throttleName.setValueInternal(m_throttle->name);
×
UNCOV
631
  return {};
×
632
}
633

634
std::error_code Train::release(Throttle& throttle)
×
635
{
UNCOV
636
  if(m_throttle.get() != &throttle)
×
637
  {
UNCOV
638
    return make_error_code(ErrorCode::InvalidThrottle);
×
639
  }
UNCOV
640
  m_throttle.reset();
×
UNCOV
641
  hasThrottle.setValueInternal(false);
×
642
  throttleName.setValueInternal("");
×
UNCOV
643
  if(isStopped && blocks.empty())
×
644
  {
UNCOV
645
    active = false; // deactive train if it is stopped and not assigned to a block
×
646
  }
UNCOV
647
  return {};
×
648
}
649

650
std::error_code Train::setSpeed(Throttle& throttle, double value)
×
651
{
652
  if(m_throttle.get() != &throttle)
×
653
  {
654
    return make_error_code(ErrorCode::InvalidThrottle);
×
655
  }
UNCOV
656
  assert(active);
×
657

658
  value = std::clamp(value, Attributes::getMin(speed), Attributes::getMax(speed));
×
659

UNCOV
660
  setSpeed(convertUnit(value, speed.unit(), SpeedUnit::KiloMeterPerHour));
×
661
  throttleSpeed.setValue(convertUnit(value, speed.unit(), throttleSpeed.unit()));
×
UNCOV
662
  m_speedTimer.cancel();
×
663
  m_speedState = SpeedState::Idle;
×
664

UNCOV
665
  const bool currentValue = isStopped;
×
666
  isStopped.setValueInternal(m_speedState == SpeedState::Idle && almostZero(speed.value()) && almostZero(throttleSpeed.value()));
×
UNCOV
667
  if(currentValue != isStopped)
×
668
  {
UNCOV
669
    updateEnabled();
×
670
  }
UNCOV
671
  return {};
×
672
}
673

674
std::error_code Train::setTargetSpeed(Throttle& throttle, double value)
×
675
{
UNCOV
676
  if(m_throttle.get() != &throttle)
×
677
  {
UNCOV
678
    return make_error_code(ErrorCode::InvalidThrottle);
×
679
  }
UNCOV
680
  assert(active);
×
681
  throttleSpeed.setValue(std::clamp(value, Attributes::getMin(throttleSpeed), Attributes::getMax(throttleSpeed)));
×
UNCOV
682
  return {};
×
683
}
684

685
std::error_code Train::setDirection(Throttle& throttle, Direction value)
×
686
{
687
  if(m_throttle.get() != &throttle)
×
688
  {
689
    return make_error_code(ErrorCode::InvalidThrottle);
×
690
  }
UNCOV
691
  if(direction != value)
×
692
  {
UNCOV
693
    if(!isStopped)
×
694
    {
UNCOV
695
      return make_error_code(ErrorCode::TrainMustBeStoppedToChangeDirection);
×
696
    }
UNCOV
697
    assert(active);
×
UNCOV
698
    direction = value;
×
699
  }
UNCOV
700
  return {};
×
701
}
702

703
void Train::fireBlockAssigned(const std::shared_ptr<BlockRailTile>& block)
10✔
704
{
705
  if(m_world.debugTrainEvents)
10✔
706
  {
UNCOV
707
    Log::log(*this, LogMessage::D3010_TRAIN_X_ASSIGNED_TO_BLOCK_X, name.value(), block->name.value());
×
708
  }
709
  fireEvent(
20✔
710
    onBlockAssigned,
10✔
711
    shared_ptr<Train>(),
20✔
712
    block);
713
}
10✔
714

715
void Train::fireBlockReserved(const std::shared_ptr<BlockRailTile>& block, BlockTrainDirection trainDirection)
5✔
716
{
717
  if(m_world.debugTrainEvents)
5✔
718
  {
UNCOV
719
    Log::log(*this, LogMessage::D3011_TRAIN_X_RESERVED_BLOCK_X, name.value(), block->name.value());
×
720
  }
721
  fireEvent(
10✔
722
    onBlockReserved,
5✔
723
    shared_ptr<Train>(),
10✔
724
    block,
725
    trainDirection);
726
}
5✔
727

728
void Train::fireBlockEntered(const std::shared_ptr<BlockRailTile>& block, BlockTrainDirection trainDirection)
×
729
{
UNCOV
730
  if(m_world.debugTrainEvents)
×
731
  {
UNCOV
732
    Log::log(*this, LogMessage::D3012_TRAIN_X_ENTERED_BLOCK_X, name.value(), block->name.value());
×
733
  }
UNCOV
734
  fireEvent(
×
735
    onBlockEntered,
×
UNCOV
736
    shared_ptr<Train>(),
×
737
    block,
738
    trainDirection);
739
}
×
740

741
void Train::fireBlockLeft(const std::shared_ptr<BlockRailTile>& block, BlockTrainDirection trainDirection)
×
742
{
UNCOV
743
  if(m_world.debugTrainEvents)
×
744
  {
UNCOV
745
    Log::log(*this, LogMessage::D3013_TRAIN_X_LEFT_BLOCK_X, name.value(), block->name.value());
×
746
  }
UNCOV
747
  fireEvent(
×
UNCOV
748
    onBlockLeft,
×
UNCOV
749
    shared_ptr<Train>(),
×
750
    block,
751
    trainDirection);
UNCOV
752
}
×
753

754
void Train::fireBlockRemoved(const std::shared_ptr<BlockRailTile>& block)
3✔
755
{
756
  if(m_world.debugTrainEvents)
3✔
757
  {
UNCOV
758
    Log::log(*this, LogMessage::D3014_TRAIN_X_REMOVED_FROM_BLOCK_X, name.value(), block->name.value());
×
759
  }
760
  fireEvent(
6✔
761
    onBlockRemoved,
3✔
762
    shared_ptr<Train>(),
6✔
763
    block);
764
}
3✔
765

766
void Train::fireZoneAssigned(const std::shared_ptr<Zone>& zone)
3✔
767
{
768
  if(m_world.debugTrainEvents)
3✔
769
  {
UNCOV
770
    Log::log(*this, LogMessage::D3020_TRAIN_X_ASSIGNED_TO_ZONE_X, name.value(), zone->name.value());
×
771
  }
772
  fireEvent(onZoneAssigned, shared_ptr<Train>(), zone);
3✔
773
}
3✔
774

UNCOV
775
void Train::fireZoneEntering(const std::shared_ptr<Zone>& zone)
×
776
{
UNCOV
777
  if(m_world.debugTrainEvents)
×
778
  {
UNCOV
779
    Log::log(*this, LogMessage::D3021_TRAIN_X_ENTERING_ZONE_X, name.value(), zone->name.value());
×
780
  }
UNCOV
781
  fireEvent(onZoneEntering, shared_ptr<Train>(), zone);
×
782
}
×
783

UNCOV
784
void Train::fireZoneEntered(const std::shared_ptr<Zone>& zone)
×
785
{
UNCOV
786
  if(m_world.debugTrainEvents)
×
787
  {
UNCOV
788
    Log::log(*this, LogMessage::D3022_TRAIN_X_ENTERED_ZONE_X, name.value(), zone->name.value());
×
789
  }
UNCOV
790
  fireEvent(onZoneEntered, shared_ptr<Train>(), zone);
×
791
}
×
792

UNCOV
793
void Train::fireZoneLeaving(const std::shared_ptr<Zone>& zone)
×
794
{
UNCOV
795
  if(m_world.debugTrainEvents)
×
796
  {
UNCOV
797
    Log::log(*this, LogMessage::D3023_TRAIN_X_LEAVING_ZONE_X, name.value(), zone->name.value());
×
798
  }
UNCOV
799
  fireEvent(onZoneLeaving, shared_ptr<Train>(), zone);
×
800
}
×
801

UNCOV
802
void Train::fireZoneLeft(const std::shared_ptr<Zone>& zone)
×
803
{
UNCOV
804
  if(m_world.debugTrainEvents)
×
805
  {
UNCOV
806
    Log::log(*this, LogMessage::D3024_TRAIN_X_LEFT_ZONE_X, name.value(), zone->name.value());
×
807
  }
UNCOV
808
  fireEvent(onZoneLeft, shared_ptr<Train>(), zone);
×
UNCOV
809
}
×
810

811
void Train::fireZoneRemoved(const std::shared_ptr<Zone>& zone)
3✔
812
{
813
  if(m_world.debugTrainEvents)
3✔
814
  {
UNCOV
815
    Log::log(*this, LogMessage::D3025_TRAIN_X_REMOVED_FROM_ZONE_X, name.value(), zone->name.value());
×
816
  }
817
  fireEvent(onZoneRemoved, shared_ptr<Train>(), zone);
3✔
818
}
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc