1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2026-01-15 11:03:00 +01:00

Gamestate snapshots (#8819)

* Add initial interface.

* Implement move operator in MemoryStream

* Add pod array serialisation traits.

* Add push_back with move semantics to CircularBuffer

* Initial implementation of GameStateSnapshots

* Add GameStateSnapshots to Context.

* Add mp_desync console command.

* Compare sprite data and fill change list.

* Minor changes.

* Proof of concept.

* Calculate offset instead of using offsetof

* Implement game state difference detection

* Update mp_desync console command.

* Fix identification of sprite remove/add.

* Fix crash when only one peep in park when using mp_desync

* Output state differences into user directory desync folder.

* Add desync debugging as an option.

* Add information to network status when a desync report was created.

* Cast to proper type for %llu.

* Update xcode project

* Add more information to the diffed data.

* Remove client-only relevant fields.

* Cleanup.

* Add better name output for misc sprites

* Add srand0 and tick information to the output

* Bump up network version

* Cleanup

* Set desync_debugging to false as default

* Apply suggestions
This commit is contained in:
ζeh Matt
2019-05-11 21:31:34 +02:00
committed by GitHub
parent 1f6c7c9942
commit c8f822ea70
22 changed files with 1152 additions and 19 deletions

View File

@@ -450,6 +450,7 @@
C6E415511FAFD6DC00D4A52A /* RideConstruction.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C6E415501FAFD6DB00D4A52A /* RideConstruction.cpp */; };
C6E96E361E0408B40076A04F /* libzip.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = C6E96E351E0408B40076A04F /* libzip.dylib */; };
C6E96E371E040E040076A04F /* libzip.dylib in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C6E96E351E0408B40076A04F /* libzip.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C9C630B62235A22D009AD16E /* GameStateSnapshots.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C9C630B52235A22C009AD16E /* GameStateSnapshots.cpp */; };
D41B73EF1C2101890080A7B9 /* libcurl.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D41B73EE1C2101890080A7B9 /* libcurl.tbd */; };
D41B741D1C210A7A0080A7B9 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D41B741C1C210A7A0080A7B9 /* libiconv.tbd */; };
D41B74731C2125E50080A7B9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D41B74721C2125E50080A7B9 /* Assets.xcassets */; };
@@ -1390,6 +1391,8 @@
C9C630BB2235A7F9009AD16E /* WaterLowerAction.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = WaterLowerAction.hpp; sourceTree = "<group>"; };
C9C630BC2235A7F9009AD16E /* WaterRaiseAction.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = WaterRaiseAction.hpp; sourceTree = "<group>"; };
C9C630BD2235A9C6009AD16E /* FootpathPlaceFromTrackAction.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = FootpathPlaceFromTrackAction.hpp; sourceTree = "<group>"; };
C9C630B42235A22C009AD16E /* GameStateSnapshots.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GameStateSnapshots.h; sourceTree = "<group>"; };
C9C630B52235A22C009AD16E /* GameStateSnapshots.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GameStateSnapshots.cpp; sourceTree = "<group>"; };
D41B73EE1C2101890080A7B9 /* libcurl.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcurl.tbd; path = usr/lib/libcurl.tbd; sourceTree = SDKROOT; };
D41B741C1C210A7A0080A7B9 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; };
D41B74721C2125E50080A7B9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = distribution/macos/Assets.xcassets; sourceTree = SOURCE_ROOT; };
@@ -2512,6 +2515,8 @@
F76C83551EC4E7CC00FA49E2 /* libopenrct2 */ = {
isa = PBXGroup;
children = (
C9C630B52235A22C009AD16E /* GameStateSnapshots.cpp */,
C9C630B42235A22C009AD16E /* GameStateSnapshots.h */,
4C358E5021C445F700ADE6BC /* ReplayManager.cpp */,
4C358E5121C445F700ADE6BC /* ReplayManager.h */,
C6352B871F477032006CCEE3 /* actions */,
@@ -3972,6 +3977,7 @@
C68878C220289B710084B384 /* DrawLineShader.cpp in Sources */,
F76C888B1EC5324E00FA49E2 /* Ui.cpp in Sources */,
C685E51A1F8907850090598F /* Staff.cpp in Sources */,
C9C630B62235A22D009AD16E /* GameStateSnapshots.cpp in Sources */,
F76C888C1EC5324E00FA49E2 /* UiContext.cpp in Sources */,
C666EE7D1F37ACB10061AA04 /* TitleMenu.cpp in Sources */,
93F6004C213DD7DD00EEB83E /* TerrainSurfaceObject.cpp in Sources */,

View File

@@ -3765,6 +3765,7 @@ STR_6314 :{WINDOW_COLOUR_2}Dest: {BLACK}{INT32}, {INT32} tolerance {INT32}
STR_6315 :{WINDOW_COLOUR_2}Pathfind Goal: {BLACK}{INT32}, {INT32}, {INT32} dir {INT32}
STR_6316 :{WINDOW_COLOUR_2}Pathfind history:
STR_6317 :{BLACK}{INT32}, {INT32}, {INT32} dir {INT32}
STR_6318 :Network desync detected.{NEWLINE}Log file: {STRING}
#############
# Scenarios #

View File

@@ -16,6 +16,7 @@
#include "FileClassifier.h"
#include "Game.h"
#include "GameState.h"
#include "GameStateSnapshots.h"
#include "Input.h"
#include "Intro.h"
#include "OpenRCT2.h"
@@ -92,6 +93,7 @@ namespace OpenRCT2
std::unique_ptr<ITrackDesignRepository> _trackDesignRepository;
std::unique_ptr<IScenarioRepository> _scenarioRepository;
std::unique_ptr<IReplayManager> _replayManager;
std::unique_ptr<IGameStateSnapshots> _gameStateSnapshots;
#ifdef __ENABLE_DISCORD__
std::unique_ptr<DiscordService> _discordService;
#endif
@@ -211,6 +213,11 @@ namespace OpenRCT2
return _replayManager.get();
}
IGameStateSnapshots* GetGameStateSnapshots() override
{
return _gameStateSnapshots.get();
}
int32_t GetDrawingEngineType() override
{
return _drawingEngineType;
@@ -340,6 +347,7 @@ namespace OpenRCT2
_trackDesignRepository = CreateTrackDesignRepository(_env);
_scenarioRepository = CreateScenarioRepository(_env);
_replayManager = CreateReplayManager();
_gameStateSnapshots = CreateGameStateSnapshots();
#ifdef __ENABLE_DISCORD__
_discordService = std::make_unique<DiscordService>();
#endif
@@ -994,6 +1002,7 @@ namespace OpenRCT2
DIRID::THEME,
DIRID::SEQUENCE,
DIRID::REPLAY,
DIRID::LOG_DESYNCS,
});
}

View File

@@ -19,6 +19,7 @@ interface IObjectRepository;
interface IScenarioRepository;
interface IStream;
interface ITrackDesignRepository;
interface IGameStateSnapshots;
class Intent;
struct rct_window;
@@ -110,6 +111,7 @@ namespace OpenRCT2
virtual ITrackDesignRepository* GetTrackDesignRepository() abstract;
virtual IScenarioRepository* GetScenarioRepository() abstract;
virtual IReplayManager* GetReplayManager() abstract;
virtual IGameStateSnapshots* GetGameStateSnapshots() abstract;
virtual int32_t GetDrawingEngineType() abstract;
virtual Drawing::IDrawingEngine* GetDrawingEngine() abstract;
virtual Paint::Painter* GetPainter() abstract;

View File

