# Add out-of-sync multiplayer checks.

Change CStr8 serialization to allow \0 in strings.

This was SVN commit r7565.
This commit is contained in:
Ykkrosh 2010-05-22 14:05:56 +00:00
parent 4e661a205d
commit 94e5d88169
8 changed files with 221 additions and 21 deletions

View File

@ -191,6 +191,7 @@ bool CNetClient::SetupSession( CNetSession* pSession )
pSession->AddTransition( NCS_INGAME, ( uint )NMT_CHAT, NCS_INGAME, (void*)&OnChat, pContext );
pSession->AddTransition( NCS_INGAME, ( uint )NMT_SIMULATION_COMMAND, NCS_INGAME, (void*)&OnInGame, pContext );
pSession->AddTransition( NCS_INGAME, ( uint )NMT_SYNC_ERROR, NSS_INGAME, (void*)&OnInGame, pContext );
pSession->AddTransition( NCS_INGAME, ( uint )NMT_END_COMMAND_BATCH, NCS_INGAME, (void*)&OnInGame, pContext );
// Set first state
@ -486,10 +487,13 @@ bool CNetClient::OnInGame( void *pContext, CFsmEvent* pEvent )
{
CSimulationMessage* simMessage = static_cast<CSimulationMessage*> (pMessage);
pClient->m_ClientTurnManager->OnSimulationMessage(simMessage);
return true;
}
if ( pMessage->GetType() == NMT_END_COMMAND_BATCH )
else if (pMessage->GetType() == NMT_SYNC_ERROR)
{
CSyncErrorMessage* syncMessage = static_cast<CSyncErrorMessage*> (pMessage);
pClient->m_ClientTurnManager->OnSyncError(syncMessage->m_Turn, syncMessage->m_HashExpected);
}
else if ( pMessage->GetType() == NMT_END_COMMAND_BATCH )
{
CEndCommandBatchMessage* pMessage = ( CEndCommandBatchMessage* )pEvent->GetParamRef();
if ( !pMessage ) return false;

View File

@ -208,7 +208,15 @@ CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
case NMT_END_COMMAND_BATCH:
pNewMessage = new CEndCommandBatchMessage;
break;
case NMT_SYNC_CHECK:
pNewMessage = new CSyncCheckMessage;
break;
case NMT_SYNC_ERROR:
pNewMessage = new CSyncErrorMessage;
break;
case NMT_CHAT:
pNewMessage = new CChatMessage;
break;

View File

@ -65,6 +65,8 @@ enum NetMessageType
NMT_FILE_PROGRESS,
NMT_GAME_START,
NMT_END_COMMAND_BATCH, // In-game stage
NMT_SYNC_CHECK,
NMT_SYNC_ERROR,
NMT_SIMULATION_COMMAND,
NMT_LAST // Last message in the list
};
@ -180,6 +182,16 @@ START_NMT_CLASS_(EndCommandBatch, NMT_END_COMMAND_BATCH)
NMT_FIELD_INT(m_TurnLength, u32, 2)
END_NMT_CLASS()
START_NMT_CLASS_(SyncCheck, NMT_SYNC_CHECK)
NMT_FIELD_INT(m_Turn, u32, 4)
NMT_FIELD(CStr, m_Hash)
END_NMT_CLASS()
START_NMT_CLASS_(SyncError, NMT_SYNC_ERROR)
NMT_FIELD_INT(m_Turn, u32, 4)
NMT_FIELD(CStr, m_HashExpected)
END_NMT_CLASS()
END_NMTS()
#else

View File

