diff --git CMakeLists.txt CMakeLists.txt index ccb567433..4de2adff6 100644 --- CMakeLists.txt +++ CMakeLists.txt @@ -280,6 +280,11 @@ if (SIMUTRANS_USE_IP4_ONLY) target_compile_definitions(simutrans PRIVATE USE_IP4_ONLY=1) endif () +if (SIMUTRANS_HEAVY_MODE) + target_compile_definitions(simutrans PRIVATE HEAVY_MODE=${SIMUTRANS_HEAVY_MODE}) + target_sources(simutrans PRIVATE io/rdwr/adler32_stream.cc) +endif () + if (SIMUTRANS_STEAM_BUILT) target_compile_definitions(simutrans PRIVATE STEAM_BUILT=1) endif () diff --git Makefile Makefile index a7862d834..8fd3e4a1c 100644 --- Makefile +++ Makefile @@ -222,6 +222,13 @@ ifdef PROFILE endif endif +ifdef HEAVY_MODE + CFLAGS += -DHEAVY_MODE=$(HEAVY_MODE) + ifeq ($(shell expr $(HEAVY_MODE) \>= 1), 1) + SOURCES += io/rdwr/adler32_stream.cc + endif +endif + ifdef MULTI_THREAD ifeq ($(shell expr $(MULTI_THREAD) \>= 1), 1) CFLAGS += -DMULTI_THREAD diff --git cmake/SimutransCompileOptions.cmake cmake/SimutransCompileOptions.cmake index 10b7cd6fb..e4676f8f1 100644 --- cmake/SimutransCompileOptions.cmake +++ cmake/SimutransCompileOptions.cmake @@ -57,6 +57,14 @@ if(NOT SIMUTRANS_MSG_LEVEL) endif () set_property(CACHE SIMUTRANS_MSG_LEVEL PROPERTY STRINGS 0 1 2 3 4) +set(SIMUTRANS_HEAVY_MODE 0 CACHE STRING "\ + Network desync debugging mode. Stores game state hash of client and server for every sync_step for comparison.\ + VERY SLOW! Do not enable for normal builds.\ + 0 = disabled (default)\ + 1 = enabled\ + 2 = enabled; Additionally saves the game every sync_step to save/heavy/ and aborts on connection loss/desync.\ + Using a ramdisk is recommended.") + include(CheckCXXCompilerFlag) diff --git cmake/SimutransSourceList.cmake cmake/SimutransSourceList.cmake index 726627f2c..31ce70db7 100644 --- cmake/SimutransSourceList.cmake +++ cmake/SimutransSourceList.cmake @@ -1,4 +1,4 @@ -target_sources(simutrans PRIVATE +target_sources(simutrans PRIVATE ${CMAKE_SOURCE_DIR}/revision.h bauer/brueckenbauer.cc bauer/fabrikbauer.cc diff --git config.template config.template index e29b2cab8..0047816d5 100644 --- config.template +++ config.template @@ -36,6 +36,14 @@ #WIN32_CONSOLE := 1 # adds a console for windows debugging +# Network desync debugging mode. Stores game state hash of client and server for every sync_step for comparison. +# VERY SLOW! Do not enable for normal builds. +# 0 = disabled (default) +# 1 = enabled +# 2 = enabled; Additionally saves the game every sync_step to save/heavy/ and aborts on connection loss/desync. +# Using a ramdisk is recommended. +#HEAVY_MODE := 0 + #MULTI_THREAD := 1 # Enable multithreading # using freetype for Truetype font support diff --git dataobj/loadsave.cc dataobj/loadsave.cc index 6e26935f1..b5ce62a08 100644 --- dataobj/loadsave.cc +++ dataobj/loadsave.cc @@ -1359,3 +1359,12 @@ uint32 loadsave_t::int_version(const char *version_text, char *pak_extension_str return version; } + + +#if defined(HEAVY_MODE) && HEAVY_MODE >= 1 +stream_loadsave_t::stream_loadsave_t(rdwr_stream_t *stream) +{ + this->stream = stream; + finfo.version = int_version(VERSION_NUMBER, NULL); +} +#endif diff --git dataobj/loadsave.h dataobj/loadsave.h index bfc2cda4b..bc1bad5af 100644 --- dataobj/loadsave.h +++ dataobj/loadsave.h @@ -199,11 +199,21 @@ public: }; -// this produced semicautomatic hierarchies -class xml_tag_t { +#if defined(HEAVY_MODE) && HEAVY_MODE >= 1 +class stream_loadsave_t : public loadsave_t +{ +public: + stream_loadsave_t(rdwr_stream_t *stream); +}; +#endif + +// this produces semi-automatic hierarchies +class xml_tag_t +{ private: loadsave_t *file; const char *tag; + public: xml_tag_t( loadsave_t *f, const char *t ) : file(f), tag(t) { file->start_tag(tag); } ~xml_tag_t() { file->end_tag(tag); } diff --git io/rdwr/adler32_stream.cc io/rdwr/adler32_stream.cc new file mode 100644 index 000000000..6aa948343 --- /dev/null +++ io/rdwr/adler32_stream.cc @@ -0,0 +1,40 @@ +/* + * This file is part of the Simutrans project under the Artistic License. + * (see LICENSE.txt) + */ + +#include "adler32_stream.h" + +#include "../../simdebug.h" + +#include + + +adler32_stream_t::adler32_stream_t() : + rdwr_stream_t(true) +{ + adler_checksum = adler32(0, NULL, 0); + status = STATUS_OK; +} + + +size_t adler32_stream_t::read(void *, size_t) +{ + dbg->fatal("adler32_stream_t::read", "Cannot reconstruct original message from checksum!"); + return 0; +} + + +size_t adler32_stream_t::write(const void *buf, size_t len) +{ + adler_checksum = adler32(adler_checksum, static_cast(buf), len); + return len; +} + + +uint32 adler32_stream_t::get_hash() +{ + const uint32 result = adler_checksum; + adler_checksum = adler32(0, NULL, 0); + return result; +} diff --git io/rdwr/adler32_stream.h io/rdwr/adler32_stream.h new file mode 100644 index 000000000..79f3663f5 --- /dev/null +++ io/rdwr/adler32_stream.h @@ -0,0 +1,35 @@ +/* + * This file is part of the Simutrans project under the Artistic License. + * (see LICENSE.txt) + */ + +#ifndef IO_RDWR_ADLER32_STREAM_H +#define IO_RDWR_ADLER32_STREAM_H + + +#include "rdwr_stream.h" + + +/// Computes the adler32 checksum over some data +class adler32_stream_t : public rdwr_stream_t +{ +public: + adler32_stream_t(); + +public: + /// @copydoc rdwr_stream_t::write + size_t write(const void *buf, size_t len) OVERRIDE; + + /// @returns the adler32 checksum of all the data written since the last get_hash() + uint32 get_hash(); + +private: + /// DO NOT USE! + size_t read(void *buf, size_t len) OVERRIDE; + +private: + uint32 adler_checksum; +}; + + +#endif diff --git network/network.cc network/network.cc index 564b7740f..a3a819345 100644 --- network/network.cc +++ network/network.cc @@ -755,7 +755,12 @@ bool network_receive_data( SOCKET sender, void *dest, const uint16 len, uint16 & } if (res == 0) { // connection closed - dbg->warning("network_receive_data", "connection [%d] already closed", sender); +#if defined(HEAVY_MODE) && HEAVY_MODE >= 2 + dbg->fatal( +#else + dbg->warning( +#endif + "network_receive_data", "Connection [%d] already closed", sender); return false; } received += res; diff --git simworld.cc simworld.cc index f88e6e0dc..b305c21e1 100644 --- simworld.cc +++ simworld.cc @@ -105,6 +105,11 @@ #include "player/ai_goods.h" #include "player/ai_scripted.h" +#include "io/rdwr/adler32_stream.h" + +#include "pathes.h" + + // forward declaration - management of rotation for scripting namespace script_api { @@ -7183,9 +7188,16 @@ void karte_t::do_network_world_command(network_world_command_t *nwc) const int offset = server_checklist.print(buf, "server"); LCHKLST(server_sync_step).print(buf + offset, "client"); dbg->warning("karte_t:::do_network_world_command", "sync_step=%u %s", server_sync_step, buf); + if( LCHKLST(server_sync_step)!=server_checklist ) { - dbg->warning("karte_t:::do_network_world_command", "disconnecting due to checklist mismatch" ); network_disconnect(); + +#if defined(HEAVY_MODE) && HEAVY_MODE >= 2 + dbg->fatal( +#else + dbg->warning( +#endif + "karte_t:::do_network_world_command", "Disconnected due to checklist mismatch" ); } } else { @@ -7243,6 +7255,23 @@ sint16 karte_t::get_sound_id(grund_t *gr) } +#if defined(HEAVY_MODE) && HEAVY_MODE >= 2 +static void heavy_rotate_saves(const char *prefix, uint32 sync_steps, uint32 num_to_keep) +{ + dr_mkdir( SAVE_PATH_X "heavy"); + + char name[128]; + sprintf(name, SAVE_PATH_X "heavy/heavy-%s-%04d.sve", prefix, sync_steps); + world()->save(name, false, SERVER_SAVEGAME_VER_NR, true); + + if (sync_steps >= num_to_keep) { + sprintf(name, SAVE_PATH_X "heavy/heavy-%s-%04d.sve", prefix, sync_steps - num_to_keep); + dr_remove(name); + } +} +#endif + + bool karte_t::interactive(uint32 quit_month) { @@ -7381,7 +7410,16 @@ bool karte_t::interactive(uint32 quit_month) network_frame_count = 0; } sync_steps = steps * settings.get_frames_per_step() + network_frame_count; +#if defined(HEAVY_MODE) && HEAVY_MODE >= 1 + LCHKLST(sync_steps) = checklist_t(get_gamestate_hash()); + +#if HEAVY_MODE >= 2 + heavy_rotate_saves(env_t::server ? "server" : "client", sync_steps, 10); +#endif + +#else LCHKLST(sync_steps) = checklist_t(get_random_seed(), halthandle_t::get_next_check(), linehandle_t::get_next_check(), convoihandle_t::get_next_check()); +#endif // some server side tasks if( env_t::networkmode && env_t::server ) { // broadcast sync info regularly and when lagged @@ -7620,3 +7658,15 @@ player_t *karte_t::get_public_player() const { return get_player(1); } + + +#if defined(HEAVY_MODE) && HEAVY_MODE >= 1 +uint32 karte_t::get_gamestate_hash() +{ + adler32_stream_t *stream = new adler32_stream_t; + stream_loadsave_t ls(stream); + + rdwr_gamestate(&ls, NULL); + return stream->get_hash(); +} +#endif diff --git simworld.h simworld.h index 00f3d23c6..9bec5d42e 100644 --- simworld.h +++ simworld.h @@ -1800,6 +1800,10 @@ public: */ uint32 generate_new_map_counter() const; +#if defined(HEAVY_MODE) && HEAVY_MODE >= 1 + uint32 get_gamestate_hash(); +#endif + private: void process_network_commands(sint32* ms_difference); void do_network_world_command(network_world_command_t *nwc); diff --git utils/checklist.cc utils/checklist.cc index e497fa996..c180c6869 100644 --- utils/checklist.cc +++ utils/checklist.cc @@ -13,30 +13,43 @@ checklist_t::checklist_t() : +#if defined(HEAVY_MODE) && HEAVY_MODE >= 1 + hash(0) +#else random_seed(0), halt_entry(0), line_entry(0), convoy_entry(0) +#endif { } +#if defined(HEAVY_MODE) && HEAVY_MODE >= 1 +checklist_t::checklist_t(const uint32 &hash) : + hash(hash) +#else checklist_t::checklist_t(uint32 _random_seed, uint16 _halt_entry, uint16 _line_entry, uint16 _convoy_entry) : random_seed(_random_seed), halt_entry(_halt_entry), line_entry(_line_entry), convoy_entry(_convoy_entry) +#endif { } bool checklist_t::operator==(const checklist_t& other) const { +#if defined(HEAVY_MODE) && HEAVY_MODE >= 1 + return hash == other.hash; +#else return random_seed==other.random_seed && halt_entry==other.halt_entry && line_entry==other.line_entry && convoy_entry==other.convoy_entry; +#endif } @@ -48,16 +61,24 @@ bool checklist_t::operator!=(const checklist_t& other) const void checklist_t::rdwr(memory_rw_t *buffer) { +#if defined(HEAVY_MODE) && HEAVY_MODE >= 1 + buffer->rdwr_long(hash); +#else buffer->rdwr_long(random_seed); buffer->rdwr_short(halt_entry); buffer->rdwr_short(line_entry); buffer->rdwr_short(convoy_entry); +#endif } int checklist_t::print(char *buffer, const char *entity) const { +#if defined(HEAVY_MODE) && HEAVY_MODE >= 1 + return sprintf(buffer, "%s=[adler32=%08x] ", entity, hash); +#else return sprintf(buffer, "%s=[rand=%u halt=%u line=%u cnvy=%u] ", entity, random_seed, halt_entry, line_entry, convoy_entry); +#endif } diff --git utils/checklist.h utils/checklist.h index b9b0cd897..2b6761872 100644 --- utils/checklist.h +++ utils/checklist.h @@ -17,7 +17,11 @@ struct checklist_t { public: checklist_t(); +#if defined(HEAVY_MODE) && HEAVY_MODE >= 1 + explicit checklist_t(const uint32 &hash); +#else checklist_t(uint32 _random_seed, uint16 _halt_entry, uint16 _line_entry, uint16 _convoy_entry); +#endif bool operator==(const checklist_t &other) const; bool operator!=(const checklist_t &other) const; @@ -25,11 +29,16 @@ public: void rdwr(memory_rw_t *buffer); int print(char *buffer, const char *entity) const; +#if defined(HEAVY_MODE) && HEAVY_MODE >= 1 +private: + uint32 hash; +#else public: uint32 random_seed; uint16 halt_entry; uint16 line_entry; uint16 convoy_entry; +#endif }; #endif