@@ -13,6 +13,7 @@
#include "Context.h"
#include "Editor.h"
#include "FileClassifier.h"
#include "GameStateSnapshots.h"
#include "Input.h"
#include "OpenRCT2.h"
#include "ParkImporter.h"
@@ -847,6 +848,9 @@ void game_load_init()
{
rct_window* mainWindow;
IGameStateSnapshots* snapshots = GetContext()->GetGameStateSnapshots();
snapshots->Reset();
gScreenFlags = SCREEN_FLAGS_PLAYING;
audio_stop_all_music_and_sounds();
if (!gLoadKeepWindowsOpen)

View File

@@ -12,9 +12,11 @@
#include "Context.h"
#include "Editor.h"
#include "Game.h"
#include "GameStateSnapshots.h"
#include "Input.h"
#include "OpenRCT2.h"
#include "ReplayManager.h"
#include "config/Config.h"
#include "interface/Screenshot.h"
#include "localisation/Date.h"
#include "localisation/Localisation.h"
@@ -238,13 +240,30 @@ void GameState::UpdateLogic()
if (network_get_mode() == NETWORK_MODE_SERVER)
{
if (network_gamestate_snapshots_enabled())
{
CreateStateSnapshot();
}
// Send current tick out.
network_send_tick();
}
else if (network_get_mode() == NETWORK_MODE_CLIENT)
{
// Check desync.
network_check_desynchronization();
bool desynced = network_check_desynchronisation();
if (desynced)
{
// If desync debugging is enabled and we are still connected request the specific game state from server.
if (network_gamestate_snapshots_enabled() && network_get_status() == NETWORK_STATUS_CONNECTED)
{
// Create snapshot from this tick so we can compare it later
// as we won't pause the game on this event.
CreateStateSnapshot();
network_request_gamestate_snapshot();
}
}
}
date_update();
@@ -311,3 +330,12 @@ void GameState::UpdateLogic()
gScenarioTicks++;
gSavedAge++;
}
void GameState::CreateStateSnapshot()
{
IGameStateSnapshots* snapshots = GetContext()->GetGameStateSnapshots();
auto& snapshot = snapshots->CreateSnapshot();
snapshots->Capture(snapshot);
snapshots->LinkSnapshot(snapshot, gCurrentTicks, scenario_rand_state().s0);
}

View File

@@ -42,5 +42,8 @@ namespace OpenRCT2
void InitAll(int32_t mapSize);
void Update();
void UpdateLogic();
private:
void CreateStateSnapshot();
};
} // namespace OpenRCT2

View File

