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

Return-To-The-Roots / s25client / 25609024779

09 May 2026 06:48PM UTC coverage: 50.245% (+0.008%) from 50.237%
25609024779

push

github

Flow86
Document print key screenshot shortcut

23091 of 45957 relevant lines covered (50.24%)

43928.37 hits per line

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

83.48
/libs/s25main/Replay.cpp
1
// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org)
2
//
3
// SPDX-License-Identifier: GPL-2.0-or-later
4

5
#include "Replay.h"
6
#include "Savegame.h"
7
#include "enum_cast.hpp"
8
#include "helpers/format.hpp"
9
#include "network/PlayerGameCommands.h"
10
#include "gameTypes/MapInfo.h"
11
#include <s25util/tmpFile.h>
12
#include <boost/filesystem.hpp>
13
#include <memory>
14
#include <mygettext/mygettext.h>
15

16
std::string Replay::GetSignature() const
9✔
17
{
18
    return "RTTRRP2";
9✔
19
}
20

21
// clang-format off
22
/// (Sub)-Version of the current replay file
23
/// Usage:
24
////    - Always save for the most current version
25
////    - Loading code may cope with file format changes
26
/// If the format changes (e.g. new enum values, types, ... increase this version and handle it in the loading code.
27
/// If the change cannot be handled:
28
///     - Remove all code handling this version.
29
///     - Reset this version to 0
30
///     - Increase the version in GetVersion
31
///
32
/// Changelog:
33
/// 1: Unused first CommandType (End) removed, GameCommand version added
34
static const uint8_t currentReplayDataVersion = 1;
35
// clang-format on
36

37
/// Format version of replay files
38
uint8_t Replay::GetLatestMinorVersion() const
9✔
39
{
40
    // 8.1: Portraits support
41
    // 8.2: Set correct initial distributions if replay starts without savegame for leather addon (see GameClient.cpp
42
    //      StartReplay function for detailed description)
43
    return 2;
9✔
44
}
45

46
uint8_t Replay::GetLatestMajorVersion() const
9✔
47
{
48
    // Search for "TODO(Replay)" when increasing this (breaking Replay compatibility)
49
    // and handle/remove the relevant code
50
    return 8;
9✔
51
}
52

53
//////////////////////////////////////////////////////////////////////////
54

55
Replay::Replay() = default;
9✔
56
Replay::~Replay() = default;
9✔
57

58
void Replay::Close()
5✔
59
{
60
    file_.Close();
5✔
61
    uncompressedDataFile_.reset();
5✔
62
    isRecording_ = false;
5✔
63
    filepath_.clear();
5✔
64
    ClearPlayers();
5✔
65
}
5✔
66

67
bool Replay::StopRecording()
3✔
68
{
69
    if(!isRecording_)
3✔
70
        return true;
×
71
    const auto replayDataSize = file_.Tell();
3✔
72
    isRecording_ = false;
3✔
73
    file_.Close();
3✔
74

75
    // Remove empty replay recordings. They are mostly produced when a game is aborted
76
    // during startup, for example after an early script error.
77
    if(lastGF_ == 0)
3✔
78
    {
79
        boost::system::error_code ec;
1✔
80
        boost::filesystem::remove(filepath_, ec);
1✔
81
        return !ec;
1✔
82
    }
83

84
    BinaryFile file;
4✔
85
    if(!file.Open(filepath_, OpenFileMode::Read))
2✔
86
        return false;
×
87
    try
88
    {
89
        TmpFile tmpReplayFile(".rpl");
6✔
90
        file.Seek(0, SEEK_SET);
2✔
91
        tmpReplayFile.close();
2✔
92
        BinaryFile compressedReplay;
4✔
93
        compressedReplay.Open(tmpReplayFile.filePath, OpenFileMode::Write);
2✔
94

95
        // Copy header data uncompressed
96
        std::vector<char> data(lastGfFilePos_);
4✔
97
        file.ReadRawData(data.data(), data.size());
2✔
98
        compressedReplay.WriteRawData(data.data(), data.size());
2✔
99
        const auto lastGF = file.ReadUnsignedInt();
2✔
100
        RTTR_Assert(lastGF == lastGF_);
2✔
101
        compressedReplay.WriteUnsignedInt(lastGF);
2✔
102
        file.ReadUnsignedChar(); // Ignore compressed flag (always zero for the temporary file)
2✔
103

104
        // Read and compress remaining data
105
        const auto uncompressedSize = replayDataSize - file.Tell(); // Always positive as there is always some game data
2✔
106
        data.resize(uncompressedSize);
2✔
107
        file.ReadRawData(data.data(), data.size());
2✔
108
        data = CompressedData::compress(data);
2✔
109
        // If the compressed data turns out to be larger than the uncompressed one, bail out
110
        // This can happen for very short replays
111
        if(data.size() >= uncompressedSize)
2✔
112
            return true;
×
113
        compressedReplay.WriteUnsignedChar(1); // Compressed flag
2✔
114
        compressedReplay.WriteUnsignedInt(uncompressedSize);
2✔
115
        compressedReplay.WriteUnsignedInt(data.size());
2✔
116
        compressedReplay.WriteRawData(data.data(), data.size());
2✔
117

118
        // All done. Replace uncompressed replay
119
        compressedReplay.Close();
2✔
120
        file.Close();
2✔
121
        boost::filesystem::rename(tmpReplayFile.filePath, filepath_);
2✔
122
        return true;
2✔
123
    } catch(const std::exception& e)
×
124
    {
125
        lastErrorMsg = e.what();
×
126
        return false;
×
127
    }
128
}
129

