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;
}
|