diff --git a/src/network/network.cpp b/src/network/network.cpp index 5e5b420a24..8465807ce9 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -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 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)); + } + } } } diff --git a/src/network/network.h b/src/network/network.h index 7ae78e949e..bba5e39e1d 100644 --- a/src/network/network.h +++ b/src/network/network.h @@ -43,6 +43,7 @@ extern "C" { #include "../game.h" #include "../platform/platform.h" #include "../localisation/string_ids.h" +#include #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> client_connection_list; std::multiset game_command_queue; diff --git a/src/openrct2.c b/src/openrct2.c index d3554430eb..5fe2a3099d 100644 --- a/src/openrct2.c +++ b/src/openrct2.c @@ -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(); } diff --git a/src/openrct2.h b/src/openrct2.h index 7f2bdb5bcf..d546e758f3 100644 --- a/src/openrct2.h +++ b/src/openrct2.h @@ -20,6 +20,10 @@ #include "common.h" #include "platform/platform.h" +#ifndef DISABLE_NETWORK +#include +#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]; diff --git a/src/world/sprite.c b/src/world/sprite.c index 6749cbcf20..6b282945c5 100644 --- a/src/world/sprite.c +++ b/src/world/sprite.c @@ -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 diff --git a/src/world/sprite.h b/src/world/sprite.h index 8efe20e79f..b956ad710c 100644 --- a/src/world/sprite.h +++ b/src/world/sprite.h @@ -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