130
bool Replay::StartRecording(const boost::filesystem::path& filepath, const MapInfo& mapInfo, const unsigned randomSeed)
5✔
131
{
132
    // Deny overwrite, also avoids double-opening by different processes
133
    if(boost::filesystem::exists(filepath))
5✔
134
        return false;
1✔
135
    if(!file_.Open(filepath, OpenFileMode::Write))
4✔
136
        return false;
×
137
    filepath_ = filepath;
4✔
138

139
    isRecording_ = true;
4✔
140
    /// End-GF (will be updated during the game)
141
    lastGF_ = 0;
4✔
142
    mapType_ = mapInfo.type;
4✔
143
    randomSeed_ = randomSeed;
4✔
144

145
    WriteAllHeaderData(file_, mapInfo.title);
4✔
146
    file_.WriteUnsignedChar(rttr::enum_cast(mapType_));
4✔
147
    // TODO(Replay): Move before mapType
148
    file_.WriteUnsignedChar(subVersion_ = currentReplayDataVersion);
4✔
149
    RTTR_Assert(gc::Deserializer::getCurrentVersion() <= std::numeric_limits<decltype(gcVersion_)>::max());
4✔
150
    file_.WriteUnsignedChar(gcVersion_ = gc::Deserializer::getCurrentVersion());
4✔
151

152
    // For (savegame) format validation
153
    if(mapType_ == MapType::Savegame)
4✔
154
        mapInfo.savegame->WriteFileHeader(file_);
1✔
155

156
    // store position to update it later
157
    lastGfFilePos_ = file_.Tell();
4✔
158
    file_.WriteUnsignedInt(lastGF_);
4✔
159
    file_.WriteUnsignedChar(0); // Compressed flag
4✔
160

161
    WritePlayerData(file_);
4✔
162
    WriteGGS(file_);
4✔
163

164
    // Game data
165
    file_.WriteUnsignedInt(randomSeed_);
4✔
166
    file_.WriteLongString(mapInfo.filepath.string());
4✔
167

168
    switch(mapType_)
4✔
169
    {
170
        default: return false;
×
171
        case MapType::OldMap:
3✔
172
            RTTR_Assert(!mapInfo.savegame);
3✔
173
            file_.WriteUnsignedInt(mapInfo.mapData.uncompressedLength);
3✔
174
            file_.WriteUnsignedInt(mapInfo.mapData.data.size());
3✔
175
            file_.WriteRawData(mapInfo.mapData.data.data(), mapInfo.mapData.data.size());
3✔
176
            file_.WriteUnsignedInt(mapInfo.luaData.uncompressedLength);
3✔
177
            file_.WriteUnsignedInt(mapInfo.luaData.data.size());
3✔
178
            if(!mapInfo.luaData.data.empty())
3✔
179
                file_.WriteRawData(mapInfo.luaData.data.data(), mapInfo.luaData.data.size());
3✔
180
            break;
3✔
181
        case MapType::Savegame: mapInfo.savegame->Save(file_, GetMapName()); break;
1✔
182
    }
183
    // Flush now to not loose any information
184
    file_.Flush();
4✔
185

186
    return true;
4✔
187
}
188

189
const boost::filesystem::path& Replay::GetPath() const
×
190
{
191
    RTTR_Assert(file_.IsOpen());
×
192
    return filepath_;
×
193
}
194