@@ -0,0 +1,578 @@
#include "GameStateSnapshots.h"
#include "core/CircularBuffer.h"
#include "peep/Peep.h"
#include "world/Sprite.h"
static constexpr size_t MaximumGameStateSnapshots = 32;
static constexpr uint32_t InvalidTick = 0xFFFFFFFF;
struct GameStateSnapshot_t
{
GameStateSnapshot_t& operator=(GameStateSnapshot_t&& mv)
{
tick = mv.tick;
storedSprites = std::move(mv.storedSprites);
return *this;
}
uint32_t tick = InvalidTick;
uint32_t srand0 = 0;
MemoryStream storedSprites;
MemoryStream parkParameters;
void SerialiseSprites(rct_sprite* sprites, const size_t numSprites, bool saving)
{
const bool loading = !saving;
storedSprites.SetPosition(0);
DataSerialiser ds(saving, storedSprites);
std::vector<uint32_t> indexTable;
indexTable.reserve(numSprites);
uint32_t numSavedSprites = 0;
if (saving)
{
for (size_t i = 0; i < numSprites; i++)
{
if (sprites[i].generic.sprite_identifier == SPRITE_IDENTIFIER_NULL)
continue;
indexTable.push_back((uint32_t)i);
}
numSavedSprites = (uint32_t)indexTable.size();
}
ds << numSavedSprites;
if (loading)
{
indexTable.resize(numSavedSprites);
}
for (uint32_t i = 0; i < numSavedSprites; i++)
{
ds << indexTable[i];
const uint32_t spriteIdx = indexTable[i];
rct_sprite& sprite = sprites[spriteIdx];
ds << sprite.generic.sprite_identifier;
switch (sprite.generic.sprite_identifier)
{
case SPRITE_IDENTIFIER_VEHICLE:
ds << reinterpret_cast<uint8_t(&)[sizeof(rct_vehicle)]>(sprite.vehicle);
break;
case SPRITE_IDENTIFIER_PEEP:
ds << reinterpret_cast<uint8_t(&)[sizeof(Peep)]>(sprite.peep);
break;
case SPRITE_IDENTIFIER_LITTER:
ds << reinterpret_cast<uint8_t(&)[sizeof(rct_litter)]>(sprite.litter);
break;
case SPRITE_IDENTIFIER_MISC:
{
ds << sprite.generic.type;
switch (sprite.generic.type)
{
case SPRITE_MISC_MONEY_EFFECT:
ds << reinterpret_cast<uint8_t(&)[sizeof(rct_money_effect)]>(sprite.money_effect);
break;
case SPRITE_MISC_BALLOON:
ds << reinterpret_cast<uint8_t(&)[sizeof(rct_balloon)]>(sprite.balloon);
break;
case SPRITE_MISC_DUCK:
ds << reinterpret_cast<uint8_t(&)[sizeof(rct_duck)]>(sprite.duck);
break;
case SPRITE_MISC_JUMPING_FOUNTAIN_WATER:
ds << reinterpret_cast<uint8_t(&)[sizeof(rct_jumping_fountain)]>(sprite.jumping_fountain);
break;
case SPRITE_MISC_STEAM_PARTICLE:
ds << reinterpret_cast<uint8_t(&)[sizeof(rct_steam_particle)]>(sprite.steam_particle);
break;
}
}
break;
}
}
}
};
struct GameStateSnapshots : public IGameStateSnapshots
{
virtual void Reset() override final
{
_snapshots.clear();
}
virtual GameStateSnapshot_t& CreateSnapshot() override final
{
auto snapshot = std::make_unique<GameStateSnapshot_t>();
_snapshots.push_back(std::move(snapshot));
return *_snapshots.back();
}
virtual void LinkSnapshot(GameStateSnapshot_t& snapshot, uint32_t tick, uint32_t srand0) override final
{
snapshot.tick = tick;
snapshot.srand0 = srand0;
}
virtual void Capture(GameStateSnapshot_t& snapshot) override final
{
snapshot.SerialiseSprites(get_sprite(0), MAX_SPRITES, true);
// log_info("Snapshot size: %u bytes\n", (uint32_t)snapshot.storedSprites.GetLength());
}
virtual const GameStateSnapshot_t* GetLinkedSnapshot(uint32_t tick) const override final
{
for (size_t i = 0; i < _snapshots.size(); i++)
{
if (_snapshots[i]->tick == tick)
return _snapshots[i].get();
}
return nullptr;
}
virtual void SerialiseSnapshot(GameStateSnapshot_t& snapshot, DataSerialiser& ds) const override final
{
ds << snapshot.tick;
ds << snapshot.srand0;
ds << snapshot.storedSprites;
ds << snapshot.parkParameters;
}
std::vector<rct_sprite> BuildSpriteList(GameStateSnapshot_t& snapshot) const
{
std::vector<rct_sprite> spriteList;
spriteList.resize(MAX_SPRITES);
for (auto& sprite : spriteList)
{
// By default they don't exist.
sprite.generic.sprite_identifier = SPRITE_IDENTIFIER_NULL;
}
snapshot.SerialiseSprites(spriteList.data(), MAX_SPRITES, false);
return spriteList;
}
#define COMPARE_FIELD(struc, field) \
if (std::memcmp(&spriteBase.field, &spriteCmp.field, sizeof(struc::field)) != 0) \
{ \
uint64_t valA = 0; \
uint64_t valB = 0; \
std::memcpy(&valA, &spriteBase.field, sizeof(struc::field)); \
std::memcpy(&valB, &spriteCmp.field, sizeof(struc::field)); \
uintptr_t offset = reinterpret_cast<uintptr_t>(&spriteBase.field) - reinterpret_cast<uintptr_t>(&spriteBase); \
changeData.diffs.push_back( \
GameStateSpriteChange_t::Diff_t{ (size_t)offset, sizeof(struc::field), #struc, #field, valA, valB }); \
}
void CompareSpriteDataCommon(
const rct_sprite_common& spriteBase, const rct_sprite_common& spriteCmp, GameStateSpriteChange_t& changeData) const
{
COMPARE_FIELD(rct_sprite_common, sprite_identifier);
COMPARE_FIELD(rct_sprite_common, type);
COMPARE_FIELD(rct_sprite_common, next_in_quadrant);
COMPARE_FIELD(rct_sprite_common, next);
COMPARE_FIELD(rct_sprite_common, previous);
COMPARE_FIELD(rct_sprite_common, linked_list_type_offset);
COMPARE_FIELD(rct_sprite_common, sprite_index);
COMPARE_FIELD(rct_sprite_common, flags);
COMPARE_FIELD(rct_sprite_common, x);
COMPARE_FIELD(rct_sprite_common, y);
COMPARE_FIELD(rct_sprite_common, z);
/* Only relevant for rendering, does not affect game state.
COMPARE_FIELD(rct_sprite_common, sprite_width);
COMPARE_FIELD(rct_sprite_common, sprite_height_negative);
COMPARE_FIELD(rct_sprite_common, sprite_height_positive);
COMPARE_FIELD(rct_sprite_common, sprite_left);
COMPARE_FIELD(rct_sprite_common, sprite_top);
COMPARE_FIELD(rct_sprite_common, sprite_right);
COMPARE_FIELD(rct_sprite_common, sprite_bottom);
*/
COMPARE_FIELD(rct_sprite_common, sprite_direction);
}
void CompareSpriteDataPeep(const Peep& spriteBase, const Peep& spriteCmp, GameStateSpriteChange_t& changeData) const
{
COMPARE_FIELD(Peep, name_string_idx);
COMPARE_FIELD(Peep, next_x);
COMPARE_FIELD(Peep, next_y);
COMPARE_FIELD(Peep, next_z);
COMPARE_FIELD(Peep, next_flags);
COMPARE_FIELD(Peep, outside_of_park);
COMPARE_FIELD(Peep, state);
COMPARE_FIELD(Peep, sub_state);
COMPARE_FIELD(Peep, sprite_type);
COMPARE_FIELD(Peep, type);
COMPARE_FIELD(Peep, no_of_rides);
COMPARE_FIELD(Peep, tshirt_colour);
COMPARE_FIELD(Peep, trousers_colour);
COMPARE_FIELD(Peep, destination_x);
COMPARE_FIELD(Peep, destination_y);
COMPARE_FIELD(Peep, destination_tolerance);
COMPARE_FIELD(Peep, var_37);
COMPARE_FIELD(Peep, energy);
COMPARE_FIELD(Peep, energy_target);
COMPARE_FIELD(Peep, happiness);
COMPARE_FIELD(Peep, happiness_target);
COMPARE_FIELD(Peep, nausea);
COMPARE_FIELD(Peep, nausea_target);
COMPARE_FIELD(Peep, hunger);
COMPARE_FIELD(Peep, thirst);
COMPARE_FIELD(Peep, toilet);
COMPARE_FIELD(Peep, mass);
COMPARE_FIELD(Peep, time_to_consume);
COMPARE_FIELD(Peep, intensity);
COMPARE_FIELD(Peep, nausea_tolerance);
COMPARE_FIELD(Peep, window_invalidate_flags);
COMPARE_FIELD(Peep, paid_on_drink);
for (int i = 0; i < PEEP_MAX_THOUGHTS; i++)
{
COMPARE_FIELD(Peep, ride_types_been_on[i]);
}
COMPARE_FIELD(Peep, item_extra_flags);
COMPARE_FIELD(Peep, photo2_ride_ref);
COMPARE_FIELD(Peep, photo3_ride_ref);
COMPARE_FIELD(Peep, photo4_ride_ref);
COMPARE_FIELD(Peep, current_ride);
COMPARE_FIELD(Peep, current_ride_station);
COMPARE_FIELD(Peep, current_train);
COMPARE_FIELD(Peep, time_to_sitdown);
COMPARE_FIELD(Peep, special_sprite);
COMPARE_FIELD(Peep, action_sprite_type);
COMPARE_FIELD(Peep, next_action_sprite_type);
COMPARE_FIELD(Peep, action_sprite_image_offset);
COMPARE_FIELD(Peep, action);
COMPARE_FIELD(Peep, action_frame);
COMPARE_FIELD(Peep, step_progress);
COMPARE_FIELD(Peep, next_in_queue);
COMPARE_FIELD(Peep, maze_last_edge);
COMPARE_FIELD(Peep, interaction_ride_index);
COMPARE_FIELD(Peep, time_in_queue);
for (int i = 0; i < 32; i++)
{
COMPARE_FIELD(Peep, rides_been_on[i]);
}
COMPARE_FIELD(Peep, id);
COMPARE_FIELD(Peep, cash_in_pocket);
COMPARE_FIELD(Peep, cash_spent);
COMPARE_FIELD(Peep, time_in_park);
COMPARE_FIELD(Peep, rejoin_queue_timeout);
COMPARE_FIELD(Peep, previous_ride);
COMPARE_FIELD(Peep, previous_ride_time_out);
for (int i = 0; i < PEEP_MAX_THOUGHTS; i++)
{
COMPARE_FIELD(Peep, thoughts[i]);
}
COMPARE_FIELD(Peep, path_check_optimisation);
COMPARE_FIELD(Peep, guest_heading_to_ride_id);
COMPARE_FIELD(Peep, staff_orders);
COMPARE_FIELD(Peep, photo1_ride_ref);
COMPARE_FIELD(Peep, peep_flags);
COMPARE_FIELD(Peep, pathfind_goal);
for (int i = 0; i < 4; i++)
{
COMPARE_FIELD(Peep, pathfind_history[i]);
}
COMPARE_FIELD(Peep, no_action_frame_num);
COMPARE_FIELD(Peep, litter_count);
COMPARE_FIELD(Peep, time_on_ride);
COMPARE_FIELD(Peep, disgusting_count);
COMPARE_FIELD(Peep, paid_to_enter);
COMPARE_FIELD(Peep, paid_on_rides);
COMPARE_FIELD(Peep, paid_on_food);
COMPARE_FIELD(Peep, paid_on_souvenirs);
COMPARE_FIELD(Peep, no_of_food);
COMPARE_FIELD(Peep, no_of_drinks);
COMPARE_FIELD(Peep, no_of_souvenirs);
COMPARE_FIELD(Peep, vandalism_seen);
COMPARE_FIELD(Peep, voucher_type);
COMPARE_FIELD(Peep, voucher_arguments);
COMPARE_FIELD(Peep, surroundings_thought_timeout);
COMPARE_FIELD(Peep, angriness);
COMPARE_FIELD(Peep, time_lost);
COMPARE_FIELD(Peep, days_in_queue);
COMPARE_FIELD(Peep, balloon_colour);
COMPARE_FIELD(Peep, umbrella_colour);
COMPARE_FIELD(Peep, hat_colour);
COMPARE_FIELD(Peep, favourite_ride);
COMPARE_FIELD(Peep, favourite_ride_rating);
COMPARE_FIELD(Peep, item_standard_flags);
}
void CompareSpriteDataVehicle(
const rct_vehicle& spriteBase, const rct_vehicle& spriteCmp, GameStateSpriteChange_t& changeData) const
{
COMPARE_FIELD(rct_vehicle, vehicle_sprite_type);
COMPARE_FIELD(rct_vehicle, bank_rotation);
COMPARE_FIELD(rct_vehicle, remaining_distance);
COMPARE_FIELD(rct_vehicle, velocity);
COMPARE_FIELD(rct_vehicle, acceleration);
COMPARE_FIELD(rct_vehicle, ride);
COMPARE_FIELD(rct_vehicle, vehicle_type);
COMPARE_FIELD(rct_vehicle, colours);
COMPARE_FIELD(rct_vehicle, track_progress);
COMPARE_FIELD(rct_vehicle, track_direction);
COMPARE_FIELD(rct_vehicle, track_x);
COMPARE_FIELD(rct_vehicle, track_y);
COMPARE_FIELD(rct_vehicle, track_z);
COMPARE_FIELD(rct_vehicle, next_vehicle_on_train);
COMPARE_FIELD(rct_vehicle, prev_vehicle_on_ride);
COMPARE_FIELD(rct_vehicle, next_vehicle_on_ride);
COMPARE_FIELD(rct_vehicle, var_44);
COMPARE_FIELD(rct_vehicle, mass);
COMPARE_FIELD(rct_vehicle, update_flags);
COMPARE_FIELD(rct_vehicle, swing_sprite);
COMPARE_FIELD(rct_vehicle, current_station);
COMPARE_FIELD(rct_vehicle, swinging_car_var_0);
COMPARE_FIELD(rct_vehicle, var_4E);
COMPARE_FIELD(rct_vehicle, status);
COMPARE_FIELD(rct_vehicle, sub_state);
for (int i = 0; i < 32; i++)
{
COMPARE_FIELD(rct_vehicle, peep[i]);
}
for (int i = 0; i < 32; i++)
{
COMPARE_FIELD(rct_vehicle, peep_tshirt_colours[i]);
}
COMPARE_FIELD(rct_vehicle, num_seats);
COMPARE_FIELD(rct_vehicle, num_peeps);
COMPARE_FIELD(rct_vehicle, next_free_seat);
COMPARE_FIELD(rct_vehicle, restraints_position);
COMPARE_FIELD(rct_vehicle, spin_speed);
COMPARE_FIELD(rct_vehicle, sound2_flags);
COMPARE_FIELD(rct_vehicle, spin_sprite);
COMPARE_FIELD(rct_vehicle, sound1_id);
COMPARE_FIELD(rct_vehicle, sound1_volume);
COMPARE_FIELD(rct_vehicle, sound2_id);
COMPARE_FIELD(rct_vehicle, sound2_volume);
COMPARE_FIELD(rct_vehicle, sound_vector_factor);
COMPARE_FIELD(rct_vehicle, cable_lift_target);
COMPARE_FIELD(rct_vehicle, speed);
COMPARE_FIELD(rct_vehicle, powered_acceleration);
COMPARE_FIELD(rct_vehicle, var_C4);
COMPARE_FIELD(rct_vehicle, animation_frame);
for (int i = 0; i < 2; i++)
{
COMPARE_FIELD(rct_vehicle, pad_C6[i]);
}
COMPARE_FIELD(rct_vehicle, var_C8);
COMPARE_FIELD(rct_vehicle, var_CA);
COMPARE_FIELD(rct_vehicle, scream_sound_id);
COMPARE_FIELD(rct_vehicle, var_CD);
COMPARE_FIELD(rct_vehicle, num_laps);
COMPARE_FIELD(rct_vehicle, brake_speed);
COMPARE_FIELD(rct_vehicle, lost_time_out);
COMPARE_FIELD(rct_vehicle, vertical_drop_countdown);
COMPARE_FIELD(rct_vehicle, var_D3);
COMPARE_FIELD(rct_vehicle, mini_golf_current_animation);
COMPARE_FIELD(rct_vehicle, mini_golf_flags);
COMPARE_FIELD(rct_vehicle, ride_subtype);
COMPARE_FIELD(rct_vehicle, colours_extended);
COMPARE_FIELD(rct_vehicle, seat_rotation);
COMPARE_FIELD(rct_vehicle, target_seat_rotation);
}
void CompareSpriteDataLitter(
const rct_litter& spriteBase, const rct_litter& spriteCmp, GameStateSpriteChange_t& changeData) const
{
COMPARE_FIELD(rct_litter, creationTick);
}
void CompareSpriteData(const rct_sprite& spriteBase, const rct_sprite& spriteCmp, GameStateSpriteChange_t& changeData) const
{
CompareSpriteDataCommon(spriteBase.generic, spriteCmp.generic, changeData);
if (spriteBase.generic.sprite_identifier == spriteCmp.generic.sprite_identifier)
{
switch (spriteBase.generic.sprite_identifier)
{
case SPRITE_IDENTIFIER_PEEP:
CompareSpriteDataPeep(spriteBase.peep, spriteCmp.peep, changeData);
break;
case SPRITE_IDENTIFIER_VEHICLE:
CompareSpriteDataVehicle(spriteBase.vehicle, spriteCmp.vehicle, changeData);
break;
case SPRITE_IDENTIFIER_LITTER:
CompareSpriteDataLitter(spriteBase.litter, spriteCmp.litter, changeData);
break;
}
}
}
virtual GameStateCompareData_t Compare(const GameStateSnapshot_t& base, const GameStateSnapshot_t& cmp) const override final
{
GameStateCompareData_t res;
res.tick = base.tick;
res.srand0Left = base.srand0;
res.srand0Right = cmp.srand0;
std::vector<rct_sprite> spritesBase = BuildSpriteList(const_cast<GameStateSnapshot_t&>(base));
std::vector<rct_sprite> spritesCmp = BuildSpriteList(const_cast<GameStateSnapshot_t&>(cmp));
for (uint32_t i = 0; i < (uint32_t)spritesBase.size(); i++)
{
GameStateSpriteChange_t changeData;
changeData.spriteIndex = i;
const rct_sprite& spriteBase = spritesBase[i];
const rct_sprite& spriteCmp = spritesCmp[i];
changeData.spriteIdentifier = spriteBase.generic.sprite_identifier;
changeData.miscIdentifier = spriteBase.generic.type;
if (spriteBase.generic.sprite_identifier == SPRITE_IDENTIFIER_NULL
&& spriteCmp.generic.sprite_identifier != SPRITE_IDENTIFIER_NULL)
{
// Sprite was added.
changeData.changeType = GameStateSpriteChange_t::ADDED;
changeData.spriteIdentifier = spriteCmp.generic.sprite_identifier;
}
else if (
spriteBase.generic.sprite_identifier != SPRITE_IDENTIFIER_NULL
&& spriteCmp.generic.sprite_identifier == SPRITE_IDENTIFIER_NULL)
{
// Sprite was removed.
changeData.changeType = GameStateSpriteChange_t::REMOVED;
changeData.spriteIdentifier = spriteBase.generic.sprite_identifier;
}
else if (
spriteBase.generic.sprite_identifier == SPRITE_IDENTIFIER_NULL
&& spriteCmp.generic.sprite_identifier == SPRITE_IDENTIFIER_NULL)
{
// Do nothing.
changeData.changeType = GameStateSpriteChange_t::EQUAL;
}
else
{
CompareSpriteData(spriteBase, spriteCmp, changeData);
if (changeData.diffs.size() == 0)
{
changeData.changeType = GameStateSpriteChange_t::EQUAL;
}
else
{
changeData.changeType = GameStateSpriteChange_t::MODIFIED;
}
}
res.spriteChanges.push_back(changeData);
}
return res;
}
static const char* GetSpriteIdentifierName(uint32_t spriteIdentifier, uint8_t miscIdentifier)
{
switch (spriteIdentifier)
{
case SPRITE_IDENTIFIER_NULL:
return "Null";
case SPRITE_IDENTIFIER_PEEP:
return "Peep";
case SPRITE_IDENTIFIER_VEHICLE:
return "Vehicle";
case SPRITE_IDENTIFIER_LITTER:
return "Litter";
case SPRITE_IDENTIFIER_MISC:
switch (miscIdentifier)
{
case SPRITE_MISC_STEAM_PARTICLE:
return "Misc: Steam Particle";
case SPRITE_MISC_MONEY_EFFECT:
return "Misc: Money effect";
case SPRITE_MISC_CRASHED_VEHICLE_PARTICLE:
return "Misc: Crash Vehicle Particle";
case SPRITE_MISC_EXPLOSION_CLOUD:
return "Misc: Explosion Cloud";
case SPRITE_MISC_CRASH_SPLASH:
return "Misc: Crash Splash";
case SPRITE_MISC_EXPLOSION_FLARE:
return "Misc: Explosion Flare";
case SPRITE_MISC_JUMPING_FOUNTAIN_WATER:
return "Misc: Jumping fountain water";
case SPRITE_MISC_BALLOON:
return "Misc: Balloon";
case SPRITE_MISC_DUCK:
return "Misc: Duck";
case SPRITE_MISC_JUMPING_FOUNTAIN_SNOW:
return "Misc: Jumping fountain snow";
}
return "Misc";
}
return "Unknown";
}
virtual bool LogCompareDataToFile(const std::string& fileName, const GameStateCompareData_t& cmpData) const override
{
std::string outputBuffer;
char tempBuffer[1024] = {};
snprintf(tempBuffer, sizeof(tempBuffer), "tick: %08X\n", cmpData.tick);
outputBuffer += tempBuffer;
snprintf(
tempBuffer, sizeof(tempBuffer), "srand0 left = %08X, srand0 right = %08X\n", cmpData.srand0Left,
cmpData.srand0Right);
outputBuffer += tempBuffer;
for (auto& change : cmpData.spriteChanges)
{
if (change.changeType == GameStateSpriteChange_t::EQUAL)
continue;
const char* typeName = GetSpriteIdentifierName(change.spriteIdentifier, change.miscIdentifier);
if (change.changeType == GameStateSpriteChange_t::ADDED)
{
snprintf(tempBuffer, sizeof(tempBuffer), "Sprite added (%s), index: %u\n", typeName, change.spriteIndex);
outputBuffer += tempBuffer;
}
else if (change.changeType == GameStateSpriteChange_t::REMOVED)
{
snprintf(tempBuffer, sizeof(tempBuffer), "Sprite removed (%s), index: %u\n", typeName, change.spriteIndex);
outputBuffer += tempBuffer;
}
else if (change.changeType == GameStateSpriteChange_t::MODIFIED)
{
snprintf(
tempBuffer, sizeof(tempBuffer), "Sprite modifications (%s), index: %u\n", typeName, change.spriteIndex);
outputBuffer += tempBuffer;
for (auto& diff : change.diffs)
{
snprintf(
tempBuffer, sizeof(tempBuffer),
" %s::%s, len = %u, offset = %u, left = 0x%.16llX, right = 0x%.16llX\n", diff.structname,
diff.fieldname, (uint32_t)diff.length, (uint32_t)diff.offset, (unsigned long long)diff.valueA,
(unsigned long long)diff.valueB);
outputBuffer += tempBuffer;
}
}
}
FILE* fp = fopen(fileName.c_str(), "wt");
if (!fp)
return false;
fputs(outputBuffer.c_str(), fp);
fclose(fp);
return true;
}
private:
CircularBuffer<std::unique_ptr<GameStateSnapshot_t>, MaximumGameStateSnapshots> _snapshots;
};
std::unique_ptr<IGameStateSnapshots> CreateGameStateSnapshots()
{
return std::make_unique<GameStateSnapshots>();
}

View File

@@ -0,0 +1,108 @@
/*****************************************************************************
* Copyright (c) 2014-2019 OpenRCT2 developers
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
*****************************************************************************/
#pragma once
#include "common.h"
#include "core/DataSerialiser.h"
#include <memory>
#include <set>
#include <string>
struct GameStateSnapshot_t;
struct GameStateSpriteChange_t
{
enum
{
REMOVED,
ADDED,
MODIFIED,
EQUAL
};
struct Diff_t
{
size_t offset;
size_t length;
const char* structname;
const char* fieldname;
uint64_t valueA;
uint64_t valueB;
};
uint8_t changeType;
uint8_t spriteIdentifier;
uint8_t miscIdentifier;
uint32_t spriteIndex;
std::vector<Diff_t> diffs;
};
struct GameStateCompareData_t
{
uint32_t tick;
uint32_t srand0Left;
uint32_t srand0Right;
std::vector<GameStateSpriteChange_t> spriteChanges;
};
/*
* Interface to create and capture game states. It only allows to have 32 active snapshots
* the oldest snapshot will be removed from the buffer. Never store the snapshot pointer
* as it may become invalid at any time when a snapshot is created, rather Link the snapshot
* to a specific tick which can be obtained by that later again assuming its still valid.
*/
interface IGameStateSnapshots
{
virtual ~IGameStateSnapshots() = default;
/*
* Removes all existing entries and starts over.
*/
virtual void Reset() = 0;
/*
* Creates a new empty snapshot, oldest snapshot will be removed.
*/
virtual GameStateSnapshot_t& CreateSnapshot() = 0;
/*
* Links the snapshot to a specific game tick.
*/
virtual void LinkSnapshot(GameStateSnapshot_t & snapshot, uint32_t tick, uint32_t srand0) = 0;
/*
* This will fill the snapshot with the current game state in a compact form.
*/
virtual void Capture(GameStateSnapshot_t & snapshot) = 0;
/*
* Returns the snapshot for a given tick in the history, nullptr if not found.
*/
virtual const GameStateSnapshot_t* GetLinkedSnapshot(uint32_t tick) const = 0;
/*
* Serialisation of GameStateSnapshot_t
*/
virtual void SerialiseSnapshot(GameStateSnapshot_t & snapshot, DataSerialiser & serialiser) const = 0;
/*
* Compares two states resulting GameStateCompareData_t with all mismatches stored.
*/
virtual GameStateCompareData_t Compare(const GameStateSnapshot_t& base, const GameStateSnapshot_t& cmp) const = 0;
/*
* Writes the GameStateCompareData_t into the specified file as readable text.
*/
virtual bool LogCompareDataToFile(const std::string& fileName, const GameStateCompareData_t& cmpData) const = 0;
};
std::unique_ptr<IGameStateSnapshots> CreateGameStateSnapshots();

View File

@@ -221,6 +221,7 @@ const char * PlatformEnvironment::DirectoryNamesOpenRCT2[] =
"track", // TRACK
"heightmap", // HEIGHTMAP
"replay", // REPLAY
"desyncs", // DESYNCS
};
const char * PlatformEnvironment::FileNames[] =

View File

@@ -47,6 +47,7 @@ namespace OpenRCT2
TRACK, // Contains track designs.
HEIGHTMAP, // Contains heightmap data.
REPLAY, // Contains recorded replays.
LOG_DESYNCS, // Contains desync reports.
};
enum class PATHID

