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

traintastic / traintastic / 24939412195

25 Apr 2026 07:59PM UTC coverage: 25.624% (-0.04%) from 25.659%
24939412195

push

github

web-flow
Merge pull request #224 from traintastic/diagnostic-report

Add diagnostic report feature

1 of 46 new or added lines in 4 files covered. (2.17%)

2 existing lines in 2 files now uncovered.

8426 of 32883 relevant lines covered (25.62%)

177.44 hits per line

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

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

22
#include "session.hpp"
23
#include <boost/algorithm/string.hpp>
24
#include <boost/uuid/random_generator.hpp>
25
#include "../traintastic/traintastic.hpp"
26
#include "clientconnection.hpp"
27
#include <traintastic/enum/interfaceitemtype.hpp>
28
#include <traintastic/enum/attributetype.hpp>
29
#include "../compat/stdformat.hpp"
30
#ifndef NDEBUG
31
  #include "../core/eventloop.hpp" // for: isEventLoopThread()
32
#endif
33
#include "../core/abstractobjectlist.hpp"
34
#include "../core/abstractunitproperty.hpp"
35
#include "../core/objectproperty.tpp"
36
#include "../core/tablemodel.hpp"
37
#include "../log/log.hpp"
38
#include "../log/logmessageexception.hpp"
39
#include "../log/memorylogger.hpp"
40
#include "../board/board.hpp"
41
#include "../board/tile/tiles.hpp"
42
#include "../hardware/input/monitor/inputmonitor.hpp"
43
#include "../hardware/output/keyboard/outputkeyboard.hpp"
44
#include "../throttle/clientthrottle.hpp"
45

46
#ifdef GetObject
47
  #undef GetObject // GetObject is defined by a winapi header
48
#endif
49

50
namespace {
51

52
std::pair<std::string_view, std::string_view> splitOnLastDot(std::string_view sv)
×
53
{
54
  if(const auto pos = sv.rfind('.'); pos != std::string_view::npos)
×
55
  {
56
    return {sv.substr(0, pos), sv.substr(pos + 1)};
×
57
  }
58
  return {{}, sv};
×
59
}
60

61
}
62

63
Session::Session(const std::shared_ptr<ClientConnection>& connection) :
×
64
  m_connection{connection},
×
65
  m_uuid{boost::uuids::random_generator()()}
×
66
{
67
  assert(isEventLoopThread());
×
68
}
×
69

70
Session::~Session()
×
71
{
72
  assert(isEventLoopThread());
×
73

74
  m_objectSignals.clear(); // disconnect all, we don't want m_handles modified during the loop
×
75
  for(const auto& it : m_handles)
×
76
  {
77
    if(it.second && isSessionObject(it.second))
×
78
    {
79
      it.second->destroy();
×
80
    }
81
  }
82
}
×
83

84
bool Session::processMessage(const Message& message)
×
85
{
86
  switch(message.command())
×
87
  {
88
    case Message::Command::GetObject:
×
89
    {
90
      std::string id;
×
91
      message.read(id);
×
92

93
      std::vector<std::string> ids;
×
94
      boost::split(ids, id, [](char c){ return c == '.'; });
×
95
      auto it = ids.cbegin();
×
96

97
      ObjectPtr obj;
×
98
      if(*it == Traintastic::classId)
×
99
        obj = Traintastic::instance;
×
100
      else if(Traintastic::instance->world)
×
101
        obj = Traintastic::instance->world->getObjectById(*it);
×
102

103
      while(obj && ++it != ids.cend())
×
104
      {
105
        if(AbstractProperty* property = obj->getProperty(*it); property && property->type() == ValueType::Object)
×
106
          obj = property->toObject();
×
107
        else if(AbstractVectorProperty* vectorProperty = obj->getVectorProperty(*it); vectorProperty && vectorProperty->type() == ValueType::Object)
×
108
        {
109
          obj = nullptr;
×
110
          const size_t size = vectorProperty->size();
×
111
          for(size_t i = 0; i < size; i++)
×
112
          {
113
            ObjectPtr v = vectorProperty->getObject(i);
×
114
            if(id == v->getObjectId())
×
115
            {
116
              obj = v;
×
117
              it++;
×
118
              break;
×
119
            }
120
          }
×
121
        }
122
        else
123
          obj = nullptr;
×
124
      }
125

126
      if(obj)
×
127
      {
128
        auto response = Message::newResponse(message.command(), message.requestId());
×
129
        writeObject(*response, obj);
×
130
        m_connection->sendMessage(std::move(response));
×
131
      }
×
132
      else
133
      {
134
        m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1015_UNKNOWN_OBJECT));
×
135
      }
136
      return true;
×
137
    }
×
138
    case Message::Command::ReleaseObject:
×
139
    {
140
      // client counter value must match server counter value,
141
      // to make sure no handles are "on the wire"
142
      //
143
      const auto handle = message.read<Handle>();
×
144
      const auto counter = message.read<uint32_t>();
×
145
      if(counter == m_handles.getCounter(handle))
×
146
      {
147
        if(auto object = m_handles.getItem(handle); object && isSessionObject(object))
×
148
        {
149
          object->destroy();
×
150
        }
×
151

152
        m_handles.removeHandle(handle);
×
153

154
        auto it = m_objectSignals.find(handle);
×
155
        while(it != m_objectSignals.end())
×
156
        {
157
          it->second.disconnect();
×
158
          m_objectSignals.erase(it);
×
159
          it = m_objectSignals.find(handle);
×
160
        }
161

162
        auto event = Message::newEvent(message.command(), sizeof(Handle));
×
163
        event->write(handle);
×
164
        m_connection->sendMessage(std::move(event));
×
165
      }
×
166
      break;
×
167
    }
168
    case Message::Command::ObjectSetProperty:
×
169
    {
170
      if(message.isRequest() || message.isEvent())
×
171
      {
172
        if(ObjectPtr object = m_handles.getItem(message.read<Handle>()))
×
173
        {
174
          if(AbstractProperty* property = object->getProperty(message.read<std::string>()); property && !property->isInternal())
×
175
          {
176
            try
177
            {
178
              switch(message.read<ValueType>())
×
179
              {
180
                case ValueType::Boolean:
×
181
                  property->fromBool(message.read<bool>());
×
182
                  break;
×
183

184
                case ValueType::Integer:
×
185
                  property->fromInt64(message.read<int64_t>());
×
186
                  break;
×
187

188
                case ValueType::Float:
×
189
                  property->fromDouble(message.read<double>());
×
190
                  break;
×
191

192
                case ValueType::String:
×
193
                  property->fromString(message.read<std::string>());
×
194
                  break;
×
195

196
                default:
×
197
                  throw std::runtime_error("invalid value type");
×
198
              }
199
            }
200
            catch(const std::exception& e) // set property failed
×
201
            {
202
              if(message.isRequest()) // send error response
×
203
              {
204
                m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1018_EXCEPTION_X, e.what()));
×
205
              }
206
              else // send changed event with current value:
207
                objectPropertyChanged(*property);
×
208
            }
×
209

210
            if(message.isRequest()) // send success response
×
211
              m_connection->sendMessage(Message::newResponse(message.command(), message.requestId()));
×
212
          }
213
          else if(message.isRequest()) // send error response
×
214
          {
215
            m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1016_UNKNOWN_PROPERTY));
×
216
          }
217
        }
218
        else if(message.isRequest()) // send error response
×
219
        {
220
          m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1015_UNKNOWN_OBJECT));
×
221
        }
×
222
      }
223
      return true;
×
224
    }
225
    case Message::Command::ObjectSetVectorProperty:
×
226
    {
227
      if(message.isRequest() || message.isEvent())
×
228
      {
229
        if(ObjectPtr object = m_handles.getItem(message.read<Handle>()))
×
230
        {
231
          if(AbstractVectorProperty* property = object->getVectorProperty(message.read<std::string>()); property && !property->isInternal())
×
232
          {
233
            try
234
            {
235
              const size_t index = message.read<uint32_t>();
×
236

237
              switch(message.read<ValueType>())
×
238
              {
239
                case ValueType::Boolean:
×
240
                  property->setBool(index, message.read<bool>());
×
241
                  break;
×
242

243
                case ValueType::Integer:
×
244
                  property->setInt64(index, message.read<int64_t>());
×
245
                  break;
×
246

247
                case ValueType::Float:
×
248
                  property->setDouble(index, message.read<double>());
×
249
                  break;
×
250

251
                case ValueType::String:
×
252
                  property->setString(index, message.read<std::string>());
×
253
                  break;
×
254

255
                default:
×
256
                  throw std::runtime_error("invalid value type");
×
257
              }
258
            }
259
            catch(const std::exception& e) // set property failed
×
260
            {
261
              if(message.isRequest()) // send error response
×
262
              {
263
                m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1018_EXCEPTION_X, e.what()));
×
264
              }
265
              else // send changed event with current value:
266
                objectPropertyChanged(*property);
×
267
            }
×
268

269
            if(message.isRequest()) // send success response
×
270
              m_connection->sendMessage(Message::newResponse(message.command(), message.requestId()));
×
271
          }
272
          else if(message.isRequest()) // send error response
×
273
          {
274
            m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1016_UNKNOWN_PROPERTY));
×
275
          }
276
        }
277
        else if(message.isRequest()) // send error response
×
278
        {
279
          m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1015_UNKNOWN_OBJECT));
×
280
        }
×
281
      }
282
      return true;
×
283
    }
284
    case Message::Command::ObjectSetUnitPropertyUnit:
×
285
    {
286
      if(ObjectPtr object = m_handles.getItem(message.read<Handle>()))
×
287
      {
288
        if(AbstractUnitProperty* property = dynamic_cast<AbstractUnitProperty*>(object->getProperty(message.read<std::string>())); property && !property->isInternal())
×
289
        {
290
          try
291
          {
292
            property->setUnitValue(message.read<int64_t>());
×
293
          }
294
          catch(const std::exception&)
×
295
          {
296
            // set unit property unit failed, send changed event with current value:
297
            objectPropertyChanged(*property);
×
298
          }
×
299
        }
300
      }
×
301
      break;
×
302
    }
303
    case Message::Command::ObjectGetObjectPropertyObject:
×
304
      if(message.isRequest())
×
305
      {
306
        if(ObjectPtr object = m_handles.getItem(message.read<Handle>()))
×
307
        {
308
          if(auto* property = object->getObjectProperty(message.read<std::string>()); property && !property->isInternal())
×
309
          {
310
            if(auto obj = property->toObject())
×
311
            {
312
              auto response = Message::newResponse(message.command(), message.requestId());
×
313
              writeObject(*response, obj);
×
314
              m_connection->sendMessage(std::move(response));
×
315
            }
×
316
            else
317
              m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1015_UNKNOWN_OBJECT));
×
318
          }
319
          else // send error response
320
            m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1016_UNKNOWN_PROPERTY));
×
321
        }
322
        else // send error response
323
          m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1015_UNKNOWN_OBJECT));
×
324

325
        return true;
×
326
      }
327
      break;
×
328

329
    case Message::Command::ObjectGetObjectVectorPropertyObject:
×
330
      if(message.isRequest())
×
331
      {
332
        if(ObjectPtr object = m_handles.getItem(message.read<Handle>()))
×
333
        {
334
          if(auto* property = object->getVectorProperty(message.read<std::string>()); property && !property->isInternal())
×
335
          {
336
            const size_t startIndex = message.read<uint32_t>();
×
337
            const size_t endIndex = message.read<uint32_t>();
×
338

339
            if(endIndex >= startIndex && endIndex < property->size())
×
340
            {
341
              auto response = Message::newResponse(message.command(), message.requestId());
×
342
              for(size_t i = startIndex; i <= endIndex; i++)
×
343
                writeObject(*response, property->getObject(i));
×
344
              m_connection->sendMessage(std::move(response));
×
345
            }
×
346
            else // send error response
347
              m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1017_INVALID_INDICES));
×
348
          }
349
          else // send error response
350
            m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1016_UNKNOWN_PROPERTY));
×
351
        }
352
        else // send error response
