forked from 0ad/0ad
# Add out-of-sync multiplayer checks.
Change CStr8 serialization to allow \0 in strings. This was SVN commit r7565.
This commit is contained in:
parent
4e661a205d
commit
94e5d88169
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user