195
bool Replay::LoadHeader(const boost::filesystem::path& filepath)
5✔
196
{
197
    Close();
5✔
198
    if(!file_.Open(filepath, OpenFileMode::Read))
5✔
199
    {
200
        lastErrorMsg = _("File could not be opened.");
×
201
        return false;
×
202
    }
203
    filepath_ = filepath;
5✔
204

205
    try
206
    {
207
        // Check file header
208
        if(!ReadAllHeaderData(file_))
5✔
209
            return false;
×
210

211
        mapType_ = static_cast<MapType>(file_.ReadUnsignedChar());
5✔
212
        // TODO(Replay): Move before mapType to have it as early as possible.
213
        // Previously mapType was an unsigned short, i.e. in little endian the 2nd byte was always unused/zero
214
        subVersion_ = file_.ReadUnsignedChar();
5✔
215
        if(subVersion_ > currentReplayDataVersion)
5✔
216
        {
217
            lastErrorMsg =
218
              helpers::format(_("Cannot play replay created with a more recent version (Current: %1%, Replay: %2%)"),
×
219
                              currentReplayDataVersion, subVersion_);
×
220
            return false;
×
221
        }
222

223
        if(subVersion_ >= 1)
5✔
224
            gcVersion_ = file_.ReadUnsignedChar();
5✔
225
        else
226
            gcVersion_ = 0;
×
227
        if(gcVersion_ > gc::Deserializer::getCurrentVersion())
5✔
228
        {
229
            lastErrorMsg =
230
              helpers::format(_("Cannot play replay created with a more recent GC version (Current: %1%, Replay: %2%)"),
×
231
                              gc::Deserializer::getCurrentVersion(), gcVersion_);
×
232
            return false;
×
233
        }
234

235
        if(mapType_ == MapType::Savegame)
5✔
236
        {
237
            // Validate savegame
238
            Savegame save;
2✔
239
            if(!save.ReadFileHeader(file_))
2✔
240
            {
241
                lastErrorMsg = std::string(_("Savegame error: ")) + save.GetLastErrorMsg();
×
242
                return false;
×
243
            }
244
        }
245

246
        lastGF_ = file_.ReadUnsignedInt();
5✔
247
    } catch(const std::runtime_error& e)
×
248
    {
249
        lastErrorMsg = e.what();
×
250
        return false;
×
251
    }
252

253
    return true;
5✔
254
}
255

256
bool Replay::LoadGameData(MapInfo& mapInfo)
3✔
257
{
258
    try
259
    {
260
        const bool isCompressed = file_.ReadUnsignedChar() != 0;
3✔
261
        if(isCompressed)
3✔
262
        {
263
            const auto uncompressedSize = file_.ReadUnsignedInt();
2✔
264
            const auto compressedSize = file_.ReadUnsignedInt();
2✔
265
            CompressedData compressedData(uncompressedSize);
4✔
266
            compressedData.data.resize(compressedSize);
2✔
267
            file_.ReadRawData(compressedData.data.data(), compressedSize);
2✔
268
            uncompressedDataFile_ = std::make_unique<TmpFile>(".rpl");
2✔
269
            uncompressedDataFile_->close();
2✔
270
            compressedData.DecompressToFile(uncompressedDataFile_->filePath);
2✔
271
            file_.Close();
2✔
272
            file_.Open(uncompressedDataFile_->filePath, OpenFileMode::Read);
2✔
273
        }
274

275
        ReadPlayerData(file_);
3✔
276
        ReadGGS(file_);
3✔
277
        randomSeed_ = file_.ReadUnsignedInt();
3✔
278

279
        mapInfo.Clear();
3✔
280
        mapInfo.type = mapType_;
3✔
281
        mapInfo.title = GetMapName();
3✔
282
        mapInfo.filepath = file_.ReadLongString();
3✔
283
        switch(mapType_)
3✔
284
        {
285
            default: return false;
×
286
            case MapType::OldMap:
2✔
287
                mapInfo.mapData.uncompressedLength = file_.ReadUnsignedInt();
2✔
288
                mapInfo.mapData.data.resize(file_.ReadUnsignedInt());
2✔
289
                file_.ReadRawData(mapInfo.mapData.data.data(), mapInfo.mapData.data.size());
2✔
290
                mapInfo.luaData.uncompressedLength = file_.ReadUnsignedInt();
2✔
291
                mapInfo.luaData.data.resize(file_.ReadUnsignedInt());
2✔
292
                if(!mapInfo.luaData.data.empty())
2✔
293
                    file_.ReadRawData(mapInfo.luaData.data.data(), mapInfo.luaData.data.size());
2✔
294
                break;
2✔
295
            case MapType::Savegame:
1✔
296
                mapInfo.savegame = std::make_unique<Savegame>();
1✔
297
                if(!mapInfo.savegame->Load(file_, SaveGameDataToLoad::All))
1✔
298
                {
299
                    lastErrorMsg = std::string(_("Savegame error: ")) + mapInfo.savegame->GetLastErrorMsg();
×
300
                    return false;
×
301
                }
302
                break;
1✔
303
        }
304
    } catch(const std::runtime_error& e)
×
305
    {
306
        lastErrorMsg = e.what();
×
307
        return false;
×
308
    }
309
    return true;
3✔
310
}
311