353
          m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1015_UNKNOWN_OBJECT));
×
354

355
        return true;
×
356
      }
357
      break;
×
358

359
    case Message::Command::ObjectSetObjectPropertyById:
×
360
    {
361
      if(ObjectPtr object = m_handles.getItem(message.read<Handle>()))
×
362
      {
363
        if(AbstractObjectProperty* property = dynamic_cast<AbstractObjectProperty*>(object->getProperty(message.read<std::string>())); property && !property->isInternal())
×
364
        {
365
          try
366
          {
367
            const auto id = message.read<std::string>();
×
368
            if(!id.empty())
×
369
            {
370
              if(ObjectPtr obj = Traintastic::instance->world->getObjectByPath(id))
×
371
                property->fromObject(obj);
×
372
              else
373
                throw std::runtime_error("");
×
374
            }
375
            else
376
              property->fromObject(nullptr);
×
377
          }
×
378
          catch(const std::exception&)
×
379
          {
380
            // set object property failed, send changed event with current value:
381
            objectPropertyChanged(*property);
×
382
          }
×
383
        }
384
      }
×
385
      break;
×
386
    }
387
    case Message::Command::ObjectCallMethod:
×
388
    {
389
      if(ObjectPtr object = m_handles.getItem(message.read<Handle>()))
×
390
      {
391
        if(AbstractMethod* method = object->getMethod(message.read<std::string>()); method && !method->isInternal())
×
392
        {
393
          if(callMethod(message, *method))
×
394
          {
395
            return true;
×
396
          }
397
        }
398
      }
×
399
      break;
×
400
    }
401
    case Message::Command::CallMethod:
×
402
    {
403
      const auto path = message.read<std::string_view>();
×
404
      const auto [objectPath, methodName] = splitOnLastDot(path);
×
405

406
      if(const auto& world = Traintastic::instance->world.value())
×
407
      {
408
        if(ObjectPtr object = world->getObjectByPath(objectPath))
×
409
        {
410
          if(AbstractMethod* method = object->getMethod(methodName); method && !method->isInternal())
×
411
          {
412
            if(callMethod(message, *method))
×
413
            {
414
              return true;
×
415
            }
416
          }
417
        }
×
418
      }
419
      break;
×
420
    }
421
    case Message::Command::GetTableModel:
×
422
    {
423
      if(ObjectPtr object = m_handles.getItem(message.read<Handle>()))
×
424
      {
425
        if(auto* table = dynamic_cast<Table*>(object.get()))
×
426
        {
427
          TableModelPtr model = table->getModel();
×
428
          assert(model);
×
429
          auto response = Message::newResponse(message.command(), message.requestId());
×
430
          writeTableModel(*response, model);
×
431
          m_connection->sendMessage(std::move(response));
×
432

433
          model->columnHeadersChanged = [this](const TableModelPtr& tableModel)
×
434
            {
435
              auto event = Message::newEvent(Message::Command::TableModelColumnHeadersChanged);
×
436
              event->write(m_handles.getHandle(std::dynamic_pointer_cast<Object>(tableModel)));
×
437
              event->write(tableModel->columnCount());
×
438
              for(const auto& text : tableModel->columnHeaders())
×
439
                event->write(text);
×
440
              m_connection->sendMessage(std::move(event));
×
441
            };
×
442

443
          model->rowCountChanged = [this](const TableModelPtr& tableModel)
×
444
            {
445
              auto event = Message::newEvent(Message::Command::TableModelRowCountChanged);
×
446
              event->write(m_handles.getHandle(std::dynamic_pointer_cast<Object>(tableModel)));
×
447
              event->write(tableModel->rowCount());
×
448
              m_connection->sendMessage(std::move(event));
×
449
            };
×
450

451
          model->updateRegion = [this](const TableModelPtr& tableModel, const TableModel::Region& region)
×
452
            {
453
              auto event = Message::newEvent(Message::Command::TableModelUpdateRegion);
×
454
              event->write(m_handles.getHandle(std::dynamic_pointer_cast<Object>(tableModel)));
×
455
              event->write(region.columnMin);
×
456
              event->write(region.columnMax);
×
457
              event->write(region.rowMin);
×
458
              event->write(region.rowMax);
×
459

460
              for(uint32_t row = region.rowMin; row <= region.rowMax; row++)
×
461
                for(uint32_t column = region.columnMin; column <= region.columnMax; column++)
×
462
                  event->write(tableModel->getText(column, row));
×
463

464
              m_connection->sendMessage(std::move(event));
×
465
            };
×
466

467
          return true;
×
468
        }
×
469
      }
×
470
      m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1019_OBJECT_NOT_A_TABLE));
×
471
      return true;
×
472
    }
473
    case Message::Command::ReleaseTableModel:
×
474
      m_handles.removeHandle(message.read<Handle>());
×
475
      break;
×
476

477
    case Message::Command::TableModelSetRegion:
×
478
    {
479
      TableModelPtr model = std::dynamic_pointer_cast<TableModel>(m_handles.getItem(message.read<Handle>()));
×
480
      if(model)
×
481
      {
482
        TableModel::Region region;
×
483
        message.read(region.columnMin);
×
484
        message.read(region.columnMax);
×
485
        message.read(region.rowMin);
×
486
        message.read(region.rowMax);
×
487
        model->setRegion(region);
×
488
      }
489
      break;
×
490
    }
×
491
    case Message::Command::InputMonitorGetInputInfo:
×
492
    {
493
      auto inputMonitor = std::dynamic_pointer_cast<InputMonitor>(m_handles.getItem(message.read<Handle>()));
×
494
      if(inputMonitor)
×
495
      {
496
        auto inputInfo = inputMonitor->getInputInfo();
×
497
        auto response = Message::newResponse(message.command(), message.requestId());
×
498
        response->write<uint32_t>(inputInfo.size());
×
499
        for(auto& info : inputInfo)
×
500
        {
501
          response->write(info.address);
×
502
          response->write(info.used);
×
503
          response->write(info.value);
×
504
        }
505
        m_connection->sendMessage(std::move(response));
×
506
        return true;
×
507
      }
×
508
      break;
×
509
    }
