Merge pull request #4179 from janisozaur/network-checksum-sprites

Send a checksum of all the sprites occasionally in multiplayer for better desync detection.
This commit is contained in:
Ted John 2016-07-27 14:31:02 +01:00 committed by GitHub
commit 17c5178dec
6 changed files with 107 additions and 2 deletions

View File

@ -586,7 +586,10 @@ bool Network::CheckSRAND(uint32 tick, uint32 srand0)
if (tick == server_srand0_tick) {
server_srand0_tick = 0;
if (srand0 != server_srand0) {
// Check that the server and client sprite hashes match
const bool sprites_mismatch = server_sprite_hash[0] != '\0' && strcmp(sprite_checksum(), server_sprite_hash);
// Check PRNG values and sprite hashes, if exist
if ((srand0 != server_srand0) || sprites_mismatch) {
return false;
}
}
@ -1081,6 +1084,22 @@ void Network::Server_Send_TICK()
last_tick_sent_time = SDL_GetTicks();
std::unique_ptr<NetworkPacket> packet = std::move(NetworkPacket::Allocate());
*packet << (uint32)NETWORK_COMMAND_TICK << (uint32)gCurrentTicks << (uint32)gScenarioSrand0;
uint32 flags = 0;
// Simple counter which limits how often a sprite checksum gets sent.
// This can get somewhat expensive, so we don't want to push it every tick in release,
// but debug version can check more often.
static int checksum_counter = 0;
checksum_counter++;
if (checksum_counter >= 100) {
checksum_counter = 0;
flags |= NETWORK_TICK_FLAG_CHECKSUMS;
}
// Send flags always, so we can understand packet structure on the other end,
// and allow for some expansion.
*packet << flags;
if (flags & NETWORK_TICK_FLAG_CHECKSUMS) {
packet->WriteString(sprite_checksum());
}
SendPacketToClients(*packet);
}
@ -1739,10 +1758,23 @@ void Network::Server_Handle_GAMECMD(NetworkConnection& connection, NetworkPacket
void Network::Client_Handle_TICK(NetworkConnection& connection, NetworkPacket& packet)
{
uint32 srand0;
packet >> server_tick >> srand0;
uint32 flags;
// Note: older server version may not advertise flags at all.
// NetworkPacket will return 0, if trying to read past end of buffer,
// so flags == 0 is expected in such cases.
packet >> server_tick >> srand0 >> flags;
if (server_srand0_tick == 0) {
server_srand0 = srand0;
server_srand0_tick = server_tick;
server_sprite_hash[0] = '\0';
if (flags & NETWORK_TICK_FLAG_CHECKSUMS)
{
const char* text = packet.ReadString();
if (text != nullptr)
{
safe_strcpy(server_sprite_hash, text, sizeof(server_sprite_hash));
}
}
}
}

View File

@ -43,6 +43,7 @@ extern "C" {
#include "../game.h"
#include "../platform/platform.h"
#include "../localisation/string_ids.h"
#include <openssl/evp.h>
#ifdef __cplusplus
}
#endif // __cplusplus
@ -77,6 +78,10 @@ extern "C" {
#include "NetworkUser.h"
#include "TcpSocket.h"
enum {
NETWORK_TICK_FLAG_CHECKSUMS = 1 << 0,
};
class Network
{
public:
@ -191,6 +196,7 @@ private:
uint32 server_tick = 0;
uint32 server_srand0 = 0;
uint32 server_srand0_tick = 0;
char server_sprite_hash[EVP_MAX_MD_SIZE + 1];
uint8 player_id = 0;
std::list<std::unique_ptr<NetworkConnection>> client_connection_list;
std::multiset<GameCommand> game_command_queue;

View File

@ -63,6 +63,11 @@ bool gOpenRCT2Headless = false;
bool gOpenRCT2ShowChangelog;
bool gOpenRCT2SilentBreakpad;
#ifndef DISABLE_NETWORK
// OpenSSL's message digest context used for calculating sprite checksums
EVP_MD_CTX *gHashCTX = NULL;
#endif // DISABLE_NETWORK
/** If set, will end the OpenRCT2 game loop. Intentially private to this module so that the flag can not be set back to 0. */
int _finished;
@ -176,6 +181,11 @@ bool openrct2_initialise()
{
utf8 userPath[MAX_PATH];
#ifndef DISABLE_NETWORK
gHashCTX = EVP_MD_CTX_create();
assert(gHashCTX != NULL);
#endif // DISABLE_NETWORK
platform_resolve_openrct_data_path();
platform_resolve_user_data_path();
platform_get_user_directory(userPath, NULL);
@ -336,6 +346,9 @@ void openrct2_dispose()
language_close_all();
rct2_dispose();
config_release();
#ifndef DISABLE_NETWORK
EVP_MD_CTX_destroy(gHashCTX);
#endif // DISABLE_NETWORK
platform_free();
}

View File

@ -20,6 +20,10 @@
#include "common.h"
#include "platform/platform.h"
#ifndef DISABLE_NETWORK
#include <openssl/evp.h>
#endif // DISABLE_NETWORK
enum {
STARTUP_ACTION_INTRO,
STARTUP_ACTION_TITLE,
@ -39,6 +43,10 @@ extern utf8 gCustomPassword[MAX_PATH];
extern bool gOpenRCT2Headless;
extern bool gOpenRCT2ShowChangelog;
#ifndef DISABLE_NETWORK
extern EVP_MD_CTX *gHashCTX;
#endif // DISABLE_NETWORK
#ifndef DISABLE_NETWORK
extern int gNetworkStart;
extern char gNetworkStartHost[128];

View File

@ -168,6 +168,50 @@ void game_command_reset_sprites(int* eax, int* ebx, int* ecx, int* edx, int* esi
*ebx = 0;
}
#ifndef DISABLE_NETWORK
static unsigned char _spriteChecksum[EVP_MAX_MD_SIZE + 1];
const char * sprite_checksum()
{
if (EVP_DigestInit_ex(gHashCTX, EVP_sha1(), NULL) <= 0)
{
openrct2_assert(false, "Failed to initialise SHA1 engine");
}
for (size_t i = 0; i < MAX_SPRITES; i++)
{
rct_sprite *sprite = get_sprite(i);
if (sprite->unknown.sprite_identifier != SPRITE_IDENTIFIER_NULL)
{
if (EVP_DigestUpdate(gHashCTX, sprite, sizeof(rct_sprite)) <= 0)
{
openrct2_assert(false, "Failed to update digest");
}
}
}
unsigned char localhash[EVP_MAX_MD_SIZE + 1];
unsigned int size = sizeof(localhash);
EVP_DigestFinal(gHashCTX, localhash, &size);
assert(size <= sizeof(localhash));
localhash[sizeof(localhash) - 1] = '\0';
char *x = (char *)_spriteChecksum;
for (unsigned int i = 0; i < size; i++)
{
sprintf(x, "%02x", localhash[i]);
x += 2;
}
*x = '\0';
return (char *)_spriteChecksum;
}
#else
const char * sprite_checksum()
{
return NULL;
}
#endif // DISABLE_NETWORK
/**
* Clears all the unused sprite memory to zero. Probably so that it can be compressed better when saving.
* rct2: 0x0069EBA4

View File

@ -441,4 +441,6 @@ void crashed_vehicle_particle_update(rct_crashed_vehicle_particle *particle);
void crash_splash_create(int x, int y, int z);
void crash_splash_update(rct_crash_splash *splash);
const char *sprite_checksum();
#endif