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

traintastic / traintastic / 20638780725

01 Jan 2026 12:44PM UTC coverage: 27.155% (-0.4%) from 27.527%
20638780725

push

github

reinder
bumped copyright year to 2026

7873 of 28993 relevant lines covered (27.15%)

191.86 hits per line

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

50.62
/server/src/world/worldloader.cpp
1
/**
2
 * server/src/world/worldloader.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 "worldloader.hpp"
24
#include <fstream>
25
#include <boost/algorithm/string.hpp>
26
#include <boost/uuid/string_generator.hpp>
27
#include <boost/uuid/uuid_io.hpp>
28
#include "world.hpp"
29
#include "../core/isvalidobjectid.hpp"
30
#include "../utils/startswith.hpp"
31
#include "../utils/stripsuffix.hpp"
32
#include "ctwreader.hpp"
33
#include "../log/logmessageexception.hpp"
34
#include <version.hpp>
35

36
#include "../board/board.hpp"
37
#include "../board/tile/tiles.hpp"
38
#include "../hardware/interface/interfaces.hpp"
39
#include "../hardware/interface/dccexinterface.hpp" //! \todo Remove in v0.4
40
#include "../hardware/decoder/decoder.hpp"
41
#include "../hardware/decoder/decoderfunction.hpp"
42
#include "../hardware/identification/identification.hpp"
43
#include "../hardware/booster/booster.hpp"
44
#include "../vehicle/rail/railvehicles.hpp"
45
#include "../vehicle/rail/freightwagon.hpp" //! \todo Remove in v0.4
46
#include "../train/train.hpp"
47
#include "../train/trainblockstatus.hpp"
48
#include "../lua/script.hpp"
49
#include "../zone/zone.hpp"
50

51
using nlohmann::json;
52

53
WorldLoader::WorldLoader()
3✔
54
  : m_world{World::create()}
3✔
55
{
56
}
3✔
57

58
WorldLoader::WorldLoader(std::filesystem::path path)
3✔
59
  : WorldLoader()
3✔
60
{
61
  if(path.extension() == World::dotCTW)
3✔
62
    m_ctw = std::make_unique<CTWReader>(path);
3✔
63
  else
64
    m_path = std::move(path);
×
65

66
  load();
3✔
67
}
3✔
68

69
WorldLoader::WorldLoader(const std::vector<std::byte>& memory)
×
70
  : WorldLoader()
×
71
{
72
  m_ctw = std::make_unique<CTWReader>(memory);
×
73

74
  load();
×
75
}
×
76

77
WorldLoader::~WorldLoader() = default; // default here, so we can use a forward declaration of CTWReader in the header.
3✔
78

79
ObjectPtr WorldLoader::getObject(std::string_view id)
4✔
80
{
81
  std::vector<std::string> ids;
4✔
82
  boost::split(ids, id, [](char c){ return c == '.'; });
52✔
83
  auto itId = ids.cbegin();
4✔
84

85
  ObjectPtr obj;
4✔
86
  if(auto it = m_objects.find(*itId); it != m_objects.end())
4✔
87
  {
88
    if(!it->second.object)
4✔
89
      createObject(it->second);
×
90
    obj = it->second.object;
4✔
91
  }
92

93
  while(obj && ++itId != ids.cend())
6✔
94
  {
95
    AbstractProperty* property = obj->getProperty(*itId);
2✔
96
    if(property && property->type() == ValueType::Object)
2✔
97
      obj = property->toObject();
2✔
98
    else
99
      obj = nullptr;
×
100
  }
101

102
  return obj;
8✔
103
}
4✔
104

105
json WorldLoader::getState(const std::string& id) const
16✔
106
{
107
  return m_states.value(id, json::object());
32✔
108
}
109

110
void WorldLoader::load()
3✔
111
{
112
  m_states = json::object();
3✔
113

114
  json data;
3✔
115
  json state;
3✔
116

117
  // load file(s):
118
  if(m_ctw)
3✔
119
  {
120
    if(!m_ctw->readFile(World::filename, data))
3✔
121
      throw std::runtime_error(std::string("can't read ").append(World::filename));
×
122

123
    if(!m_ctw->readFile(World::filenameState, state))
3✔
124
      throw std::runtime_error(std::string("can't read ").append(World::filenameState));
×
125
  }
126
  else
127
  {
128
    std::ifstream file(m_path / World::filename);
×
129
    if(!file.is_open())
×
130
      throw std::runtime_error("can't open " + (m_path / World::filename).string());
×
131
    data = json::parse(file);
×
132

133
    std::ifstream stateFile(m_path / World::filenameState);
×
134
    if(!stateFile.is_open())
×
135
      throw std::runtime_error("can't open " + (m_path / World::filenameState).string());
×
136
    state = json::parse(stateFile);
×
137
  }
×
138

139
  // check if UUID is valid:
140
  m_world->uuid.setValueInternal(to_string(boost::uuids::string_generator()(std::string(data["uuid"]))));
3✔
141

142
  // check version:
143
  //! \todo require this for >= v0.2
144
  {
145
    json traintastic = data["traintastic"];
3✔
146
    if(traintastic.is_object())
3✔
147
    {
148
      json version = traintastic["version"];
3✔
149
      if(version.is_object())
3✔
150
      {
151
        const uint16_t major = version["major"].get<uint16_t>();
3✔
152
        const uint16_t minor = version["minor"].get<uint16_t>();
3✔
153
        const uint16_t patch = version["patch"].get<uint16_t>();
3✔
154

155
        if((major != TRAINTASTIC_VERSION_MAJOR) ||
3✔
156
            (minor > TRAINTASTIC_VERSION_MINOR) ||
3✔
157
            (minor == TRAINTASTIC_VERSION_MINOR && patch > TRAINTASTIC_VERSION_PATCH))
3✔
158
        {
159
          throw LogMessageException(LogMessage::C1013_CANT_LOAD_WORLD_SAVED_WITH_NEWER_VERSION_REQUIRES_AT_LEAST_X,
160
              std::string("Traintastic server v").append(std::to_string(major)).append(".").append(std::to_string(minor)).append(".").append(std::to_string(patch)));
×
161
        }
162
      }
163
    }
3✔
164
  }
3✔
165

166
  // state data
167
  if(state.is_object() && state["uuid"] == data["uuid"])
3✔
168
  {
169
    m_states = state["states"];
3✔
170
    auto stateObjects = state.value("objects", json::array());
9✔
171
    data["objects"].insert(data["objects"].end(), stateObjects.begin(), stateObjects.end());
3✔
172
  }
3✔
173

174
  // create a list of all objects
175
  m_objects.insert({m_world->getObjectId(), {data, m_world, false}});
6✔
176
  for(json object : data["objects"])
9✔
177
  {
178
    //! \todo Remove in v0.4
179
    if(object["class_id"].get<std::string_view>() == "output") // don't create Output objects, no longer stored in file.
6✔
180
    {
181
      continue;
×
182
    }
183

184
    if(auto it = object.find("id"); it != object.end())
6✔
185
    {
186
      auto id = it.value().get<std::string>();
6✔
187
      if(!isValidObjectId(id))
6✔
188
        throw std::runtime_error("invalid object id value");
×
189
      m_objects.insert({std::move(id), {object, nullptr, false}});
12✔
190
    }
6✔
191
    else
192
      throw std::runtime_error("id missing");
×
193
  }
6✔
194

195
  //! \todo Remove in v0.4
196
  {
197
    // patch for input refactor:
198
    for(auto& objectData : m_objects)
12✔
199
    {
200
      if(!objectData.second.json.contains("class_id"))
9✔
201
      {
202
        continue;
3✔
203
      }
204
      auto classId = objectData.second.json["class_id"].get<std::string_view>();
6✔
205
      if(classId == "board_tile.rail.sensor" ||
12✔
206
          classId == "board_tile.rail.nx_button" ||
12✔
207
          classId == "input_map_item.block")
12✔
208
      {
209
        const auto& input = objectData.second.json["input"];
×
210
        if(input.is_string())
×
211
        {
212
          if(auto it = m_objects.find(input.get<std::string>()); it != m_objects.end()) [[likely]]
×
213
          {
214
            objectData.second.json["interface"] = it->second.json["interface"];
×
215
            objectData.second.json["channel"] = it->second.json["channel"];
×
216
            objectData.second.json["address"] = it->second.json["address"];
×
217
          }
218
        }
219
        objectData.second.json.erase("input");
×
220
      }
221
      else if(classId == "board_tile.rail.block")
6✔
222
      {
223
        auto& inputMap = objectData.second.json["input_map"];
×
224
        if(inputMap.is_object())
×
225
        {
226
          auto& items = inputMap["items"];
×
227
          if(items.is_array())
×
228
          {
229
            for(auto& item : items)
×
230
            {
231
              const auto& input = item["input"];
×
232
              if(input.is_string())
×
233
              {
234
                if(auto it = m_objects.find(input.get<std::string>()); it != m_objects.end()) [[likely]]
×
235
                {
236
                  item["interface"] = it->second.json["interface"];
×
237
                  const int ch = it->second.json["channel"].get<int>();
×
238
                  if(ch == 0)
×
239
                  {
240
                    item["channel"] = "input";
×
241
                  }
242
                  else if(auto interface = m_objects.find(item["interface"].get<std::string>()); it != m_objects.end()) [[likely]]
×
243
                  {
244
                    const auto interfaceClassId = interface->second.json["class_id"].get<std::string_view>();
×
245
                    if(interfaceClassId == "interface.ecos")
×
246
                    {
247
                      if(ch == 1)
×
248
                      {
249
                        item["channel"] = "s88";
×
250
                      }
251
                      else if(ch == 2)
×
252
                      {
253
                        item["channel"] = "ecos_detector";
×
254
                      }
255
                      else [[unlikely]]
×
256
                      {
257
                        assert(false);
×
258
                      }
259
                    }
260
                    else if(interfaceClassId == "interface.hsi88")
×
261
                    {
262
                      if(ch == 1)
×
263
                      {
264
                        item["channel"] = "s88_left";
×
265
                      }
266
                      else if(ch == 2)
×
267
                      {
268
                        item["channel"] = "s88_middle";
×
269
                      }
270
                      else if(ch == 3)
×
271
                      {
272
                        item["channel"] = "s88_middle";
×
273
                      }
274
                      else [[unlikely]]
×
275
                      {
276
                        assert(false);
×
277
                      }
278
                    }
279
                    else if(interfaceClassId == "interface.z21")
×
280
                    {
281
                      if(ch == 1)
×
282
                      {
283
                        item["channel"] = "rbus";
×
284
                      }
285
                      else if(ch == 2)
×
286
                      {
287
                        item["channel"] = "loconet";
×
288
                      }
289
                      else [[unlikely]]
×
290
                      {
291
                        assert(false);
×
292
                      }
293
                    }
294
                    else [[unlikely]]
×
295
                    {
296
                      assert(false);
×
297
                    }
298
                  }
299
                  item["address"] = it->second.json["address"];
×
300
                }
301
              }
302
              item.erase("input");
×
303
            }
304
          }
305
        }
306
      }
307
    }
308
    // remove all input objects:
309
    for(auto it = m_objects.begin(); it != m_objects.end();)
12✔
310
    {
311
      if(it->second.json.contains("class_id") && it->second.json["class_id"].get<std::string_view>() == "input")
9✔
312
      {
313
        it = m_objects.erase(it);
×
314
      }
315
      else
316
      {
317
        it++;
9✔
318
      }
319
    }
320
  }
321

322
  // then create all objects
323
  for(auto& it : m_objects)
12✔
324
    if(!it.second.object)
9✔
325
      createObject(it.second);
6✔
326

327
  // and load their data/state
328
  for(auto& it : m_objects)
12✔
329
    if(!it.second.loaded)
9✔
330
      loadObject(it.second);
9✔
331

332
  // and finally notify loading is completed
333
  for(auto& it : m_objects)
12✔
334
    it.second.object->loaded();
9✔
335
}
12✔
336

337
void WorldLoader::createObject(ObjectData& objectData)
6✔
338
{
339
  assert(!objectData.object);
6✔
340

341
  std::string_view classId = objectData.json["class_id"].get<std::string_view>();
6✔
342
  std::string_view id = objectData.json["id"].get<std::string_view>();
6✔
343

344
  if(startsWith(classId, Interfaces::classIdPrefix))
6✔
345
  {
346
    if(classId == "interface.dccplusplus") //! \todo Remove in v0.4
×
347
    {
348
      objectData.json["dccex"] = objectData.json["dccplusplus"];
×
349
      objectData.json["dccex"]["class_id"] = "dccex_settings";
×
350
      objectData.json.erase("dccplusplus");
×
351
      classId = DCCEXInterface::classId;
×
352
    }
353
    objectData.object = Interfaces::create(*m_world, classId, id);
×
354
  }
355
  else if(classId == Decoder::classId)
6✔
356
  {
357
    if(objectData.json["protocol"].get<std::string_view>() == "dcc") //! \todo Remove in v0.4
1✔
358
    {
359
      if(objectData.json["long_address"].get<bool>())
×
360
        objectData.json["protocol"] = "dcc_long";
×
361
      else
362
        objectData.json["protocol"] = "dcc_short";
×
363
    }
364
    objectData.object = Decoder::create(*m_world, id);
1✔
365
  }
366
  else if(classId == Identification::classId)
5✔
367
    objectData.object = Identification::create(*m_world, id);
×
368
  else if(classId == Booster::classId)
5✔
369
  {
370
    objectData.object = Booster::create(*m_world, id);
×
371
  }
372
  else if(classId == Board::classId)
5✔
373
    objectData.object = Board::create(*m_world, id);
1✔
374
  else if(startsWith(classId, Tiles::classIdPrefix))
4✔
375
  {
376
    if(auto tile = Tiles::create(*m_world, classId, id))
1✔
377
    {
378
      // x, y, width, height are read in Board::load()
379
      tile->x.setValueInternal(objectData.json["x"]);
1✔
380
      tile->y.setValueInternal(objectData.json["y"]);
1✔
381
      tile->height.setValueInternal(objectData.json.value("height", 1));
2✔
382
      tile->width.setValueInternal(objectData.json.value("width", 1));
2✔
383
      objectData.object = tile;
1✔
384
    }
1✔
385
  }
386
  else if(startsWith(classId, RailVehicles::classIdPrefix))
3✔
387
  {
388
    if(classId == "vehicle.rail.freight_car") { classId = FreightWagon::classId; } //! \todo Remove in v0.4
1✔
389
    if(objectData.json.contains("lob") && !objectData.json.contains("length")) //! \todo Remove in v0.4
1✔
390
    {
391
      objectData.json["length"] = objectData.json["lob"];
×
392
      objectData.json.erase("lob");
×
393
    }
394
    objectData.object = RailVehicles::create(*m_world, classId, id);
1✔
395
  }
396
  else if(classId == Train::classId)
2✔
397
  {
398
    if(objectData.json.contains("lob") && !objectData.json.contains("length")) //! \todo Remove in v0.4
1✔
399
    {
400
      objectData.json["length"] = objectData.json["lob"];
×
401
      objectData.json.erase("lob");
×
402
    }
403
    objectData.object = Train::create(*m_world, id);
1✔
404
  }
405
  else if(classId == TrainBlockStatus::classId)
1✔
406
  {
407
    auto block = std::dynamic_pointer_cast<BlockRailTile>(getObject(objectData.json["block"].get<std::string_view>()));
×
408

409
    if(block) /*[[likely]]*/