@ -143,6 +143,7 @@ bool CNetServer::SetupSession( CNetSession* pSession )
pSession->AddTransition( NSS_INGAME, ( uint )NMT_ERROR, NSS_INGAME, (void*)&OnError, pContext );
pSession->AddTransition( NSS_INGAME, ( uint )NMT_CHAT, NSS_INGAME, (void*)&OnChat, pContext );
pSession->AddTransition( NSS_INGAME, ( uint )NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnInGame, pContext );
pSession->AddTransition( NSS_INGAME, ( uint )NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnInGame, pContext );
pSession->AddTransition( NSS_INGAME, ( uint )NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnInGame, pContext );
// Set first state
@ -511,10 +512,13 @@ bool CNetServer::OnInGame( void* pContext, CFsmEvent* pEvent )
{
CSimulationMessage* simMessage = static_cast<CSimulationMessage*> (pMessage);
pServer->m_ServerTurnManager->OnSimulationMessage(simMessage);
return true;
}
if ( pMessage->GetType() == NMT_END_COMMAND_BATCH )
else if (pMessage->GetType() == NMT_SYNC_CHECK)
{
CSyncCheckMessage* syncMessage = static_cast<CSyncCheckMessage*> (pMessage);
pServer->m_ServerTurnManager->NotifyFinishedClientUpdate(pSession->GetID(), syncMessage->m_Turn, syncMessage->m_Hash);
}
else if ( pMessage->GetType() == NMT_END_COMMAND_BATCH )
{
CEndCommandBatchMessage* endMessage = static_cast<CEndCommandBatchMessage*> (pMessage);
pServer->m_ServerTurnManager->NotifyFinishedClientCommands(pSession->GetID(), endMessage->m_Turn);

View File

@ -21,7 +21,9 @@
#include "NetServer.h"
#include "NetClient.h"
#include "gui/GUIManager.h"
#include "maths/MathUtil.h"
#include "ps/Profile.h"
#include "simulation2/Simulation2.h"
static const int TURN_LENGTH = 200; // TODO: this should be a variable controlled by the server depending on latency
@ -30,9 +32,18 @@ static const int COMMAND_DELAY = 2;
//#define NETTURN_LOG debug_printf
static std::string Hexify(const std::string& s)
{
std::stringstream str;
str << std::hex;
for (size_t i = 0; i < s.size(); ++i)
str << std::setfill('0') << std::setw(2) << (int)(unsigned char)s[i];
return str.str();
}
CNetTurnManager::CNetTurnManager(CSimulation2& simulation, int playerId, int clientId) :
m_Simulation2(simulation), m_CurrentTurn(0), m_ReadyTurn(1), m_DeltaTime(0),
m_PlayerId(playerId), m_ClientId(clientId)
m_PlayerId(playerId), m_ClientId(clientId), m_HasSyncError(false)
{
// When we are on turn n, we schedule new commands for n+2.
// We know that all other clients have finished scheduling commands for n (else we couldn't have got here).
@ -59,7 +70,8 @@ bool CNetTurnManager::Update(float frameLength)
if (m_ReadyTurn > m_CurrentTurn)
{
NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);
m_CurrentTurn += 1;
m_CurrentTurn += 1; // increase the turn number now, so Update can send new commands for a subsequent turn
// Put all the client commands into a single list, in a globally consistent order
std::vector<SimulationCommand> commands;
@ -76,7 +88,13 @@ bool CNetTurnManager::Update(float frameLength)
m_Simulation2.Update(TURN_LENGTH, commands);
// TODO: Compute state hash, send SyncCheck message
{
PROFILE("state hash check");
std::string hash;
bool ok = m_Simulation2.ComputeStateHash(hash);
debug_assert(ok);
NotifyFinishedUpdate(m_CurrentTurn, hash);
}
// Set the time for the next turn update
m_DeltaTime -= TURN_LENGTH / 1000.f;
@ -99,6 +117,33 @@ bool CNetTurnManager::Update(float frameLength)
}
}
void CNetTurnManager::OnSyncError(u32 turn, const std::string& expectedHash)
{
#ifdef NETTURN_LOG
NETTURN_LOG(L"OnSyncError(%d, %s)\n", turn, Hexify(expectedHash).c_str());
#endif
// Only complain the first time
if (m_HasSyncError)
return;
m_HasSyncError = true;
std::string hash;
bool ok = m_Simulation2.ComputeStateHash(hash);
debug_assert(ok);
fs::wpath path (psLogDir()/L"oos_dump.txt");
std::ofstream file (path.external_file_string().c_str(), std::ofstream::out | std::ofstream::trunc);
m_Simulation2.DumpDebugState(file);
file.close();
std::wstringstream msg;
msg << L"Out of sync on turn " << turn << L": expected hash " << CStrW(Hexify(expectedHash)) << L"\n\n";
msg << L"Current state: turn " << m_CurrentTurn << L", hash " << CStrW(Hexify(hash)) << L"\n\n";
msg << L"Dumping current state to " << path;
g_GUI->DisplayMessageBox(600, 350, L"Sync error", msg.str());
}
void CNetTurnManager::Interpolate(float frameLength)
{
float offset = clamp(m_DeltaTime / (TURN_LENGTH / 1000.f) + 1.0, 0.0, 1.0);
@ -155,6 +200,7 @@ void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
NETTURN_LOG(L"NotifyFinishedOwnCommands(%d)\n", turn);
#endif
// Send message to the server
CEndCommandBatchMessage msg;
msg.m_TurnLength = TURN_LENGTH;
msg.m_Turn = turn;
@ -162,6 +208,20 @@ void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
m_NetClient.SendMessage(session, &msg);
}
void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn, const std::string& hash)
{
#ifdef NETTURN_LOG
NETTURN_LOG(L"NotifyFinishedUpdate(%d, %s)\n", turn, Hexify(hash).c_str());
#endif
// Send message to the server
CSyncCheckMessage msg;
msg.m_Turn = turn;
msg.m_Hash = hash;
CNetSession* session = m_NetClient.GetSession(0);
m_NetClient.SendMessage(session, &msg);
}
void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg)
{
// Command received from the server - store it for later execution
@ -193,6 +253,15 @@ void CNetServerTurnManager::NotifyFinishedOwnCommands(u32 turn)
NotifyFinishedClientCommands(m_ClientId, turn);
}
void CNetServerTurnManager::NotifyFinishedUpdate(u32 turn, const std::string& hash)
{
#ifdef NETTURN_LOG
NETTURN_LOG(L"NotifyFinishedUpdate(%d, %s)\n", turn, Hexify(hash).c_str());
#endif
NotifyFinishedClientUpdate(m_ClientId, turn, hash);
}
void CNetServerTurnManager::NotifyFinishedClientCommands(int client, u32 turn)
{
#ifdef NETTURN_LOG
@ -226,10 +295,63 @@ void CNetServerTurnManager::NotifyFinishedClientCommands(int client, u32 turn)
FinishedAllCommands(m_ReadyTurn + 1);
}
void CNetServerTurnManager::NotifyFinishedClientUpdate(int client, u32 turn, const std::string& hash)
{
// Clients must advance one turn at a time
debug_assert(turn == m_ClientsSimulated[client] + 1);
m_ClientsSimulated[client] = turn;
m_ClientStateHashes[turn][client] = hash;
// Find the newest turn which we know all clients have simulated
u32 newest = std::numeric_limits<u32>::max();
for (std::map<int, u32>::iterator it = m_ClientsSimulated.begin(); it != m_ClientsSimulated.end(); ++it)
{
if (it->second < newest)
newest = it->second;
}
// For every set of state hashes that all clients have simulated, check for OOS
for (std::map<u32, std::map<int, std::string> >::iterator it = m_ClientStateHashes.begin(); it != m_ClientStateHashes.end(); ++it)
{
if (it->first > newest)
break;
// Assume the host is correct (maybe we should choose the most common instead to help debugging)
std::string expected = it->second[m_ClientId];
for (std::map<int, std::string>::iterator cit = it->second.begin(); cit != it->second.end(); ++cit)
{
#ifdef NETTURN_LOG
NETTURN_LOG(L"sync check %d: %d = %s\n", it->first, cit->first, Hexify(cit->second).c_str());
#endif
if (cit->second != expected)
{
// Oh no, out of sync
// Tell everyone about it
CSyncErrorMessage msg;
msg.m_Turn = it->first;
msg.m_HashExpected = expected;
m_NetServer.Broadcast(&msg);
// Process it ourselves
OnSyncError(it->first, expected);
break;
}
}
}
// Delete the saved hashes for all turns that we've already verified
m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
}
void CNetServerTurnManager::InitialiseClient(int client)
{
debug_assert(m_ClientsReady.find(client) == m_ClientsReady.end());
m_ClientsReady[client] = 1;
m_ClientsSimulated[client] = 0;
// TODO: do we need some kind of UninitialiseClient in case they leave?
}
@ -259,6 +381,10 @@ void CNetLocalTurnManager::NotifyFinishedOwnCommands(u32 turn)
FinishedAllCommands(turn);
}
void CNetLocalTurnManager::NotifyFinishedUpdate(u32 UNUSED(turn), const std::string& UNUSED(hash))
{
}
void CNetLocalTurnManager::OnSimulationMessage(CSimulationMessage* UNUSED(msg))
{
debug_warn(L"This should never be called");

View File

@ -75,6 +75,11 @@ public:
*/
virtual void OnSimulationMessage(CSimulationMessage* msg) = 0;
/**
* Called when there has been an out-of-sync error.
*/
virtual void OnSyncError(u32 turn, const std::string& expectedHash);
/**
* Called by simulation code, to add a new command to be distributed to all clients and executed soon.
*/
@ -97,6 +102,11 @@ protected:
*/
virtual void NotifyFinishedOwnCommands(u32 turn) = 0;
/**
* Called when this client has finished a simulation update, with the current state hash.
*/
virtual void NotifyFinishedUpdate(u32 turn, const std::string& hash) = 0;
CSimulation2& m_Simulation2;
/// The turn that we have most recently executed
@ -113,6 +123,8 @@ protected:
/// Time remaining until we ought to execute the next turn
float m_DeltaTime;
bool m_HasSyncError;
};
class CNetClientTurnManager : public CNetTurnManager
@ -130,6 +142,8 @@ public:
protected:
virtual void NotifyFinishedOwnCommands(u32 turn);
virtual void NotifyFinishedUpdate(u32 turn, const std::string& hash);
CNetClient& m_NetClient;
};
@ -147,14 +161,25 @@ public:
void NotifyFinishedClientCommands(int client, u32 turn);
void NotifyFinishedClientUpdate(int client, u32 turn, const std::string& hash);
void InitialiseClient(int client);
protected:
virtual void NotifyFinishedOwnCommands(u32 turn);
// Client ID -> ready turn number (the latest turn for which all commands have been received)
virtual void NotifyFinishedUpdate(u32 turn, const std::string& hash);
// Client ID -> ready turn number (the latest turn for which all commands have been received from that client)
std::map<int, u32> m_ClientsReady;
// Client ID -> last known simulated turn number (for which we have the state hash)
// (the client has reached the start of this turn, not done the update for it yet)
std::map<int, u32> m_ClientsSimulated;
// Map of turn -> {Client ID -> state hash}; old indexes <= min(m_ClientsSimulated) are deleted
std::map<u32, std::map<int, std::string> > m_ClientStateHashes;
CNetServer& m_NetServer;
};
@ -172,6 +197,8 @@ public:
protected:
virtual void NotifyFinishedOwnCommands(u32 turn);
virtual void NotifyFinishedUpdate(u32 turn, const std::string& hash);
};
#endif // INCLUDED_NETTURNMANAGER