×
510
    case Message::Command::OutputKeyboardGetOutputInfo:
×
511
    {
512
      auto outputKeyboard = std::dynamic_pointer_cast<OutputKeyboard>(m_handles.getItem(message.read<Handle>()));
×
513
      if(outputKeyboard)
×
514
      {
515
        const auto outputType = outputKeyboard->outputType.value();
×
516
        auto outputInfo = outputKeyboard->getOutputInfo();
×
517
        auto response = Message::newResponse(message.command(), message.requestId());
×
518
        response->write<uint32_t>(outputInfo.size());
×
519
        for(auto& info : outputInfo)
×
520
        {
521
          response->write(info.address);
×
522
          response->write(info.used);
×
523
          switch(outputType)
×
524
          {
525
            case OutputType::Single:
×
526
              response->write(std::get<TriState>(info.value));
×
527
              break;
×
528

529
            case OutputType::Pair:
×
530
              response->write(std::get<OutputPairValue>(info.value));
×
531
              break;
×
532

533
            case OutputType::Aspect: /*[[unlikely]]*/
×
534
            case OutputType::ECoSState: /*[[unlikely]]*/
535
              assert(false);
×
536
              break;
537
          }
538
        }
539
        m_connection->sendMessage(std::move(response));
×
540
        return true;
×
541
      }
×
542
      break;
×
543
    }
×
544
    case Message::Command::BoardGetTileData:
×
545
    {
546
      auto board = std::dynamic_pointer_cast<Board>(m_handles.getItem(message.read<Handle>()));
×
547
      if(board)
×
548
      {
549
        auto response = Message::newResponse(message.command(), message.requestId());
×
550
        for(const auto& it : board->tileMap())
×
551
        {
552
          const Tile& tile = *(it.second);
×
553
          if(it.first != tile.location()) // only tiles at origin
×
554
            continue;
×
555
          response->write(tile.location());
×
556
          response->write(tile.data());
×
557
          assert(tile.data().isActive() == isActive(tile.data().id()));
×
558
          if(tile.data().isActive())
×
559
            writeObject(*response, it.second);
×
560
        }
561
        m_connection->sendMessage(std::move(response));
×
562
        return true;
×
563
      }
×
564
      break;
×
565
    }
×
566
    case Message::Command::BoardGetTileInfo:
×
567
    {
568
      auto response = Message::newResponse(message.command(), message.requestId());
×
569
      const auto& info = Tiles::getInfo();
×
570
      response->write<uint32_t>(info.size());
×
571
      for(const auto& item : info)
×
572
      {
573
        response->writeBlock();
×
574
        response->write(item.classId);
×
575
        response->write(item.tileId);
×
576
        response->write(item.rotates);
×
577
        response->write(item.menu);
×
578
        response->writeBlockEnd();
×
579
      }
580
      m_connection->sendMessage(std::move(response));
×
581
      return true;
×
582
    }
×
583
    case Message::Command::ServerLog:
×
584
      if(message.read<bool>())
×
585
      {
586
        if(auto* logger = Log::getMemoryLogger())
×
587
        {
588
          m_memoryLoggerChanged = logger->changed.connect(std::bind(&Session::memoryLoggerChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
×
589
          memoryLoggerChanged(*logger, logger->size(), 0);
×
590
        }
591
      }
592
      else // disable
593
        m_memoryLoggerChanged.disconnect();
×
594
      break;
×
595

596
    case Message::Command::ImportWorld:
×
597
      if(message.isRequest())
×
598
      {
599
        try
600
        {
601
          std::vector<std::byte> worldData;
×
602
          message.read(worldData);
×
603
          Traintastic::instance->importWorld(worldData);
×
604
          m_connection->sendMessage(Message::newResponse(message.command(), message.requestId()));
×
605
        }
×
606
        catch(const LogMessageException& e)
×
607
        {
608
          m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), e.message(), e.args()));
×
609
        }
×
610
      }
611
      break;
×
612

613
    case Message::Command::ExportWorld:
×
614
      if(message.isRequest())
×
615
      {
616
        if(Traintastic::instance->world)
×
617
        {
618
          try
619
          {
620
            std::vector<std::byte> worldData;
×
621
            Traintastic::instance->world->export_(worldData);
×
622
            auto response = Message::newResponse(message.command(), message.requestId());
×
623
            response->write(worldData);
×
624
            m_connection->sendMessage(std::move(response));
×
625
          }
×
626
          catch(const LogMessageException& e)
×
627
          {
628
            m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), e.message(), e.args()));
×
629
          }
×
630
        }
631
        else
632
        {
633
          m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1010_EXPORTING_WORLD_FAILED_X, "nullptr"));
×
634
        }
635
        return true;
×
636
      }
637
      break;
×
638

639
    case Message::Command::CreateObject:
×
640
      if(message.isRequest())
×
641
      {
642
        if(Traintastic::instance->world)
×
643
        {
644
          const auto classId = message.read<std::string_view>();
×
645
          if(classId == ClientThrottle::classId)
×
646
          {
647
            auto throttle = ClientThrottle::create(*Traintastic::instance->world);
×
648
            auto response = message.response();
×
649
            writeObject(*response, throttle);
×
650
            m_connection->sendMessage(std::move(response));
×
651
          }
×
652
          else
653
          {
654
            m_connection->sendMessage(message.errorResponse(LogMessage::C1015_UNKNOWN_OBJECT)); // FIXME change error
×
655
          }
656
        }
657
        else
658
        {
659
          m_connection->sendMessage(message.errorResponse(LogMessage::C1015_UNKNOWN_OBJECT)); // FIXME change error
×
660
        }
661
        return true;
×
662
      }
663
      break;
×
664

665
    case Message::Command::ObjectListGetObjects:
×
666
      if(message.isRequest())