×
410
    {
411
      if(objectData.json["train"].is_string())
×
412
      {
413
        auto train = std::dynamic_pointer_cast<Train>(getObject(objectData.json["train"].get<std::string_view>()));
×
414
        objectData.object = TrainBlockStatus::create(*block, *train, to<BlockTrainDirection>(objectData.json["direction"]), id);
×
415
      }
×
416
      else
417
      {
418
        objectData.object = TrainBlockStatus::create(*block, objectData.json["identification"].get<std::string>(), to<BlockTrainDirection>(objectData.json["direction"]), id);
×
419
      }
420
    }
421
  }
×
422
  else if(classId == TrainZoneStatus::classId)
1✔
423
  {
424
    if(auto zone = std::dynamic_pointer_cast<Zone>(getObject(objectData.json["zone"].get<std::string_view>()))) /*[[likely]]*/
×
425
    {
426
      auto train = std::dynamic_pointer_cast<Train>(getObject(objectData.json["train"].get<std::string_view>()));
×
427
      objectData.object = TrainZoneStatus::create(*zone, *train, to<ZoneTrainState>(objectData.json["state"]), id);
×
428
    }
×
429
  }
430
  else if(classId == Lua::Script::classId)
1✔
431
    objectData.object = Lua::Script::create(*m_world, id);