312
void Replay::AddChatCommand(unsigned gf, uint8_t player, ChatDestination dest, const std::string& str)
12✔
313
{
314
    RTTR_Assert(IsRecording());
12✔
315
    if(!file_.IsOpen())
12✔
316
        return;
×
317

318
    file_.WriteUnsignedInt(gf);
12✔
319

320
    file_.WriteUnsignedChar(rttr::enum_cast(CommandType::Chat));
12✔
321
    file_.WriteUnsignedChar(player);
12✔
322
    file_.WriteUnsignedChar(rttr::enum_cast(dest));
12✔
323
    file_.WriteLongString(str);
12✔
324

325
    // Prevent loss in case of crash
326
    file_.Flush();
12✔
327
}
328

329
void Replay::AddGameCommand(unsigned gf, uint8_t player, const PlayerGameCommands& cmds)
3✔
330
{
331
    RTTR_Assert(IsRecording());
3✔
332
    if(!file_.IsOpen())
3✔
333
        return;
×
334

335
    file_.WriteUnsignedInt(gf);
3✔
336

337
    file_.WriteUnsignedChar(rttr::enum_cast(CommandType::Game));
3✔
338
    Serializer ser;
6✔
339
    ser.PushUnsignedChar(player);
3✔
340
    cmds.Serialize(ser);
3✔
341
    ser.WriteToFile(file_);
3✔
342

343
    // Prevent loss in case of crash
344
    file_.Flush();
3✔
345
}
346

347
std::optional<unsigned> Replay::ReadGF()
18✔
348
{
349
    RTTR_Assert(IsReplaying());
18✔
350
    try
351
    {
352
        return file_.ReadUnsignedInt();
18✔
353
    } catch(const std::runtime_error&)
3✔
354
    {
355
        if(file_.IsEndOfFile())
3✔
356
            return std::nullopt;
3✔
357
        throw;
×
358
    }
359
}
360

361
boost_variant2<Replay::ChatCommand, Replay::GameCommand> Replay::ReadCommand()
15✔
362
{
363
    RTTR_Assert(IsReplaying());
15✔
364
    const auto type = static_cast<CommandType>(file_.ReadUnsignedChar() - (subVersion_ == 0 ? 1 : 0));
15✔
365
    switch(type)
15✔
366
    {
367
        case CommandType::Chat: return ChatCommand(file_);
12✔
368
        case CommandType::Game: return GameCommand(file_, gcVersion_);
3✔
369
        default: throw std::invalid_argument("Invalid command type: " + std::to_string(rttr::enum_cast(type)));
×
370
    }
371
}
372

373
void Replay::UpdateLastGF(unsigned last_gf)
6✔
374
{
375
    RTTR_Assert(IsRecording());
6✔
376
    if(!file_.IsOpen())
6✔
377
        return;
×
378

379
    file_.Seek(lastGfFilePos_, SEEK_SET);
6✔
380
    file_.WriteUnsignedInt(last_gf);
6✔
381
    file_.Seek(0, SEEK_END);
6✔
382
    lastGF_ = last_gf;
6✔
383
}
384

385
Replay::ChatCommand::ChatCommand(BinaryFile& file)
12✔
386
    : player(file.ReadUnsignedChar()), dest(static_cast<ChatDestination>(file.ReadUnsignedChar())),
12✔
387
      msg(file.ReadLongString())
12✔
388
{}
12✔
389

390
Replay::GameCommand::GameCommand(BinaryFile& file, const unsigned version)
3✔
391
{
392
    gc::Deserializer ser{version};
6✔
393
    ser.ReadFromFile(file);
3✔
394
    player = ser.PopUnsignedChar();
3✔
395
    cmds.Deserialize(ser);
3✔
396
}
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