×
667
      {
668
        if(ObjectPtr object = m_handles.getItem(message.read<Handle>()))
×
669
        {
670
          if(auto* list = dynamic_cast<AbstractObjectList*>(object.get()))
×
671
          {
672
            const uint32_t startIndex = message.read<uint32_t>();
×
673
            const uint32_t endIndex = message.read<uint32_t>();
×
674

675
            if(endIndex >= startIndex && endIndex < list->length.value())
×
676
            {
677
              auto response = message.response();
×
678
              for(uint32_t i = startIndex; i <= endIndex; i++)
×
679
              {
680
                writeObject(*response, list->getObject(i));
×
681
              }
682
              m_connection->sendMessage(std::move(response));
×
683
            }
×
684
            else // send error response
685
            {
686
              m_connection->sendMessage(message.errorResponse(LogMessage::C1017_INVALID_INDICES));
×
687
            }
688
          }
689
          else
690
          {
691
            m_connection->sendMessage(message.errorResponse(LogMessage::C1015_UNKNOWN_OBJECT));
×
692
          }
693
        }
694
        else
695
        {
696
          m_connection->sendMessage(message.errorResponse(LogMessage::C1015_UNKNOWN_OBJECT));
×
697
        }
×
698
        return true;
×
699
      }
700
      break;
×
701

NEW
702
    case Message::Command::GetDiagnosticReport:
×
NEW
703
      if(message.isRequest())
×
704
      {
NEW
705
        auto response = message.response();
×
706

NEW
707
        response->write(Traintastic::getInfo());
×
708

709
        {
NEW
710
          std::string serverLog;
×
NEW
711
          if(auto* memoryLogger = Log::getMemoryLogger())
×
712
          {
NEW
713
            serverLog.reserve(memoryLogger->size() * 50); // ~50 chars/line
×
714

715
#ifdef HAS_CXX20_TIMEZONES
NEW
716
            const auto tz = std::chrono::current_zone();
×
717
#endif
NEW
718
            for(const auto& log : *memoryLogger)
×
719
            {
720
#ifdef HAS_CXX20_TIMEZONES
NEW
721
              const std::chrono::zoned_time zoneTime{tz, log.time};
×
NEW
722
              const auto localTime = zoneTime.get_local_time();
×
723
#else
724
              const auto localTime = log.time;
725
#endif
NEW
726
              const auto s = std::chrono::floor<std::chrono::seconds>(localTime);
×
NEW
727
              const auto us = std::chrono::duration_cast<std::chrono::microseconds>(localTime - s).count();
×
728

NEW
729
              serverLog.append(
×
NEW
730
                std::format(
×
731
                  "{:%Y-%m-%d;%H:%M:%S}.{:06}"
732
#ifdef HAS_CXX20_TIMEZONES
733
                  "{:%z}"
734
#endif
735
                  ";{};{}{:04};",
736
                  s,
737
                  us,
738
#ifdef HAS_CXX20_TIMEZONES
739
                  zoneTime,
740
#endif
NEW
741
                  log.objectId,
×
NEW
742
                  logMessageChar(log.message),
×
NEW
743
                  logMessageNumber(log.message)));
×
744

NEW
745
              if(log.args)
×
746
              {
NEW
747
                serverLog.append(Logger::toString(log.message, *log.args));
×
748
              }
749
              else
750
              {
NEW
751
                serverLog.append(Logger::toString(log.message));
×
752
              }
753

NEW
754
              serverLog.append("\n");
×
755
            }
756
          }
NEW
757
          response->write(serverLog);
×
NEW
758
        }
×
759

760
        {
NEW
761
          std::vector<std::byte> worldData;
×
NEW
762
          Traintastic::instance->world->export_(worldData);
×
NEW
763
          response->write(worldData);
×
NEW
764
        }
×
765

NEW
766
        m_connection->sendMessage(std::move(response));
×
NEW
767
      }
×
NEW
768
      break;
×
769

770
    default:
×
771
      break;
×
772
  }
773
  return false;
×
774
}
775

776
bool Session::isSessionObject(const ObjectPtr& object)
×
777
{
778
  assert(object);
×
779
  return dynamic_cast<ClientThrottle*>(object.get());
×
780
}
781

782
bool Session::callMethod(const Message& message, AbstractMethod& method)
×
783
{
784
  const auto resultType = message.read<ValueType>();
×
785
  const auto argumentCount = message.read<uint8_t>();
×
786

787
  Arguments args;
×
788
  for(uint8_t i = 0; i < argumentCount; i++)
×
789
  {
790
    switch(message.read<ValueType>())
×
791
    {
792
      case ValueType::Boolean:
×
793
        args.push_back(message.read<bool>());
×
794
        break;
×
795

796
      case ValueType::Enum:
×
797
      case ValueType::Integer:
798
      case ValueType::Set:
799
        args.push_back(message.read<int64_t>());
×
800
        break;
×
801

802
      case ValueType::Float:
×
803
        args.push_back(message.read<double>());
×
804
        break;
×
805

806
      case ValueType::String:
×
807
      {
808
        auto arg = message.read<std::string>();
×
809
        if(i < method.argumentTypeInfo().size() && method.argumentTypeInfo()[i].type == ValueType::Object)
×
810
        {
811
          if(arg.empty())
×
812
            args.push_back(ObjectPtr());
×
813
          else if(ObjectPtr obj = Traintastic::instance->world->getObjectByPath(arg))
×
814
            args.push_back(obj);
×
815
          else
816
            args.push_back(arg);
×
817
        }
818
        else
819
          args.push_back(arg);
×
820
        break;
×
821
      }
×
822
      case ValueType::Object:
×
823
        args.push_back(m_handles.getItem(message.read<Handle>()));
×
824
        break;
×
825

826
      default:
×
827
        assert(false);
×
828
        break;
829
    }
830
  }
831

832
  try
833
  {
834
    AbstractMethod::Result result = method.call(args);
×
835

836
    if(message.isRequest())
×
837
    {
838
      auto response = Message::newResponse(message.command(), message.requestId());
×
839

840
      switch(resultType)
×
841
      {
842
        case ValueType::Boolean:
×
843
          response->write(std::get<bool>(result));
×
844
          break;
×
845

846
        case ValueType::Integer:
×
847
        case ValueType::Enum:
848
        case ValueType::Set:
849
          response->write(std::get<int64_t>(result));
×
850
          break;
×
851

852
        case ValueType::Float:
×
853
          response->write(std::get<double>(result));
×
854
          break;
×
855

856
        case ValueType::String:
×
857
          response->write(std::get<std::string>(result));
×
858
          break;
×
859

860
        case ValueType::Object:
×
861
          writeObject(*response, std::get<ObjectPtr>(result));
×
862
          break;
×
863

864
        case ValueType::Invalid:
×
865
          break;
×
866
      }
867

868
      m_connection->sendMessage(std::move(response));
×
869
      return true;
×
870
    }
×
871
  }
×
872
  catch(const LogMessageException& e)
×
873
  {
874
    if(message.isRequest())
×
875
    {
876
      m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), e.message(), e.args()));