View File

@ -508,24 +508,18 @@ u8* CStr8::Serialize(u8* buffer) const
size_t i = 0;
for (i = 0; i < len; i++)
buffer[i] = (*this)[i];
buffer[i] = 0;
return buffer+len+1;
return buffer+len;
}
const u8* CStr8::Deserialize(const u8* buffer, const u8* bufferend)
{
const u8 *strend = buffer;
while (strend < bufferend && *strend) strend++;
if (strend >= bufferend) return NULL;
*this = std::string(buffer, strend);
return strend+1;
*this = std::string(buffer, bufferend);
return bufferend;
}
size_t CStr::GetSerializedLength() const
{
return size_t(length() + 1);
return length();
}
#endif // _UNICODE

View File

@ -74,4 +74,29 @@ public:
TS_ASSERT_SAME_DATA(str_utf8to16.data(), str_utf16.data(), str_utf16.length()*sizeof(wchar_t));
}
}
template <typename T>
void roundtrip(const T& str)
{
size_t len = str.GetSerializedLength();
u8* buf = new u8[len+1];
buf[len] = '!';
TS_ASSERT_EQUALS(str.Serialize(buf) - (buf+len), 0);
TS_ASSERT_EQUALS(buf[len], '!');
T str2;
TS_ASSERT_EQUALS(str2.Deserialize(buf, buf+len) - (buf+len), 0);
TS_ASSERT_EQUALS(str2.length(), str.length());
TS_ASSERT_EQUALS(str2, str);
}
void test_serialize_8()
{
CStr8 str1("hello");
roundtrip(str1);
CStr8 str2 = str1;
str2[3] = '\0';
roundtrip(str2);
}
};