File: netreplay.cpp

package info (click to toggle)
warzone2100 4.6.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 660,320 kB
  • sloc: cpp: 676,209; ansic: 391,201; javascript: 78,238; python: 16,632; php: 4,294; sh: 4,094; makefile: 2,629; lisp: 1,492; cs: 489; xml: 404; perl: 224; ruby: 156; java: 89
file content (484 lines) | stat: -rw-r--r-- 15,954 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
/*
	This file is part of Warzone 2100.
	Copyright (C) 1999-2004  Eidos Interactive
	Copyright (C) 2005-2011  Warzone 2100 Project

	Warzone 2100 is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 2 of the License, or
	(at your option) any later version.

	Warzone 2100 is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with Warzone 2100; if not, write to the Free Software
	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include <nlohmann/json.hpp> // Must come before WZ includes

#include "lib/framework/frame.h"
#include "lib/framework/physfs_ext.h"
#include "lib/framework/wzapp.h"
#include "lib/gamelib/gtime.h"

#if defined(__clang__)
#  pragma clang diagnostic push
#  pragma clang diagnostic ignored "-Wcast-align"
#elif defined(__GNUC__)
#  pragma GCC diagnostic push
#  pragma GCC diagnostic ignored "-Wcast-align"
#endif

#include <3rdparty/readerwriterqueue/readerwriterqueue.h>

#if defined(__clang__)
#  pragma clang diagnostic pop
#elif defined(__GNUC__)
#  pragma GCC diagnostic pop
#endif

#include <ctime>
#include <memory>

#include "netreplay.h"
#include "netplay.h"

static PHYSFS_file *replaySaveHandle = nullptr;
static PHYSFS_file *replayLoadHandle = nullptr;

static const uint32_t magicReplayNumber = 0x575A7270;  // "WZrp"
static const uint32_t currentReplayFormatVer = 3;
static const uint32_t minReplayFormatVerSupported = 3;
static const size_t DefaultReplayBufferSize = 32768;
static const size_t MaxReplayBufferSize = 2 * 1024 * 1024;

typedef std::vector<uint8_t> SerializedNetMessagesBuffer;
static moodycamel::BlockingReaderWriterQueue<SerializedNetMessagesBuffer> serializedBufferWriteQueue(256);
static nlohmann::json queuedSaveSettings;
static SerializedNetMessagesBuffer latestWriteBuffer;
static size_t minBufferSizeToQueue = DefaultReplayBufferSize;
static WZ_THREAD *saveThread = nullptr;

// This function is run in its own thread! Do not call any non-threadsafe functions!
static int replaySaveThreadFunc(void *data)
{
	PHYSFS_file *pSaveHandle = (PHYSFS_file *)data;
	if (pSaveHandle == nullptr)
	{
		return 1;
	}
	SerializedNetMessagesBuffer item;
	while (true)
	{
		serializedBufferWriteQueue.wait_dequeue(item);
		if (item.empty())
		{
			// end chunk - we're done
			break;
		}
		WZ_PHYSFS_writeBytes(pSaveHandle, item.data(), item.size());
	}
	return 0;
}

static bool NETreplaySaveWritePreamble(const nlohmann::json& settings, ReplayOptionsHandler const &optionsHandler)
{
	if (!replaySaveHandle)
	{
		return false;
	}

	auto data = settings.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace);
	PHYSFS_writeUBE32(replaySaveHandle, data.size());
	WZ_PHYSFS_writeBytes(replaySaveHandle, data.data(), data.size());

	// Save extra map data (if present)
	ReplayOptionsHandler::EmbeddedMapData embeddedMapData;
	if (!optionsHandler.saveMap(embeddedMapData))
	{
		// Failed to save map data - just empty it out for now
		embeddedMapData.mapBinaryData.clear();
	}
	PHYSFS_writeUBE32(replaySaveHandle, embeddedMapData.dataVersion);
#if SIZE_MAX > UINT32_MAX
	ASSERT_OR_RETURN(false, embeddedMapData.mapBinaryData.size() <= static_cast<size_t>(std::numeric_limits<uint32_t>::max()), "Embedded map data is way too big");
#endif
	PHYSFS_writeUBE32(replaySaveHandle, static_cast<uint32_t>(embeddedMapData.mapBinaryData.size()));
	if (!embeddedMapData.mapBinaryData.empty())
	{
		WZ_PHYSFS_writeBytes(replaySaveHandle, embeddedMapData.mapBinaryData.data(), static_cast<uint32_t>(embeddedMapData.mapBinaryData.size()));
	}

	return true;
}

std::string NETreplaySaveStart(std::string const& subdir, ReplayOptionsHandler const &optionsHandler, int maxReplaysSaved, bool appendPlayerToFilename)
{
	if (NETisReplay())
	{
		// Have already loaded and will be running a replay - don't bother saving another
		debug(LOG_WZ, "Replay loaded - skip recording of new replay");
		return "";
	}

	ASSERT_OR_RETURN("", !subdir.empty(), "Must provide a valid subdir");

	if (maxReplaysSaved > 0)
	{
		// clean up old replay files
		std::string replayFullDir = "replay/" + subdir;
		WZ_PHYSFS_cleanupOldFilesInFolder(replayFullDir.c_str(), ".wzrp", maxReplaysSaved - 1, [](const char *fileName){
			if (PHYSFS_delete(fileName) == 0)
			{
				debug(LOG_ERROR, "Failed to delete old replay file: %s", fileName);
				return false;
			}
			return true;
		});
	}

	time_t aclock;
	time(&aclock);                     // Get time in seconds
	tm *newtime = localtime(&aclock);  // Convert time to struct

	std::string filename;
	if (appendPlayerToFilename)
	{
		filename = astringf("replay/%s/%04d%02d%02d_%02d%02d%02d_%s_p%u.wzrp", subdir.c_str(), newtime->tm_year + 1900, newtime->tm_mon + 1, newtime->tm_mday, newtime->tm_hour, newtime->tm_min, newtime->tm_sec, subdir.c_str(), selectedPlayer);
	}
	else
	{
		filename = astringf("replay/%s/%04d%02d%02d_%02d%02d%02d_%s.wzrp", subdir.c_str(), newtime->tm_year + 1900, newtime->tm_mon + 1, newtime->tm_mday, newtime->tm_hour, newtime->tm_min, newtime->tm_sec, subdir.c_str());
	}
	replaySaveHandle = PHYSFS_openWrite(filename.c_str());  // open the file
	if (replaySaveHandle == nullptr)
	{
		debug(LOG_ERROR, "Could not create replay file %s: %s", filename.c_str(), WZ_PHYSFS_getLastError());
		return "";
	}

	WZ_PHYSFS_SETBUFFER(replaySaveHandle, 1024 * 32)//;

	PHYSFS_writeSBE32(replaySaveHandle, magicReplayNumber);

	// Save map name or map data and game settings and list of players in game and stuff.
	nlohmann::json settings = nlohmann::json::object();

	// Save "replay file format version"
	settings["replayFormatVer"] = currentReplayFormatVer;

	// Save Netcode version
	settings["major"] = NETGetMajorVersion();
	settings["minor"] = NETGetMinorVersion();

	// Save desired info from optionsHandler
	nlohmann::json gameOptions = nlohmann::json::object();
	optionsHandler.saveOptions(gameOptions);
	settings["gameOptions"] = gameOptions;

	// determine best buffer size
	size_t desiredBufferSize = optionsHandler.desiredBufferSize();
	if (desiredBufferSize == 0)
	{
		// use default
		minBufferSizeToQueue = DefaultReplayBufferSize;
	}
	else if (desiredBufferSize >= MaxReplayBufferSize)
	{
		minBufferSizeToQueue = MaxReplayBufferSize;
	}
	else
	{
		minBufferSizeToQueue = desiredBufferSize;
	}

	debug(LOG_INFO, "Started writing replay file \"%s\".", filename.c_str());

	// Create a background thread and hand off all responsibility for writing to the file handle to it
	ASSERT(saveThread == nullptr, "Failed to release prior thread");
	latestWriteBuffer.reserve(minBufferSizeToQueue);
	if (desiredBufferSize != std::numeric_limits<size_t>::max())
	{
		// Write the preamble immediately (settings, etc)
		NETreplaySaveWritePreamble(settings, optionsHandler);

		// use a background thread
		saveThread = wzThreadCreate(replaySaveThreadFunc, replaySaveHandle, "replaySaveThread");
		wzThreadStart(saveThread);
	}
	else
	{
		// Do not immediately write settings out - instead, queue them for later writing
		queuedSaveSettings = std::move(settings);

		// don't use a background thread
		saveThread = nullptr;
	}

	return filename;
}

bool NETreplaySaveStop(ReplayOptionsHandler const &optionsHandler)
{
	if (!replaySaveHandle)
	{
		return false;
	}

	// v2: Append the "REPLAY_ENDED" message (from hostPlayer)
	auto replayEndedMessage = NetMessageBuilder(REPLAY_ENDED, 0).build();
	latestWriteBuffer.push_back(NetPlay.hostPlayer);
	replayEndedMessage.rawDataAppendToVector(latestWriteBuffer);

	// Queue the last chunk for writing
	if (!latestWriteBuffer.empty())
	{
		serializedBufferWriteQueue.enqueue(std::move(latestWriteBuffer));
	}

	// Then push one empty chunk to signify "we're done!"
	latestWriteBuffer.resize(0);
	serializedBufferWriteQueue.enqueue(std::move(latestWriteBuffer));

	// Wait for writing thread to finish
	if (saveThread)
	{
		wzThreadJoin(saveThread);
		saveThread = nullptr;
	}
	else
	{
		// update the queued settings (ex. might have revealed player identities in a blind game)
		optionsHandler.optionsUpdatePlayerInfo(queuedSaveSettings.at("gameOptions"));

		// write the preamble
		NETreplaySaveWritePreamble(queuedSaveSettings, optionsHandler);

		// do the writing now on the main thread
		replaySaveThreadFunc(replaySaveHandle);
	}

	// v2: Write the "end of game info" chunk
	// (this is JSON that is preceded *and* followed by its size - so it should be possible to seek to the end of the file, read the last uint32_t, and then back up and grab the JSON without processing the whole file)
	nlohmann::json endOfGameInfo = nlohmann::json::object();
	endOfGameInfo["gameTimeElapsed"] = gameTime;
	// FUTURE TODO: Could save things like the game results / winners + losers

	auto data = endOfGameInfo.dump();
	PHYSFS_writeUBE32(replaySaveHandle, data.size());
	WZ_PHYSFS_writeBytes(replaySaveHandle, data.data(), data.size());
	PHYSFS_writeUBE32(replaySaveHandle, data.size()); // should also end with the json size for easy reading from end of file

	if (!PHYSFS_close(replaySaveHandle))
	{
		debug(LOG_ERROR, "Could not close replay file: %s", WZ_PHYSFS_getLastError());
		return false;
	}
	replaySaveHandle = nullptr;

	return true;
}

void NETreplaySaveNetMessage(NetMessage const *message, uint8_t player)
{
	if (!replaySaveHandle)
	{
		return;
	}

	if (message->type() > GAME_MIN_TYPE && message->type() < GAME_MAX_TYPE)
	{
		latestWriteBuffer.push_back(player);
		message->rawDataAppendToVector(latestWriteBuffer);

		if (latestWriteBuffer.size() >= minBufferSizeToQueue)
		{
			serializedBufferWriteQueue.enqueue(std::move(latestWriteBuffer));
			latestWriteBuffer = std::vector<uint8_t>();
			latestWriteBuffer.reserve(minBufferSizeToQueue);
		}
	}
}

bool NETreplayLoadStart(std::string const &filename, ReplayOptionsHandler& optionsHandler, uint32_t& output_replayFormatVer)
{
	auto onFail = [&](char const *reason) {
		debug(LOG_ERROR, "Could not load replay file %s: %s", filename.c_str(), reason);
		if (replayLoadHandle != nullptr)
		{
			PHYSFS_close(replayLoadHandle);
			replayLoadHandle = nullptr;
		}
		return false;
	};

	replayLoadHandle = PHYSFS_openRead(filename.c_str());
	if (replayLoadHandle == nullptr)
	{
		return onFail(WZ_PHYSFS_getLastError());
	}

	int32_t replayNumber = 0;
	PHYSFS_readSBE32(replayLoadHandle, &replayNumber);
	if ((uint32_t)replayNumber != magicReplayNumber)
	{
		return onFail("bad header");
	}

	uint32_t dataSize = 0;
	PHYSFS_readUBE32(replayLoadHandle, &dataSize);
	std::string data;
	data.resize(dataSize);
	size_t dataRead = WZ_PHYSFS_readBytes(replayLoadHandle, &data[0], data.size());
	if (dataRead != data.size())
	{
		return onFail("truncated header");
	}

	// Restore map name or map data and game settings and list of players in game and stuff.
	try
	{
		nlohmann::json settings = nlohmann::json::parse(data);

		uint32_t replayFormatVer = settings.at("replayFormatVer").get<uint32_t>();
		output_replayFormatVer = replayFormatVer;
		if (replayFormatVer > currentReplayFormatVer)
		{
			std::string mismatchVersionDescription = _("The replay file format is newer than this version of Warzone 2100 can support.");
			mismatchVersionDescription += "\n\n";
			mismatchVersionDescription += astringf(_("Replay Format Version: %u"), static_cast<unsigned>(replayFormatVer));
			wzDisplayDialog(Dialog_Error, _("Replay File Format Unsupported"), mismatchVersionDescription.c_str());

			std::string failLogStr = "Replay file format is newer than this version of Warzone 2100 can support: " + std::to_string(replayFormatVer);
			return onFail(failLogStr.c_str());
		}

		if (replayFormatVer < minReplayFormatVerSupported)
		{
			std::string mismatchVersionDescription = _("The replay file format is older than this version of Warzone 2100 can support.");
			mismatchVersionDescription += "\n\n";
			mismatchVersionDescription += astringf(_("Replay Format Version: %u"), static_cast<unsigned>(replayFormatVer));
			wzDisplayDialog(Dialog_Error, _("Replay File Format Unsupported"), mismatchVersionDescription.c_str());

			std::string failLogStr = "Replay file format is older than this version of Warzone 2100 can support: " + std::to_string(replayFormatVer);
			return onFail(failLogStr.c_str());
		}

		uint32_t replay_netcodeMajor = settings.at("major").get<uint32_t>();
		uint32_t replay_netcodeMinor = settings.at("minor").get<uint32_t>();
		if (!NETisCorrectVersion(replay_netcodeMajor, replay_netcodeMinor))
		{
			debug(LOG_INFO, "NetCode Version mismatch: (replay file: 0x%" PRIx32 ", 0x%" PRIx32 ") - (current: 0x%" PRIx32 ", 0x%" PRIx32 ")", replay_netcodeMajor, replay_netcodeMinor, NETGetMajorVersion(), NETGetMinorVersion());
			// do not immediately fail out - restoreOptions handles displaying a nicer warning popup
		}

		ReplayOptionsHandler::EmbeddedMapData embeddedMapData;
		if (replayFormatVer >= 2)
		{
			PHYSFS_readUBE32(replayLoadHandle, &embeddedMapData.dataVersion);
			uint32_t binaryDataSize = 0;
			PHYSFS_readUBE32(replayLoadHandle, &binaryDataSize);
			if (binaryDataSize > 0)
			{
				if (binaryDataSize <= optionsHandler.maximumEmbeddedMapBufferSize())
				{
					embeddedMapData.mapBinaryData.resize(binaryDataSize);
					PHYSFS_sint64 binaryDataRead = WZ_PHYSFS_readBytes(replayLoadHandle, embeddedMapData.mapBinaryData.data(), embeddedMapData.mapBinaryData.size());
					if (binaryDataRead != binaryDataSize)
					{
						return onFail("truncated embedded map data");
					}
				}
				else
				{
					// don't even bother trying to load this - it's too big
					// just attempt to skip to where it claims the map data ends
					PHYSFS_sint64 filePos = PHYSFS_tell(replayLoadHandle);
					if (filePos < 0)
					{
						return onFail("error getting current file position");
					}
					PHYSFS_uint64 afterMapDataPos = filePos + binaryDataSize;
					if (PHYSFS_seek(replayLoadHandle, afterMapDataPos) == 0)
					{
						return onFail("failed to seek after map data");
					}
				}
			}
		}

		// Load game options using optionsHandler
		if (!optionsHandler.restoreOptions(settings.at("gameOptions"), std::move(embeddedMapData), replay_netcodeMajor, replay_netcodeMinor))
		{
			return onFail("invalid options");
		}
	}
	catch (const std::exception& e)
	{
		// Failed to parse or find a key in the json
		std::string parseError = std::string("Error parsing info JSON (\"") + e.what() + "\")";
		return onFail(parseError.c_str());
	}

	debug(LOG_INFO, "Started reading replay file \"%s\".", filename.c_str());
	return true;
}

bool NETreplayLoadNetMessage(std::unique_ptr<NetMessage> &message, uint8_t &player)
{
	if (!replayLoadHandle)
	{
		return false;
	}

	WZ_PHYSFS_readBytes(replayLoadHandle, &player, 1);

	uint8_t type;
	WZ_PHYSFS_readBytes(replayLoadHandle, &type, 1);

	uint8_t b[2];
	bool rd = WZ_PHYSFS_readBytes(replayLoadHandle, &b, 2);
	if (!rd)
	{
		return false;
	}
	uint16_t len = 0;
	// Load payload length from uint16_t (network byte order) starting at the second byte of the data buffer.
	wz_ntohs_load_unaligned(len, b);

	std::vector<uint8_t> replayData(len);
	size_t messageRead = WZ_PHYSFS_readBytes(replayLoadHandle, replayData.data(), len);

	if (messageRead != len)
	{
		return false;
	}

	NetMessageBuilder msgBuilder(type, len);
	msgBuilder.append(replayData.data(), len);

	message = std::make_unique<NetMessage>(msgBuilder.build());

	return (message->type() > GAME_MIN_TYPE && message->type() < GAME_MAX_TYPE) || message->type() == REPLAY_ENDED;
}

bool NETreplayLoadStop()
{
	if (!replayLoadHandle)
	{
		return false;
	}

	if (!PHYSFS_close(replayLoadHandle))
	{
		debug(LOG_ERROR, "Could not close replay file: %s", WZ_PHYSFS_getLastError());
		return false;
	}
	replayLoadHandle = nullptr;

	return true;
}