×
877
      return true;
×
878
    }
879
    // we can't report it back to the caller, so just log it.
880
    Log::log(method.object(), e.message(), e.args());
×
881
  }
×
882
  catch(const std::exception& e)
×
883
  {
884
    if(message.isRequest())
×
885
    {
886
      m_connection->sendMessage(Message::newErrorResponse(message.command(), message.requestId(), LogMessage::C1018_EXCEPTION_X, e.what()));
×
887
      return true;
×
888
    }
889
  }
×
890
  return false;
×
891
}
×
892

893
void Session::writeObject(Message& message, const ObjectPtr& object)
×
894
{
895
  message.writeBlock(); // object
×
896

897
  auto handle = m_handles.getHandle(object);
×
898
  if(handle == Handles::invalidHandle)
×
899
  {
900
    using namespace std::placeholders;
901

902
    handle = m_handles.addItem(object);
×
903

904
    m_objectSignals.emplace(handle, object->onDestroying.connect(std::bind(&Session::objectDestroying, this, std::placeholders::_1)));
×
905
    m_objectSignals.emplace(handle, object->propertyChanged.connect(std::bind(&Session::objectPropertyChanged, this, std::placeholders::_1)));
×
906
    m_objectSignals.emplace(handle, object->attributeChanged.connect(std::bind(&Session::objectAttributeChanged, this, std::placeholders::_1)));
×
907

908
    if(auto* board = dynamic_cast<Board*>(object.get()))
×
909
    {
910
      m_objectSignals.emplace(handle, board->tileDataChanged.connect(std::bind(&Session::boardTileDataChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)));
×
911
    }
912

913
    bool hasPublicEvents = false;
×
914

915
    message.write(handle);
×
916
    message.write(object->getClassId());
×
917

918
    message.writeBlock(); // items
×
919
    const InterfaceItems& interfaceItems = object->interfaceItems();
×
920
    for(const auto& name : interfaceItems.names())
×
921
    {
922
      InterfaceItem& item = interfaceItems[name];
×
923

924
      if(item.isInternal())
×
925
        continue;
×
926

927
      message.writeBlock(); // item
×
928
      message.write(name);
×
929

930
      if(auto* baseProperty = dynamic_cast<BaseProperty*>(&item))
×
931
      {
932
        AbstractProperty* property = nullptr;
×
933
        AbstractUnitProperty* unitProperty = nullptr;
×
934
        AbstractVectorProperty* vectorProperty = nullptr;
×
935

936
        if((property = dynamic_cast<AbstractProperty*>(baseProperty)))
×
937
        {
938
          if((unitProperty = dynamic_cast<AbstractUnitProperty*>(property)))
×
939
            message.write(InterfaceItemType::UnitProperty);
×
940
          else
941
            message.write(InterfaceItemType::Property);
×
942
        }
943
        else if((vectorProperty = dynamic_cast<AbstractVectorProperty*>(baseProperty)))
×
944
          message.write(InterfaceItemType::VectorProperty);
×
945
        else
946
        {
947
          assert(false);
×
948
          continue;
949
        }
950

951
        message.write(baseProperty->flags());
×
952
        message.write(baseProperty->type());
×
953

954
        if(baseProperty->type() == ValueType::Enum)
×
955
          message.write(baseProperty->enumName());
×
956
        else if(baseProperty->type() == ValueType::Set)
×
957
          message.write(baseProperty->setName());
×
958

959
        if(property)
×
960
        {
961
          writePropertyValue(message, *property);
×
962

963
          if(unitProperty)
×
964
          {
965
            message.write(unitProperty->unitName());
×
966
            message.write(unitProperty->unitValue());
×
967
          }
968
        }
969
        else if(vectorProperty)
×
970
          writeVectorPropertyValue(message, *vectorProperty);
×
971
        else
972
          assert(false);
×
973
      }
974
      else if(const auto* method = dynamic_cast<const AbstractMethod*>(&item))
×
975
      {
976
        message.write(InterfaceItemType::Method);
×
977
        message.write(method->resultTypeInfo().type);
×
978
        message.write(static_cast<uint8_t>(method->argumentTypeInfo().size()));
×
979
        for(const auto& info : method->argumentTypeInfo())
×
980
          message.write(info.type);
×
981
      }
982
      else if(const auto* event = dynamic_cast<const AbstractEvent*>(&item))
×
983
      {
984
        hasPublicEvents = true;
×
985

986
        message.write(InterfaceItemType::Event);
×
987
        message.write(static_cast<uint8_t>(event->argumentTypeInfo().size()));
×
988
        for(const auto& typeInfo : event->argumentTypeInfo())
×
989
          writeTypeInfo(message, typeInfo);
×
990
      }
991
      else
992
        assert(false);
×
993

994
      message.writeBlock(); // attributes
×
995

996
      for(const auto& it : item.attributes())
×
997
      {
998
        const AbstractAttribute& attribute = *it.second;
×
999
        message.writeBlock(); // attribute
×
1000
        writeAttribute(message, attribute);
×
1001
        message.writeBlockEnd(); // end attribute
×
1002
      }
1003

1004
      message.writeBlockEnd(); // end attributes
×
1005
      message.writeBlockEnd(); // end item
×
1006
    }
1007
    message.writeBlockEnd(); // end items
×
1008

1009
    if(hasPublicEvents)
×
1010
      m_objectSignals.emplace(handle, object->onEventFired.connect(std::bind(&Session::objectEventFired, this, std::placeholders::_1, std::placeholders::_2)));
×
1011
  }
1012
  else
1013
    message.write(handle);
×
1014

1015
  message.writeBlockEnd(); // end object
×
1016
}
×
1017