View File

@@ -395,6 +395,7 @@ namespace Config
model->log_chat = reader->GetBoolean("log_chat", false);
model->log_server_actions = reader->GetBoolean("log_server_actions", false);
model->pause_server_if_no_clients = reader->GetBoolean("pause_server_if_no_clients", false);
model->desync_debugging = reader->GetBoolean("desync_debugging", false);
}
}
@@ -420,6 +421,7 @@ namespace Config
writer->WriteBoolean("log_chat", model->log_chat);
writer->WriteBoolean("log_server_actions", model->log_server_actions);
writer->WriteBoolean("pause_server_if_no_clients", model->pause_server_if_no_clients);
writer->WriteBoolean("desync_debugging", model->desync_debugging);
}
static void ReadNotifications(IIniReader* reader)

View File

@@ -156,6 +156,7 @@ struct NetworkConfiguration
bool log_chat;
bool log_server_actions;
bool pause_server_if_no_clients;
bool desync_debugging;
};
struct NotificationConfiguration

View File

@@ -104,6 +104,34 @@ public:
}
}
void push_back(value_type&& val)
{
if (_size == 0)
{
_elements[_head] = std::move(val);
_tail = _head;
_size++;
}
else if (_size != capacity())
{
_tail++;
if (_tail == capacity())
_tail = 0;
_size++;
_elements[_tail] = std::move(val);
}
else
{
_head++;
if (_head == capacity())
_head = 0;
_tail++;
if (_tail == capacity())
_tail = 0;
_elements[_tail] = std::move(val);
}
}
private:
size_t _head = 0;
size_t _tail = 0;