1✔
432
  else if(classId == Zone::classId)
×
433
  {
434
    objectData.object = Zone::create(*m_world, id);
×
435
  }
436

437
  if(!objectData.object)
6✔
438
    throw LogMessageException(LogMessage::C1012_UNKNOWN_CLASS_X_CANT_RECREATE_OBJECT_X, classId, id);
×
439
}
6✔
440

441
void WorldLoader::loadObject(ObjectData& objectData)
9✔
442
{
443
  assert(objectData.object);
9✔
444
  assert(!objectData.loaded);
9✔
445
  objectData.object->load(*this, objectData.json);
9✔
446
  objectData.loaded = true;
9✔
447
}
9✔
448

449
bool WorldLoader::readFile(const std::filesystem::path& filename, std::string& data)
1✔
450
{
451
  if(m_ctw)
1✔
452
  {
453
    if(!m_ctw->readFile(filename, data))
1✔
454
      return false;
×
455
  }
456
  else
457
  {
458
    std::ifstream file(m_path / filename, std::ios::in | std::ios::binary | std::ios::ate);
×
459
    if(!file.is_open())
×
460
      return false;
×
461
    const size_t size = file.tellg();
×
462
    data.resize(size);
×
463
    file.seekg(std::ios::beg);
×
464
    file.read(data.data(), size);
×
465
  }
×
466
  return true;
1✔
467
}
468

469
bool WorldLoader::readFile(const std::filesystem::path& filename, nlohmann::json& data)
×
470
{
471
  std::string text;
×
472
  if(readFile(filename, text))
×
473
  {
474
    try
475
    {
476
      data = nlohmann::json::parse(text);
×
477
      return true;
×
478
    }
479
    catch(...)
×
480
    {
481
    }
×
482
  }
483
  return false;
×
484
}
×
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