1018
void Session::writeTableModel(Message& message, const TableModelPtr& model)
×
1019
{
1020
  message.writeBlock(); // model
×
1021
  assert(m_handles.getHandle(std::dynamic_pointer_cast<Object>(model)) == Handles::invalidHandle);
×
1022
  auto handle = m_handles.addItem(std::dynamic_pointer_cast<Object>(model));
×
1023
  message.write(handle);
×
1024
  message.write(model->getClassId());
×
1025
  message.write(model->columnCount());
×
1026
  for(const auto& text : model->columnHeaders())
×
1027
    message.write(text);
×
1028
  message.write(model->rowCount());
×
1029
  message.writeBlockEnd(); // end model
×
1030
}
×
1031

1032
void Session::memoryLoggerChanged(const MemoryLogger& logger, const uint32_t added, const uint32_t removed)
×
1033
{
1034
  auto event = Message::newEvent(Message::Command::ServerLog);
×
1035
  event->write(removed);
×
1036
  event->write(added);
×
1037

1038
  const uint32_t size = logger.size();
×
1039
  for(uint32_t i = size - added; i < size; i++)
×
1040
  {
1041
    const auto& log = logger[i];
×
1042
    event->write((std::chrono::duration_cast<std::chrono::microseconds>(log.time.time_since_epoch())).count());
×
1043
    event->write(log.objectId);
×
1044
    event->write(log.message);
×
1045
    const size_t argc = log.args ? std::min<size_t>(log.args->size(), std::numeric_limits<uint8_t>::max()) : 0;
×
1046
    event->write(static_cast<uint8_t>(argc));
×
1047
    for(size_t j = 0; j < argc; j++)
×
1048
      event->write(log.args->at(j));
×
1049
  }
1050

1051
  m_connection->sendMessage(std::move(event));
×
1052
}
×
1053

1054
void Session::objectDestroying(Object& object)
×
1055
{
1056
  const auto handle = m_handles.getHandle(object.shared_from_this());
×
1057
  m_handles.removeHandle(handle);
×
1058
  m_objectSignals.erase(handle);
×
1059

1060
  auto event = Message::newEvent(Message::Command::ObjectDestroyed, sizeof(Handle));
×
1061
  event->write(handle);
×
1062
  m_connection->sendMessage(std::move(event));
×
1063
}
×
1064

1065
void Session::objectPropertyChanged(BaseProperty& baseProperty)
×
1066
{
1067
  if(baseProperty.isInternal())
×
1068
    return;
×
1069

1070
  auto event = Message::newEvent(Message::Command::ObjectPropertyChanged);
×
1071
  event->write(m_handles.getHandle(baseProperty.object().shared_from_this()));
×
1072
  event->write(baseProperty.name());
×
1073
  event->write(baseProperty.type());
×
1074
  if(auto* property = dynamic_cast<AbstractProperty*>(&baseProperty))
×
1075
  {
1076
    writePropertyValue(*event, *property);
×
1077

1078
    if(auto* unitProperty = dynamic_cast<AbstractUnitProperty*>(property))
×
1079
      event->write(unitProperty->unitValue());
×
1080
  }
1081
  else if(auto* vectorProperty = dynamic_cast<AbstractVectorProperty*>(&baseProperty))
×
1082
    writeVectorPropertyValue(*event, *vectorProperty);
×
1083
  else
1084
    assert(false);
×
1085

1086
  m_connection->sendMessage(std::move(event));
×
1087
}
×
1088

1089
void Session::writePropertyValue(Message& message , const AbstractProperty& property)
×
1090
{
1091
  switch(property.type())
×
1092
  {
1093
    case ValueType::Boolean:
×
1094
      message.write(property.toBool());
×
1095
      break;
×
1096

1097
    case ValueType::Enum:
×
1098
    case ValueType::Integer:
1099
    case ValueType::Set:
1100
      message.write(property.toInt64());
×
1101
      break;
×
1102

1103
    case ValueType::Float:
×
1104
      message.write(property.toDouble());
×
1105
      break;
×
1106

1107
    case ValueType::String:
×
1108
      message.write(property.toString());
×
1109
      break;
×
1110

1111
    case ValueType::Object:
×
1112
      if(ObjectPtr obj = property.toObject())
×
1113
        message.write(obj->getObjectId());
×
1114
      else
1115
        message.write<std::string_view>("");
×
1116
      break;
×
1117

1118
    case ValueType::Invalid:
×
1119
      assert(false);
×
1120
      break;
1121
  }
1122
}
×
1123

1124
void Session::writeVectorPropertyValue(Message& message , const AbstractVectorProperty& vectorProperty)
×
1125
{
1126
  const size_t size = vectorProperty.size();
×
1127
  message.write(static_cast<uint32_t>(size));
×
1128

1129
  switch(vectorProperty.type())
×
1130
  {
1131
    case ValueType::Boolean:
×
1132
      for(size_t i = 0; i < size; i++)
×
1133
        message.write(vectorProperty.getBool(i));
×
1134
      break;
×
1135

1136
    case ValueType::Enum:
×
1137
    case ValueType::Integer:
1138
    case ValueType::Set:
1139
      for(size_t i = 0; i < size; i++)
×
1140
        message.write(vectorProperty.getInt64(i));
×
1141
      break;
×
1142

1143
    case ValueType::Float:
×
1144
      for(size_t i = 0; i < size; i++)
×
1145
        message.write(vectorProperty.getDouble(i));
×
1146
      break;
×
1147

1148
    case ValueType::String:
×
1149
      for(size_t i = 0; i < size; i++)
×
1150
        message.write(vectorProperty.getString(i));
×
1151
      break;
×
1152

1153
    case ValueType::Object:
×
1154
      for(size_t i = 0; i < size; i++)
×
1155
        if(ObjectPtr obj = vectorProperty.getObject(i))
×
1156
          message.write(obj->getObjectId());
×
1157
        else
1158
          message.write<std::string>("");
×
1159
      break;
×
1160

1161
    case ValueType::Invalid:
×
1162
      assert(false);
×
1163
      break;
1164
  }
1165
}
×
1166