View File

@@ -253,6 +253,64 @@ template<> struct DataSerializerTraits<MemoryStream>
}
};
template<typename _Ty, size_t _Size> struct DataSerializerTraitsPODArray
{
static void encode(IStream* stream, const _Ty (&val)[_Size])
{
uint16_t len = (uint16_t)_Size;
uint16_t swapped = ByteSwapBE(len);
stream->Write(&swapped);
DataSerializerTraits<uint8_t> s;
for (auto&& sub : val)
{
s.encode(stream, sub);
}
}
static void decode(IStream* stream, _Ty (&val)[_Size])
{
uint16_t len;
stream->Read(&len);
len = ByteSwapBE(len);
if (len != _Size)
throw std::runtime_error("Invalid size, can't decode");
DataSerializerTraits<_Ty> s;
for (auto&& sub : val)
{
s.decode(stream, sub);
}
}
static void log(IStream* stream, const _Ty (&val)[_Size])
{
stream->Write("{", 1);
DataSerializerTraits<_Ty> s;
for (auto&& sub : val)
{
s.log(stream, sub);
stream->Write("; ", 2);
}
stream->Write("}", 1);
}
};
template<size_t _Size> struct DataSerializerTraits<uint8_t[_Size]> : public DataSerializerTraitsPODArray<uint8_t, _Size>
{
};
template<size_t _Size> struct DataSerializerTraits<uint16_t[_Size]> : public DataSerializerTraitsPODArray<uint16_t, _Size>
{
};
template<size_t _Size> struct DataSerializerTraits<uint32_t[_Size]> : public DataSerializerTraitsPODArray<uint32_t, _Size>
{
};
template<size_t _Size> struct DataSerializerTraits<uint64_t[_Size]> : public DataSerializerTraitsPODArray<uint64_t, _Size>
{
};
template<typename _Ty, size_t _Size> struct DataSerializerTraits<std::array<_Ty, _Size>>
{
static void encode(IStream* stream, const std::array<_Ty, _Size>& val)

View File

@@ -49,6 +49,11 @@ MemoryStream::MemoryStream(const void* data, size_t dataSize)
{
}
MemoryStream::MemoryStream(MemoryStream&& mv)
{
*this = std::move(mv);
}
MemoryStream::~MemoryStream()
{
if (_access & MEMORY_ACCESS::OWNER)
@@ -60,6 +65,21 @@ MemoryStream::~MemoryStream()
_data = nullptr;
}
MemoryStream& MemoryStream::operator=(MemoryStream&& mv)
{
_access = mv._access;
_dataCapacity = mv._dataCapacity;
_data = mv._data;
_position = mv._position;
mv._data = nullptr;
mv._position = nullptr;
mv._dataCapacity = 0;
mv._dataSize = 0;
return *this;
}
const void* MemoryStream::GetData() const
{
return _data;

View File

@@ -34,11 +34,14 @@ private:
public:
MemoryStream() = default;
MemoryStream(const MemoryStream& copy);
MemoryStream(MemoryStream&& mv);
explicit MemoryStream(size_t capacity);
MemoryStream(void* data, size_t dataSize, uint8_t access = MEMORY_ACCESS::READ);
MemoryStream(const void* data, size_t dataSize);
virtual ~MemoryStream();
MemoryStream& operator=(MemoryStream&& mv);
const void* GetData() const;
void* GetDataCopy() const;
void* TakeData();

View File

@@ -1549,6 +1549,66 @@ static int32_t cc_replay_normalise(InteractiveConsole& console, const arguments_
return 0;
}
static int32_t cc_mp_desync(InteractiveConsole& console, const arguments_t& argv)
{
int32_t desyncType = 0;
if (argv.size() >= 1)
{
desyncType = atoi(argv[0].c_str());
}
std::vector<rct_sprite*> peeps;
std::vector<rct_sprite*> vehicles;
for (int i = 0; i < MAX_SPRITES; i++)
{
rct_sprite* sprite = get_sprite(i);
if (sprite->generic.sprite_identifier == SPRITE_IDENTIFIER_NULL)
continue;
if (sprite->generic.sprite_identifier == SPRITE_IDENTIFIER_PEEP)
peeps.push_back(sprite);
else if (sprite->generic.sprite_identifier == SPRITE_IDENTIFIER_VEHICLE)
vehicles.push_back(sprite);
}
switch (desyncType)
{
case 0: // Peep t-shirts.
{
if (peeps.empty())
{
console.WriteFormatLine("No peeps");
}
else
{
rct_sprite* sprite = peeps[0];
if (peeps.size() > 1)
sprite = peeps[util_rand() % peeps.size() - 1];
sprite->peep.tshirt_colour = util_rand() & 0xFF;
invalidate_sprite_0(sprite);
}
break;
}
case 1: // Remove random peep.
{
if (peeps.empty())
{
console.WriteFormatLine("No peep removed");
}
else
{
rct_sprite* sprite = peeps[0];
if (peeps.size() > 1)
sprite = peeps[util_rand() % peeps.size() - 1];
sprite->AsPeep()->Remove();
}
break;
}
}
return 0;
}
#pragma warning(push)
#pragma warning(disable : 4702) // unreachable code
static int32_t cc_abort([[maybe_unused]] InteractiveConsole& console, [[maybe_unused]] const arguments_t& argv)
@@ -1670,6 +1730,7 @@ static constexpr const console_command console_command_table[] = {
{ "replay_start", cc_replay_start, "Starts a replay", "replay_start <name>"},
{ "replay_stop", cc_replay_stop, "Stops the replay", "replay_stop"},
{ "replay_normalise", cc_replay_normalise, "Normalises the replay to remove all gaps", "replay_normalise <input file> <output file>"},
{ "mp_desync", cc_mp_desync, "Forces a multiplayer desync", "cc_mp_desync [desync_type, 0 = Random t-shirt color on random peep, 1 = Remove random peep ]"},
};
// clang-format on

View File

@@ -3934,6 +3934,7 @@ enum
STR_DOWNLOADING_OBJECTS = 6303,
STR_SHORTCUT_OPEN_SCENERY_PICKER = 6304,
STR_MULTITHREADING = 6305,
STR_MULTITHREADING_TIP = 6306,
@@ -3952,6 +3953,8 @@ enum
STR_PEEP_DEBUG_PATHFIND_HISTORY = 6316,
STR_PEEP_DEBUG_PATHFIND_HISTORY_ITEM = 6317,
STR_DESYNC_REPORT = 6318,
// Have to include resource strings (from scenarios and objects) for the time being now that language is partially working
STR_COUNT = 32768
};

View File

@@ -11,6 +11,7 @@
#include "../Context.h"
#include "../Game.h"
#include "../GameStateSnapshots.h"
#include "../OpenRCT2.h"
#include "../PlatformEnvironment.h"
#include "../actions/LoadOrQuitAction.hpp"
@@ -31,12 +32,16 @@
// This string specifies which version of network stream current build uses.
// It is used for making sure only compatible builds get connected, even within
// single OpenRCT2 version.
#define NETWORK_STREAM_VERSION "24"
#define NETWORK_STREAM_VERSION "25"
#define NETWORK_STREAM_ID OPENRCT2_VERSION "-" NETWORK_STREAM_VERSION
static Peep* _pickup_peep = nullptr;
static int32_t _pickup_peep_old_x = LOCATION_NULL;
// General chunk size is 63 KiB, this can not be any larger because the packet size is encoded
// with uint16_t and needs some spare room for other data in the packet.
static constexpr uint32_t CHUNK_SIZE = 1024 * 63;
#ifndef DISABLE_NETWORK
# include "../Cheats.h"
@@ -137,7 +142,9 @@ public:
void SendPacketToClients(NetworkPacket& packet, bool front = false, bool gameCmd = false);
bool CheckSRAND(uint32_t tick, uint32_t srand0);
bool IsDesynchronised();
void CheckDesynchronizaton();
bool CheckDesynchronizaton();
void RequestStateSnapshot();
NetworkServerState_t GetServerState() const;
void KickPlayer(int32_t playerId);
void SetPassword(const char* password);
void ShutdownClient();
@@ -160,6 +167,8 @@ public:
void AppendServerLog(const std::string& s);
void CloseServerLog();
void Client_Send_RequestGameState(uint32_t tick);
void Client_Send_TOKEN();
void Client_Send_AUTH(
const std::string& name, const std::string& password, const std::string& pubkey, const std::vector<uint8_t>& signature);
@@ -309,7 +318,6 @@ private:
uint16_t listening_port = 0;
SOCKET_STATUS _lastConnectStatus = SOCKET_STATUS_CLOSED;
uint32_t last_ping_sent_time = 0;
uint32_t server_tick = 0;
uint8_t player_id = 0;
std::list<std::unique_ptr<NetworkConnection>> client_connection_list;
std::multiset<GameCommand> game_command_queue;
@@ -317,7 +325,8 @@ private:
std::string _host;
uint16_t _port = 0;
std::string _password;
bool _desynchronised = false;
NetworkServerState_t _serverState;
MemoryStream _serverGameState;
uint32_t server_connect_time = 0;
uint8_t default_group = 0;
uint32_t _commandId;
@@ -336,6 +345,7 @@ private:
private:
std::vector<void (Network::*)(NetworkConnection& connection, NetworkPacket& packet)> client_command_handlers;
std::vector<void (Network::*)(NetworkConnection& connection, NetworkPacket& packet)> server_command_handlers;
void Server_Handle_REQUEST_GAMESTATE(NetworkConnection& connection, NetworkPacket& packet);
void Client_Handle_AUTH(NetworkConnection& connection, NetworkPacket& packet);
void Server_Handle_AUTH(NetworkConnection& connection, NetworkPacket& packet);
void Server_Client_Joined(const char* name, const std::string& keyhash, NetworkConnection& connection);
@@ -361,6 +371,7 @@ private:
void Client_Handle_TOKEN(NetworkConnection& connection, NetworkPacket& packet);
void Server_Handle_TOKEN(NetworkConnection& connection, NetworkPacket& packet);
void Client_Handle_OBJECTS(NetworkConnection& connection, NetworkPacket& packet);
void Client_Handle_GAMESTATE(NetworkConnection& connection, NetworkPacket& packet);
void Server_Handle_OBJECTS(NetworkConnection& connection, NetworkPacket& packet);
uint8_t* save_for_network(size_t& out_size, const std::vector<const ObjectRepositoryItem*>& objects) const;
@@ -397,6 +408,7 @@ Network::Network()
client_command_handlers[NETWORK_COMMAND_GAMEINFO] = &Network::Client_Handle_GAMEINFO;
client_command_handlers[NETWORK_COMMAND_TOKEN] = &Network::Client_Handle_TOKEN;
client_command_handlers[NETWORK_COMMAND_OBJECTS] = &Network::Client_Handle_OBJECTS;
client_command_handlers[NETWORK_COMMAND_GAMESTATE] = &Network::Client_Handle_GAMESTATE;
server_command_handlers.resize(NETWORK_COMMAND_MAX, nullptr);
server_command_handlers[NETWORK_COMMAND_AUTH] = &Network::Server_Handle_AUTH;
server_command_handlers[NETWORK_COMMAND_CHAT] = &Network::Server_Handle_CHAT;
@@ -406,6 +418,7 @@ Network::Network()
server_command_handlers[NETWORK_COMMAND_GAMEINFO] = &Network::Server_Handle_GAMEINFO;
server_command_handlers[NETWORK_COMMAND_TOKEN] = &Network::Server_Handle_TOKEN;
server_command_handlers[NETWORK_COMMAND_OBJECTS] = &Network::Server_Handle_OBJECTS;
server_command_handlers[NETWORK_COMMAND_REQUEST_GAMESTATE] = &Network::Server_Handle_REQUEST_GAMESTATE;
_chat_log_fs << std::unitbuf;
_server_log_fs << std::unitbuf;
@@ -532,6 +545,8 @@ bool Network::BeginClient(const std::string& host, uint16_t port)
_serverConnection = std::make_unique<NetworkConnection>();
_serverConnection->Socket = CreateTcpSocket();
_serverConnection->Socket->ConnectAsync(host, port);
_serverState.gamestateSnapshotsEnabled = false;
status = NETWORK_STATUS_CONNECTING;
_lastConnectStatus = SOCKET_STATUS_CLOSED;
_clientMapLoaded = false;
@@ -664,6 +679,8 @@ bool Network::BeginServer(uint16_t port, const std::string& address)
status = NETWORK_STATUS_CONNECTED;
listening_port = port;
_serverState.gamestateSnapshotsEnabled = gConfigNetwork.desync_debugging;
if (gConfigNetwork.advertise)
{
_advertiser = CreateServerAdvertiser(listening_port);
@@ -703,7 +720,7 @@ int32_t Network::GetAuthStatus()
uint32_t Network::GetServerTick()
{
return server_tick;
return _serverState.tick;
}
uint8_t Network::GetPlayerID()
@@ -997,7 +1014,10 @@ bool Network::CheckSRAND(uint32_t tick, uint32_t srand0)
_serverTickData.erase(itTickData);
if (storedTick.srand0 != srand0)
{
log_info("Srand0 mismatch, client = %08X, server = %08X", srand0, storedTick.srand0);
return false;
}
if (!storedTick.spriteHash.empty())
{
@@ -1005,6 +1025,7 @@ bool Network::CheckSRAND(uint32_t tick, uint32_t srand0)
std::string clientSpriteHash = checksum.ToString();
if (clientSpriteHash != storedTick.spriteHash)
{
log_info("Sprite hash mismatch, client = %s, server = %s", clientSpriteHash.c_str(), storedTick.spriteHash.c_str());
return false;
}
}
@@ -1014,15 +1035,17 @@ bool Network::CheckSRAND(uint32_t tick, uint32_t srand0)
bool Network::IsDesynchronised()
{
return gNetwork._desynchronised;
return _serverState.state == NETWORK_SERVER_STATE_DESYNCED;
}
void Network::CheckDesynchronizaton()
bool Network::CheckDesynchronizaton()
{
// Check synchronisation
if (GetMode() == NETWORK_MODE_CLIENT && !_desynchronised && !CheckSRAND(gCurrentTicks, scenario_rand_state().s0))
if (GetMode() == NETWORK_MODE_CLIENT && _serverState.state != NETWORK_SERVER_STATE_DESYNCED
&& !CheckSRAND(gCurrentTicks, scenario_rand_state().s0))
{
_desynchronised = true;
_serverState.state = NETWORK_SERVER_STATE_DESYNCED;
_serverState.desyncTick = gCurrentTicks;
char str_desync[256];
format_string(str_desync, 256, STR_MULTIPLAYER_DESYNC, nullptr);
@@ -1035,7 +1058,23 @@ void Network::CheckDesynchronizaton()
{
Close();
}
return true;
}
return false;
}
void Network::RequestStateSnapshot()
{
log_info("Requesting game state for tick %u", _serverState.desyncTick);
Client_Send_RequestGameState(_serverState.desyncTick);
}
NetworkServerState_t Network::GetServerState() const
{
return _serverState;
}
void Network::KickPlayer(int32_t playerId)
@@ -1403,6 +1442,20 @@ void Network::CloseServerLog()
_server_log_fs.close();
}
void Network::Client_Send_RequestGameState(uint32_t tick)
{
if (_serverState.gamestateSnapshotsEnabled == false)
{
log_verbose("Server does not store a gamestate history");
return;
}
log_verbose("Requesting gamestate from server for tick %u", tick);
std::unique_ptr<NetworkPacket> packet(NetworkPacket::Allocate());
*packet << (uint32_t)NETWORK_COMMAND_REQUEST_GAMESTATE << tick;
_serverConnection->QueuePacket(std::move(packet));
}
void Network::Client_Send_TOKEN()
{
log_verbose("requesting token");
@@ -1531,7 +1584,7 @@ void Network::Server_Send_MAP(NetworkConnection* connection)
}
return;
}
size_t chunksize = 65000;
size_t chunksize = CHUNK_SIZE;
for (size_t i = 0; i < out_size; i += chunksize)
{
size_t datasize = std::min(chunksize, out_size - i);
@@ -1784,6 +1837,8 @@ void Network::Server_Send_GAMEINFO(NetworkConnection& connection)
json_object_set_new(obj, "provider", jsonProvider);
packet->WriteString(json_dumps(obj, 0));
*packet << _serverState.gamestateSnapshotsEnabled;
json_decref(obj);
# endif
connection.QueuePacket(std::move(packet));
@@ -2313,6 +2368,48 @@ void Network::Client_Handle_TOKEN(NetworkConnection& connection, NetworkPacket&
Client_Send_AUTH(gConfigNetwork.player_name.c_str(), password, pubkey.c_str(), signature);
}
void Network::Server_Handle_REQUEST_GAMESTATE(NetworkConnection& connection, NetworkPacket& packet)
{
uint32_t tick;
packet >> tick;
if (_serverState.gamestateSnapshotsEnabled == false)
{
// Ignore this if this is off.
return;
}
IGameStateSnapshots* snapshots = GetContext()->GetGameStateSnapshots();
const GameStateSnapshot_t* snapshot = snapshots->GetLinkedSnapshot(tick);
if (snapshot)
{
MemoryStream snapshotMemory;
DataSerialiser ds(true, snapshotMemory);
snapshots->SerialiseSnapshot(const_cast<GameStateSnapshot_t&>(*snapshot), ds);
uint32_t bytesSent = 0;
uint32_t length = (uint32_t)snapshotMemory.GetLength();
while (bytesSent < length)
{
uint32_t dataSize = CHUNK_SIZE;
if (bytesSent + dataSize > snapshotMemory.GetLength())
{
dataSize = snapshotMemory.GetLength() - bytesSent;
}
std::unique_ptr<NetworkPacket> gameStateChunk(NetworkPacket::Allocate());
*gameStateChunk << (uint32_t)NETWORK_COMMAND_GAMESTATE << tick << length << bytesSent << dataSize;
gameStateChunk->Write((const uint8_t*)snapshotMemory.GetData() + bytesSent, dataSize);
connection.QueuePacket(std::move(gameStateChunk));
bytesSent += dataSize;
}
}
}
void Network::Client_Handle_AUTH(NetworkConnection& connection, NetworkPacket& packet)
{
uint32_t auth_status;
@@ -2436,6 +2533,73 @@ void Network::Client_Handle_OBJECTS(NetworkConnection& connection, NetworkPacket
Client_Send_OBJECTS(requested_objects);
}
void Network::Client_Handle_GAMESTATE(NetworkConnection& connection, NetworkPacket& packet)
{
uint32_t tick;
uint32_t totalSize;
uint32_t offset;
uint32_t dataSize;
packet >> tick >> totalSize >> offset >> dataSize;
if (offset == 0)
{
// Reset
_serverGameState = MemoryStream();
}
_serverGameState.SetPosition(offset);
const uint8_t* data = packet.Read(dataSize);
_serverGameState.Write(data, dataSize);
log_verbose("Received Game State %.02f%%", ((float)_serverGameState.GetLength() / (float)totalSize) * 100.0f);
if (_serverGameState.GetLength() == totalSize)
{
_serverGameState.SetPosition(0);
DataSerialiser ds(false, _serverGameState);
IGameStateSnapshots* snapshots = GetContext()->GetGameStateSnapshots();
GameStateSnapshot_t& serverSnapshot = snapshots->CreateSnapshot();
snapshots->SerialiseSnapshot(serverSnapshot, ds);
const GameStateSnapshot_t* desyncSnapshot = snapshots->GetLinkedSnapshot(tick);
if (desyncSnapshot)
{
GameStateCompareData_t cmpData = snapshots->Compare(serverSnapshot, *desyncSnapshot);
std::string outputPath = GetContext()->GetPlatformEnvironment()->GetDirectoryPath(
DIRBASE::USER, DIRID::LOG_DESYNCS);
platform_ensure_directory_exists(outputPath.c_str());
char uniqueFileName[128] = {};
snprintf(
uniqueFileName, sizeof(uniqueFileName), "desync_%llu_%u.txt",
(long long unsigned)platform_get_datetime_now_utc(), tick);
std::string outputFile = Path::Combine(outputPath, uniqueFileName);
if (snapshots->LogCompareDataToFile(outputFile, cmpData))
{
log_info("Wrote desync report to '%s'", outputFile.c_str());
uint8_t args[32]{};
set_format_arg_on(args, 0, char*, uniqueFileName);
char str_desync[1024];
format_string(str_desync, sizeof(str_desync), STR_DESYNC_REPORT, args);
auto intent = Intent(WC_NETWORK_STATUS);
intent.putExtra(INTENT_EXTRA_MESSAGE, std::string{ str_desync });
context_open_intent(&intent);
}
}
}
}
void Network::Server_Handle_OBJECTS(NetworkConnection& connection, NetworkPacket& packet)
{
uint32_t size;
@@ -2644,9 +2808,10 @@ void Network::Client_Handle_MAP([[maybe_unused]] NetworkConnection& connection,
game_load_init();
game_command_queue.clear();
_serverTickData.clear();
server_tick = gCurrentTicks;
_serverState.tick = gCurrentTicks;
_serverTickData.clear();
// window_network_status_open("Loaded new map from network");
_desynchronised = false;
_serverState.state = NETWORK_SERVER_STATE_OK;
_clientMapLoaded = true;
gFirstTimeSaving = true;
@@ -2977,11 +3142,13 @@ void Network::Client_Handle_TICK([[maybe_unused]] NetworkConnection& connection,
{
uint32_t srand0;
uint32_t flags;
packet >> server_tick >> srand0 >> flags;
uint32_t serverTick;
packet >> serverTick >> srand0 >> flags;
ServerTickData_t tickData;
tickData.srand0 = srand0;
tickData.tick = server_tick;
tickData.tick = serverTick;
if (flags & NETWORK_TICK_FLAG_CHECKSUMS)
{
@@ -2998,7 +3165,8 @@ void Network::Client_Handle_TICK([[maybe_unused]] NetworkConnection& connection,
_serverTickData.erase(_serverTickData.begin());
}
_serverTickData.emplace(server_tick, tickData);
_serverState.tick = serverTick;
_serverTickData.emplace(serverTick, tickData);
}
void Network::Client_Handle_PLAYERINFO([[maybe_unused]] NetworkConnection& connection, NetworkPacket& packet)
@@ -3154,6 +3322,7 @@ static std::string json_stdstring_value(const json_t* string)
void Network::Client_Handle_GAMEINFO([[maybe_unused]] NetworkConnection& connection, NetworkPacket& packet)
{
const char* jsonString = packet.ReadString();
packet >> _serverState.gamestateSnapshotsEnabled;
json_error_t error;
json_t* root = json_loads(jsonString, 0, &error);
@@ -3234,11 +3403,16 @@ bool network_is_desynchronised()
return gNetwork.IsDesynchronised();
}
void network_check_desynchronization()
bool network_check_desynchronisation()
{
return gNetwork.CheckDesynchronizaton();
}
void network_request_gamestate_snapshot()
{
return gNetwork.RequestStateSnapshot();
}
void network_send_tick()
{
gNetwork.Server_Send_TICK();
@@ -3995,6 +4169,16 @@ NetworkStats_t network_get_stats()
return gNetwork.GetStats();
}
NetworkServerState_t network_get_server_state()
{
return gNetwork.GetServerState();
}
bool network_gamestate_snapshots_enabled()
{
return network_get_server_state().gamestateSnapshotsEnabled;
}
#else
int32_t network_get_mode()
{
@@ -4022,7 +4206,15 @@ bool network_is_desynchronised()
{
return false;
}
void network_check_desynchronization()
bool network_gamestate_snapshots_enabled()
{
return false;
}
bool network_check_desynchronisation()
{
return false;
}
void network_request_gamestate_snapshot()
{
}
void network_enqueue_game_action(const GameAction* action)
@@ -4240,4 +4432,8 @@ NetworkStats_t network_get_stats()
{
return NetworkStats_t{};
}
NetworkServerState_t network_get_server_state()
{
return NetworkServerState_t{};
}
#endif /* DISABLE_NETWORK */

View File

@@ -66,12 +66,29 @@ enum NETWORK_COMMAND
NETWORK_COMMAND_OBJECTS,
NETWORK_COMMAND_GAME_ACTION,
NETWORK_COMMAND_PLAYERINFO,
NETWORK_COMMAND_REQUEST_GAMESTATE,
NETWORK_COMMAND_GAMESTATE,
NETWORK_COMMAND_MAX,
NETWORK_COMMAND_INVALID = -1
};
static_assert(NETWORK_COMMAND::NETWORK_COMMAND_GAMEINFO == 9, "Master server expects this to be 9");
enum NETWORK_SERVER_STATE
{
NETWORK_SERVER_STATE_OK,
NETWORK_SERVER_STATE_DESYNCED,
};
struct NetworkServerState_t
{
NETWORK_SERVER_STATE state = NETWORK_SERVER_STATE_OK;
uint32_t desyncTick = 0;
uint32_t tick = 0;
uint32_t srand0 = 0;
bool gamestateSnapshotsEnabled = false;
};
// Structure is used for networking specific fields with meaning,
// this structure can be used in combination with DataSerialiser
// to provide extra details with template specialization.

View File

@@ -39,8 +39,10 @@ int32_t network_begin_server(int32_t port, const std::string& address);
int32_t network_get_mode();
int32_t network_get_status();
bool network_is_desynchronised();
void network_check_desynchronization();
bool network_check_desynchronisation();
void network_request_gamestate_snapshot();
void network_send_tick();
bool network_gamestate_snapshots_enabled();
void network_update();
void network_process_pending();
void network_flush();
@@ -106,3 +108,4 @@ const utf8* network_get_server_provider_website();
std::string network_get_version();
NetworkStats_t network_get_stats();
NetworkServerState_t network_get_server_state();