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

Return-To-The-Roots / s25client / 14612318600

23 Apr 2025 07:22AM UTC coverage: 50.27% (-0.03%) from 50.295%
14612318600

Pull #1761

github

web-flow
Merge a96bc721f into 0d97404a1
Pull Request #1761: Consistenly catch by const-ref

4 of 20 new or added lines in 14 files covered. (20.0%)

11 existing lines in 2 files now uncovered.

22350 of 44460 relevant lines covered (50.27%)

35200.95 hits per line

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

83.03
/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
8✔
17
{
18
    return "RTTRRP2";
8✔
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
uint16_t Replay::GetVersion() const
8✔
39
{
40
    // Search for "TODO(Replay)" when increasing this (breaking Replay compatibility)
41
    // and handle/remove the relevant code
42
    return 8;
8✔
43
}
44

45
//////////////////////////////////////////////////////////////////////////
46

47
Replay::Replay() = default;
8✔
48
Replay::~Replay() = default;
8✔
49

50
void Replay::Close()
5✔
51
{
52
    file_.Close();
5✔
53
    uncompressedDataFile_.reset();
5✔
54
    isRecording_ = false;
5✔
55
    filepath_.clear();
5✔
56
    ClearPlayers();
5✔
57
}
5✔
58

59
bool Replay::StopRecording()
2✔
60
{
61
    if(!isRecording_)
2✔
62
        return true;
×
63
    const auto replayDataSize = file_.Tell();
2✔
64
    isRecording_ = false;
2✔
65
    file_.Close();
2✔
66

67
    BinaryFile file;
4✔
68
    if(!file.Open(filepath_, OpenFileMode::Read))
2✔
69
        return false;
×
70
    try
71
    {
72
        TmpFile tmpReplayFile(".rpl");
6✔
73
        file.Seek(0, SEEK_SET);
2✔
74
        tmpReplayFile.close();
2✔
75
        BinaryFile compressedReplay;
4✔
76
        compressedReplay.Open(tmpReplayFile.filePath, OpenFileMode::Write);
2✔
77

78
        // Copy header data uncompressed
79
        std::vector<char> data(lastGfFilePos_);
4✔
80
        file.ReadRawData(data.data(), data.size());
2✔
81
        compressedReplay.WriteRawData(data.data(), data.size());
2✔
82
        const auto lastGF = file.ReadUnsignedInt();
2✔
83
        RTTR_Assert(lastGF == lastGF_);
2✔
84
        compressedReplay.WriteUnsignedInt(lastGF);
2✔
85
        file.ReadUnsignedChar(); // Ignore compressed flag (always zero for the temporary file)
2✔
86

87
        // Read and compress remaining data
88
        const auto uncompressedSize = replayDataSize - file.Tell(); // Always positive as there is always some game data
2✔
89
        data.resize(uncompressedSize);
2✔
90
        file.ReadRawData(data.data(), data.size());
2✔
91
        data = CompressedData::compress(data);
2✔
92
        // If the compressed data turns out to be larger than the uncompressed one, bail out
93
        // This can happen for very short replays
94
        if(data.size() >= uncompressedSize)
2✔
95
            return true;
×
96
        compressedReplay.WriteUnsignedChar(1); // Compressed flag
2✔
97
        compressedReplay.WriteUnsignedInt(uncompressedSize);
2✔
98
        compressedReplay.WriteUnsignedInt(data.size());
2✔
99
        compressedReplay.WriteRawData(data.data(), data.size());
2✔
100

101
        // All done. Replace uncompressed replay
102
        compressedReplay.Close();
2✔
103
        file.Close();
2✔
104
        boost::filesystem::rename(tmpReplayFile.filePath, filepath_);
2✔
105
        return true;
2✔
106
    } catch(const std::exception& e)
×
107
    {
108
        lastErrorMsg = e.what();
×
109
        return false;
×
110
    }
111
}
112

113
bool Replay::StartRecording(const boost::filesystem::path& filepath, const MapInfo& mapInfo, const unsigned randomSeed)
4✔
114
{
115
    // Deny overwrite, also avoids double-opening by different processes
116
    if(boost::filesystem::exists(filepath))
4✔
117
        return false;
1✔
118
    if(!file_.Open(filepath, OpenFileMode::Write))
3✔
119
        return false;
×
120
    filepath_ = filepath;
3✔
121

122
    isRecording_ = true;
3✔
123
    /// End-GF (will be updated during the game)
124
    lastGF_ = 0;
3✔
125
    mapType_ = mapInfo.type;
3✔
126
    randomSeed_ = randomSeed;
3✔
127

128
    WriteAllHeaderData(file_, mapInfo.title);
3✔
129
    file_.WriteUnsignedChar(rttr::enum_cast(mapType_));
3✔
130
    // TODO(Replay): Move before mapType
131
    file_.WriteUnsignedChar(subVersion_ = currentReplayDataVersion);
3✔
132
    RTTR_Assert(gc::Deserializer::getCurrentVersion() <= std::numeric_limits<decltype(gcVersion_)>::max());
3✔
133
    file_.WriteUnsignedChar(gcVersion_ = gc::Deserializer::getCurrentVersion());
3✔
134

135
    // For (savegame) format validation
136
    if(mapType_ == MapType::Savegame)
3✔
137
        mapInfo.savegame->WriteFileHeader(file_);
1✔
138

139
    // store position to update it later
140
    lastGfFilePos_ = file_.Tell();
3✔
141
    file_.WriteUnsignedInt(lastGF_);
3✔
142
    file_.WriteUnsignedChar(0); // Compressed flag
3✔
143

144
    WritePlayerData(file_);
3✔
145
    WriteGGS(file_);
3✔
146

147
    // Game data
148
    file_.WriteUnsignedInt(randomSeed_);
3✔
149
    file_.WriteLongString(mapInfo.filepath.string());
3✔
150

151
    switch(mapType_)
3✔
152
    {
153
        default: return false;
×
154
        case MapType::OldMap:
2✔
155
            RTTR_Assert(!mapInfo.savegame);
2✔
156
            file_.WriteUnsignedInt(mapInfo.mapData.uncompressedLength);
2✔
157
            file_.WriteUnsignedInt(mapInfo.mapData.data.size());
2✔
158
            file_.WriteRawData(&mapInfo.mapData.data[0], mapInfo.mapData.data.size());
2✔
159
            file_.WriteUnsignedInt(mapInfo.luaData.uncompressedLength);
2✔
160
            file_.WriteUnsignedInt(mapInfo.luaData.data.size());
2✔
161
            if(!mapInfo.luaData.data.empty())
2✔
162
                file_.WriteRawData(&mapInfo.luaData.data[0], mapInfo.luaData.data.size());
2✔
163
            break;
2✔
164
        case MapType::Savegame: mapInfo.savegame->Save(file_, GetMapName()); break;
1✔
165
    }
166
    // Flush now to not loose any information
167
    file_.Flush();
3✔
168

169
    return true;
3✔
170
}
171

172
const boost::filesystem::path& Replay::GetPath() const
×
173
{
174
    RTTR_Assert(file_.IsOpen());
×
175
    return filepath_;
×
176
}
177

178
bool Replay::LoadHeader(const boost::filesystem::path& filepath)
5✔
179
{
180
    Close();
5✔
181
    if(!file_.Open(filepath, OpenFileMode::Read))
5✔
182
    {
183
        lastErrorMsg = _("File could not be opened.");
×
184
        return false;
×
185
    }
186
    filepath_ = filepath;
5✔
187

188
    try
189
    {
190
        // Check file header
191
        if(!ReadAllHeaderData(file_))
5✔
192
            return false;
×
193

194
        mapType_ = static_cast<MapType>(file_.ReadUnsignedChar());
5✔
195
        // TODO(Replay): Move before mapType to have it as early as possible.
196
        // Previously mapType was an unsigned short, i.e. in little endian the 2nd byte was always unused/zero
197
        subVersion_ = file_.ReadUnsignedChar();
5✔
198
        if(subVersion_ > currentReplayDataVersion)
5✔
199
        {
200
            lastErrorMsg =
201
              helpers::format(_("Cannot play replay created with a more recent version (Current: %1%, Replay: %2%)"),
×
202
                              currentReplayDataVersion, subVersion_);
×
203
            return false;
×
204
        }
205

206
        if(subVersion_ >= 1)
5✔
207
            gcVersion_ = file_.ReadUnsignedChar();
5✔
208
        else
209
            gcVersion_ = 0;
×
210
        if(gcVersion_ > gc::Deserializer::getCurrentVersion())
5✔
211
        {
212
            lastErrorMsg =
213
              helpers::format(_("Cannot play replay created with a more recent GC version (Current: %1%, Replay: %2%)"),
×
214
                              gc::Deserializer::getCurrentVersion(), gcVersion_);
×
215
            return false;
×
216
        }
217

218
        if(mapType_ == MapType::Savegame)
5✔
219
        {
220
            // Validate savegame
221
            Savegame save;
2✔
222
            if(!save.ReadFileHeader(file_))
2✔
223
            {
224
                lastErrorMsg = std::string(_("Savegame error: ")) + save.GetLastErrorMsg();
×
225
                return false;
×
226
            }
227
        }
228

229
        lastGF_ = file_.ReadUnsignedInt();
5✔
NEW
230
    } catch(const std::runtime_error& e)
×
231
    {
232
        lastErrorMsg = e.what();
×
233
        return false;
×
234
    }
235

236
    return true;
5✔
237
}
238

239
bool Replay::LoadGameData(MapInfo& mapInfo)
3✔
240
{
241
    try
242
    {
243
        const bool isCompressed = file_.ReadUnsignedChar() != 0;
3✔
244
        if(isCompressed)
3✔
245
        {
246
            const auto uncompressedSize = file_.ReadUnsignedInt();
2✔
247
            const auto compressedSize = file_.ReadUnsignedInt();
2✔
248
            CompressedData compressedData(uncompressedSize);
4✔
249
            compressedData.data.resize(compressedSize);
2✔
250
            file_.ReadRawData(compressedData.data.data(), compressedSize);
2✔
251
            uncompressedDataFile_ = std::make_unique<TmpFile>(".rpl");
2✔
252
            uncompressedDataFile_->close();
2✔
253
            compressedData.DecompressToFile(uncompressedDataFile_->filePath);
2✔
254
            file_.Close();
2✔
255
            file_.Open(uncompressedDataFile_->filePath, OpenFileMode::Read);
2✔
256
        }
257

258
        ReadPlayerData(file_);
3✔
259
        ReadGGS(file_);
3✔
260
        randomSeed_ = file_.ReadUnsignedInt();
3✔
261

262
        mapInfo.Clear();
3✔
263
        mapInfo.type = mapType_;
3✔
264
        mapInfo.title = GetMapName();
3✔
265
        mapInfo.filepath = file_.ReadLongString();
3✔
266
        switch(mapType_)
3✔
267
        {
268
            default: return false;
×
269
            case MapType::OldMap:
2✔
270
                mapInfo.mapData.uncompressedLength = file_.ReadUnsignedInt();
2✔
271
                mapInfo.mapData.data.resize(file_.ReadUnsignedInt());
2✔
272
                file_.ReadRawData(&mapInfo.mapData.data[0], mapInfo.mapData.data.size());
2✔
273
                mapInfo.luaData.uncompressedLength = file_.ReadUnsignedInt();
2✔
274
                mapInfo.luaData.data.resize(file_.ReadUnsignedInt());
2✔
275
                if(!mapInfo.luaData.data.empty())
2✔
276
                    file_.ReadRawData(&mapInfo.luaData.data[0], mapInfo.luaData.data.size());
2✔
277
                break;
2✔
278
            case MapType::Savegame:
1✔
279
                mapInfo.savegame = std::make_unique<Savegame>();
1✔
280
                if(!mapInfo.savegame->Load(file_, SaveGameDataToLoad::All))
1✔
281
                {
282
                    lastErrorMsg = std::string(_("Savegame error: ")) + mapInfo.savegame->GetLastErrorMsg();
×
283
                    return false;
×
284
                }
285
                break;
1✔
286
        }
NEW
287
    } catch(const std::runtime_error& e)
×
288
    {
289
        lastErrorMsg = e.what();
×
290
        return false;
×
291
    }
292
    return true;
3✔
293
}
294

295
void Replay::AddChatCommand(unsigned gf, uint8_t player, ChatDestination dest, const std::string& str)
12✔
296
{
297
    RTTR_Assert(IsRecording());
12✔
298
    if(!file_.IsOpen())
12✔
299
        return;
×
300

301
    file_.WriteUnsignedInt(gf);
12✔
302

303
    file_.WriteUnsignedChar(rttr::enum_cast(CommandType::Chat));
12✔
304
    file_.WriteUnsignedChar(player);
12✔
305
    file_.WriteUnsignedChar(rttr::enum_cast(dest));
12✔
306
    file_.WriteLongString(str);
12✔
307

308
    // Prevent loss in case of crash
309
    file_.Flush();
12✔
310
}
311

312
void Replay::AddGameCommand(unsigned gf, uint8_t player, const PlayerGameCommands& cmds)
3✔
313
{
314
    RTTR_Assert(IsRecording());
3✔
315
    if(!file_.IsOpen())
3✔
316
        return;
×
317

318
    file_.WriteUnsignedInt(gf);
3✔
319

320
    file_.WriteUnsignedChar(rttr::enum_cast(CommandType::Game));
3✔
321
    Serializer ser;
6✔
322
    ser.PushUnsignedChar(player);
3✔
323
    cmds.Serialize(ser);
3✔
324
    ser.WriteToFile(file_);
3✔
325

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

330
std::optional<unsigned> Replay::ReadGF()
18✔
331
{
332
    RTTR_Assert(IsReplaying());
18✔
333
    try
334
    {
335
        return file_.ReadUnsignedInt();
18✔
336
    } catch(const std::runtime_error&)
3✔
337
    {
338
        if(file_.IsEndOfFile())
3✔
339
            return std::nullopt;
3✔
340
        throw;
×
341
    }
342
}
343

344
boost_variant2<Replay::ChatCommand, Replay::GameCommand> Replay::ReadCommand()
15✔
345
{
346
    RTTR_Assert(IsReplaying());
15✔
347
    const auto type = static_cast<CommandType>(file_.ReadUnsignedChar() - (subVersion_ == 0 ? 1 : 0));
15✔
348
    switch(type)
15✔
349
    {
350
        case CommandType::Chat: return ChatCommand(file_);
12✔
351
        case CommandType::Game: return GameCommand(file_, gcVersion_);
3✔
352
        default: throw std::invalid_argument("Invalid command type: " + std::to_string(rttr::enum_cast(type)));
×
353
    }
354
}
355

356
void Replay::UpdateLastGF(unsigned last_gf)
6✔
357
{
358
    RTTR_Assert(IsRecording());
6✔
359
    if(!file_.IsOpen())
6✔
360
        return;
×
361

362
    file_.Seek(lastGfFilePos_, SEEK_SET);
6✔
363
    file_.WriteUnsignedInt(last_gf);
6✔
364
    file_.Seek(0, SEEK_END);
6✔
365
    lastGF_ = last_gf;
6✔
366
}
367

368
Replay::ChatCommand::ChatCommand(BinaryFile& file)
12✔
369
    : player(file.ReadUnsignedChar()), dest(static_cast<ChatDestination>(file.ReadUnsignedChar())),
12✔
370
      msg(file.ReadLongString())
12✔
371
{}
12✔
372

373
Replay::GameCommand::GameCommand(BinaryFile& file, const unsigned version)
3✔
374
{
375
    gc::Deserializer ser{version};
6✔
376
    ser.ReadFromFile(file);
3✔
377
    player = ser.PopUnsignedChar();
3✔
378
    cmds.Deserialize(ser);
3✔
379
}
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