1167
void Session::objectAttributeChanged(AbstractAttribute& attribute)
×
1168
{
1169
  auto event = Message::newEvent(Message::Command::ObjectAttributeChanged);
×
1170
  event->write(m_handles.getHandle(attribute.item().object().shared_from_this()));
×
1171
  event->write(attribute.item().name());
×
1172
  writeAttribute(*event, attribute);
×
1173
  m_connection->sendMessage(std::move(event));
×
1174
}
×
1175

1176
void Session::objectEventFired(const AbstractEvent& event, const Arguments& arguments)
×
1177
{
1178
  auto message = Message::newEvent(Message::Command::ObjectEventFired);
×
1179
  message->write(m_handles.getHandle(event.object().shared_from_this()));
×
1180
  message->write(event.name());
×
1181
  message->write(static_cast<uint32_t>(arguments.size()));
×
1182
  size_t i = 0;
×
1183
  for(const auto& typeInfo : event.argumentTypeInfo())
×
1184
  {
1185
    switch(typeInfo.type)
×
1186
    {
1187
      case ValueType::Boolean:
×
1188
        message->write(std::get<bool>(arguments[i]));
×
1189
        break;
×
1190

1191
      case ValueType::Enum:
×
1192
      case ValueType::Integer:
1193
      case ValueType::Set:
1194
        message->write(std::get<int64_t>(arguments[i]));
×
1195
        break;
×
1196

1197
      case ValueType::Float:
×
1198
        message->write(std::get<double>(arguments[i]));
×
1199
        break;
×
1200

1201
      case ValueType::String:
×
1202
        message->write(std::get<std::string>(arguments[i]));
×
1203
        break;
×
1204

1205
      case ValueType::Object:
×
1206
        if(ObjectPtr obj = std::get<ObjectPtr>(arguments[i]))
×
1207
          message->write(obj->getObjectId());
×
1208
        else
1209
          message->write(std::string_view{});
×
1210
        break;
×
1211

1212
      case ValueType::Invalid:
×
1213
        assert(false);
×
1214
        return;
1215
    }
1216
    i++;
×
1217
  }
1218
  m_connection->sendMessage(std::move(message));
×
1219
}
×
1220

1221
void Session::writeAttribute(Message& message , const AbstractAttribute& attribute)
×
1222
{
1223
  message.write(attribute.name());
×
1224
  message.write(attribute.type());
×
1225
  if(const auto* valueAttribute = dynamic_cast<const AbstractValueAttribute*>(&attribute))
×
1226
  {
1227
    message.write(AttributeType::Value);
×
1228
    switch(attribute.type())
×
1229
    {
1230
      case ValueType::Boolean:
×
1231
        message.write(valueAttribute->toBool());
×
1232
        break;
×
1233

1234
      case ValueType::Enum:
×
1235
      case ValueType::Integer:
1236
        message.write(valueAttribute->toInt64());
×
1237
        break;
×
1238

1239
      case ValueType::Float:
×
1240
        message.write(valueAttribute->toDouble());
×
1241
        break;
×
1242

1243
      case ValueType::String:
×
1244
        message.write(valueAttribute->toString());
×
1245
        break;
×
1246

1247
      default:
×
1248
        assert(false);
×
1249
        break;
1250
    }
1251
  }
1252
  else if(const auto* valuesAttributes = dynamic_cast<const AbstractValuesAttribute*>(&attribute))
×
1253
  {
1254
    const uint32_t length = valuesAttributes->length();
×
1255
    message.write(AttributeType::Values);
×
1256
    message.write(length);
×
1257
    switch(attribute.type())
×
1258
    {
1259
      case ValueType::Boolean:
×
1260
        for(uint32_t i = 0; i < length; i++)
×
1261
          message.write(valuesAttributes->getBool(i));
×
1262
        break;
×
1263

1264
      case ValueType::Enum:
×
1265
      case ValueType::Integer:
1266
        for(uint32_t i = 0; i < length; i++)
×
1267
          message.write(valuesAttributes->getInt64(i));
×
1268
        break;
×
1269

1270
      case ValueType::Float:
×
1271
        for(uint32_t i = 0; i < length; i++)
×
1272
          message.write(valuesAttributes->getDouble(i));
×
1273
        break;
×
1274

1275
      case ValueType::String:
×
1276
        for(uint32_t i = 0; i < length; i++)
×
1277
          message.write(valuesAttributes->getString(i));
×
1278
        break;
×
1279

1280
      default:
×
1281
        assert(false);
×
1282
        break;
1283
    }
1284
  }
1285
  else
1286
    assert(false);
×
1287
}
×
1288

1289
void Session::writeTypeInfo(Message& message, const TypeInfo& typeInfo)
×
1290
{
1291
  assert((typeInfo.type == ValueType::Enum) != typeInfo.enumName.empty());
×
1292
  assert((typeInfo.type == ValueType::Set) != typeInfo.setName.empty());
×
1293

1294
  message.write(typeInfo.type);
×
1295
  if(typeInfo.type == ValueType::Enum)
×
1296
    message.write(typeInfo.enumName);
×
1297
  else if(typeInfo.type == ValueType::Set)
×
1298
    message.write(typeInfo.setName);
×
1299
}
×
1300

1301
void Session::boardTileDataChanged(Board& board, const TileLocation& location, const TileData& data)
×
1302
{
1303
  auto event = Message::newEvent(Message::Command::BoardTileDataChanged);
×
1304
  event->write(m_handles.getHandle(board.shared_from_this()));
×
1305
  event->write(location);
×
1306
  event->write(data);
×
1307
  assert(data.isActive() == isActive(data.id()));
×
1308
  if(data.isActive())
×
1309
  {
1310
    auto tile = board.getTile(location);
×
1311
    assert(tile);
×
1312
    writeObject(*event, tile);
×
1313
  }
×
1314
  m_connection->sendMessage(std::move(event));
×
1315
}
×
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