From c684c211a2f444fc653e4dbe43506db57fa10523 Mon Sep 17 00:00:00 2001 From: Ykkrosh Date: Thu, 20 May 2010 00:59:01 +0000 Subject: [PATCH] # Basic experimental multiplayer integration with new simulation system. Add new turn manager that should be more correct and potentially more efficient. Remove evil CNetServer/CNetClient multiple inheritance of CTurnManager. Add multiplayer autostart. Various minor cleanups. This was SVN commit r7551. --- .../data/mods/public/gui/pregame/mainmenu.xml | 1 - source/gui/GUIManager.cpp | 5 + source/gui/GUIManager.h | 23 +- source/gui/scripting/ScriptFunctions.cpp | 9 +- source/main.cpp | 11 +- source/network/NMTCreator.h | 77 +---- source/network/NetClient.cpp | 139 ++++++--- source/network/NetClient.h | 27 +- source/network/NetMessage.cpp | 15 +- source/network/NetMessage.h | 34 ++- source/network/NetMessageSim.cpp | 126 ++++++++ source/network/NetMessages.h | 5 +- source/network/NetServer.cpp | 119 +++++--- source/network/NetServer.h | 35 +-- source/network/NetSession.cpp | 50 ++-- source/network/NetSession.h | 16 +- source/network/NetTurnManager.cpp | 265 +++++++++++++++++ source/network/NetTurnManager.h | 177 +++++++++++ source/ps/Game.cpp | 52 ++-- source/ps/Game.h | 16 + source/ps/GameSetup/Config.cpp | 3 - source/ps/GameSetup/Config.h | 3 - source/ps/GameSetup/GameSetup.cpp | 281 ++++++++++++------ source/scripting/ScriptingHost.cpp | 9 - source/scripting/ScriptingHost.h | 2 - source/scriptinterface/ScriptTypes.h | 1 + source/simulation/TurnManager.cpp | 8 +- source/simulation/TurnManager.h | 4 +- source/simulation2/Simulation2.cpp | 110 ++++--- source/simulation2/Simulation2.h | 11 +- .../components/CCmpCommandQueue.cpp | 52 ++-- .../components/ICmpCommandQueue.cpp | 5 +- .../simulation2/components/ICmpCommandQueue.h | 31 +- .../components/tests/test_CommandQueue.h | 14 +- .../simulation2/helpers/SimulationCommand.h | 32 ++ .../simulation2/serialization/ISerializer.cpp | 5 + .../simulation2/serialization/ISerializer.h | 6 + .../serialization/StdDeserializer.cpp | 5 + .../serialization/StdDeserializer.h | 1 + .../tools/atlas/GameInterface/ActorViewer.cpp | 4 +- source/tools/atlas/GameInterface/View.cpp | 4 +- 41 files changed, 1330 insertions(+), 463 deletions(-) create mode 100644 source/network/NetMessageSim.cpp create mode 100644 source/network/NetTurnManager.cpp create mode 100644 source/network/NetTurnManager.h create mode 100644 source/simulation2/helpers/SimulationCommand.h diff --git a/binaries/data/mods/public/gui/pregame/mainmenu.xml b/binaries/data/mods/public/gui/pregame/mainmenu.xml index f6c22da8cc..41c0a53d64 100644 --- a/binaries/data/mods/public/gui/pregame/mainmenu.xml +++ b/binaries/data/mods/public/gui/pregame/mainmenu.xml @@ -143,7 +143,6 @@ Watch for updates or get involved in the development: http://wildfiregames.com/0 sprite_over="pgMultiPlayerOver" sprite_disabled="pgMultiPlayerDisabled" tooltip="Tired of playing with yourself? Fight against one or more human players in a multiplayer game." - enabled="false" > GetLocalPlayer()) - player = g_Game->GetLocalPlayer()->GetPlayerID(); - jsval cmd2 = sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), cmd.get()); - queue->PushClientCommand(player, cmd2); - // TODO: This shouldn't call Push, it should send the message to the network layer - // (which should propagate it across the network and eventually call Push on the - // appropriate turn) + queue->PostNetworkCommand(cmd2); } std::vector PickEntitiesAtPoint(void* UNUSED(cbdata), int x, int y) diff --git a/source/main.cpp b/source/main.cpp index 9f14d4ff34..7080d295fe 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -257,10 +257,15 @@ static void Frame() PROFILE_START("input"); MICROLOG(L"input"); PumpEvents(); - //g_SessionManager.Poll(); - if ( CNetHost::IsInitialised() ) - CNetHost::GetSingleton().Poll(); PROFILE_END("input"); + //g_SessionManager.Poll(); + + PROFILE_START("network poll"); + if (g_NetServer) + g_NetServer->Poll(); + if (g_NetClient) + g_NetClient->Poll(); + PROFILE_END("network poll"); ogl_WarnIfError(); diff --git a/source/network/NMTCreator.h b/source/network/NMTCreator.h index c5d46b97e8..f4ad582751 100644 --- a/source/network/NMTCreator.h +++ b/source/network/NMTCreator.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -218,14 +218,6 @@ u8 *_nm::Serialize(u8 *buffer) const \ #define START_NMT_CLASS(_nm, _tp) \ START_NMT_CLASS_DERIVED(CNetMessage, _nm, _tp) #define START_NMT_CLASS_DERIVED(_base, _nm, _tp) \ -CNetMessage *Deserialize##_nm(const u8 *buffer, size_t length) \ -{ \ - _nm *ret=new _nm(); \ - if (ret->Deserialize(buffer, buffer+length)) \ - return ret; \ - else \ - { delete ret; return NULL; } \ -} \ const u8 *_nm::Deserialize(const u8 *pos, const u8 *end) \ { \ pos=_base::Deserialize(pos, end); \ @@ -262,46 +254,7 @@ const u8 *_nm::Deserialize(const u8 *pos, const u8 *end) \ #undef NMT_CREATOR_PASS_DESERIALIZE /*************************************************************************/ -// Pass 5, Deserializer Registration - -#define NMT_CREATOR_PASS_REGISTRATION - -//#define START_NMTS() SNetMessageDeserializerRegistration g_DeserializerRegistrations[] = { -//#define END_NMTS() { NMT_INVALID, NULL } }; -#define START_NMTS() -#define END_NMTS() - -#define START_NMT_CLASS(_nm, _tp) \ - START_NMT_CLASS_DERIVED(CNetMessage, _nm, _tp) - -/* -#define START_NMT_CLASS_DERIVED(_base, _nm, _tp) \ - { _tp, Deserialize##_nm }, -*/ - -#define START_NMT_CLASS_DERIVED(_base, _nm, _tp) - -#define NMT_START_ARRAY(_nm) - -#define NMT_END_ARRAY() - -#define NMT_FIELD_INT(_nm, _hosttp, _netsz) - -#define NMT_FIELD(_tp, _nm) - -#define END_NMT_CLASS() - -#include "NMTCreator.h" - -#undef NMT_CREATOR_PASS_REGISTRATION - -/*************************************************************************/ -// Pass 6, String Representation - -#define _T(s) s -// PT: I'm not sure whether this is correct - it appears that this code -// relied on CStr.h leaving _T defined in ASCII mode, so I'm not sure -// why the macro is actually used at all... +// Pass 5, String Representation #define START_NMTS() #define END_NMTS() @@ -309,8 +262,8 @@ const u8 *_nm::Deserialize(const u8 *pos, const u8 *end) \ #define START_NMT_CLASS(_nm, _tp) \ CStr _nm::ToString() const \ { \ - CStr ret=#_nm _T(" { "); \ - return ret + ToStringRaw() + _T(" }"); \ + CStr ret=#_nm " { "; \ + return ret + ToStringRaw() + " }"; \ } \ CStr _nm::ToStringRaw() const \ { \ @@ -321,39 +274,39 @@ CStr _nm::ToStringRaw() const \ #define START_NMT_CLASS_DERIVED(_base, _nm, _tp) \ CStr _nm::ToString() const \ { \ - CStr ret=#_nm _T(" { "); \ - return ret + ToStringRaw() + _T(" }"); \ + CStr ret=#_nm " { "; \ + return ret + ToStringRaw() + " }"; \ } \ CStr _nm::ToStringRaw() const \ { \ - CStr ret=_base::ToStringRaw() + _T(", "); \ + CStr ret=_base::ToStringRaw() + ", "; \ const _nm *thiz=this;\ UNUSED2(thiz); // preempt any "unused" warning #define NMT_START_ARRAY(_nm) \ - ret+=#_nm _T(": { "); \ + ret+=#_nm ": { "; \ std::vector < ARRAY_STRUCT_PREFIX(_nm) >::const_iterator it=_nm.begin(); \ while (it != _nm.end()) \ { \ - ret+=_T(" { "); \ + ret+=" { "; \ const ARRAY_STRUCT_PREFIX(_nm) *thiz=&*it;\ UNUSED2(thiz); // preempt any "unused" warning #define NMT_END_ARRAY() \ ++it; \ - ret=ret.substr(0, ret.length()-2)+_T(" }, "); \ + ret=ret.substr(0, ret.length()-2)+" }, "; \ } \ - ret=ret.substr(0, ret.length()-2)+_T(" }, "); + ret=ret.substr(0, ret.length()-2)+" }, "; #define NMT_FIELD_INT(_nm, _hosttp, _netsz) \ - ret += #_nm _T(": "); \ + ret += #_nm ": "; \ ret += NetMessageStringConvert(thiz->_nm); \ - ret += _T(", "); + ret += ", "; #define NMT_FIELD(_tp, _nm) \ - ret += #_nm _T(": "); \ + ret += #_nm ": "; \ ret += NetMessageStringConvert(thiz->_nm); \ - ret += _T(", "); + ret += ", "; #define END_NMT_CLASS() \ return ret.substr(0, ret.length()-2); \ diff --git a/source/network/NetClient.cpp b/source/network/NetClient.cpp index 589eb9a41d..a5ea81df21 100644 --- a/source/network/NetClient.cpp +++ b/source/network/NetClient.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -39,13 +39,24 @@ #include "ps/Globals.h" #include "ps/GameAttributes.h" #include "simulation/Simulation.h" +#include "simulation2/Simulation2.h" // DECLARATIONS #define LOG_CATEGORY L"net" -CNetClient *g_NetClient=NULL; -extern int fps; +CNetClient *g_NetClient = NULL; + +class CClientTurnManager : public CTurnManager +{ +public: + CClientTurnManager(CNetClient& client) : m_Client(client) { } + virtual void QueueLocalCommand(CNetMessage* pMessage); + virtual void NewTurn(); + virtual bool NewTurnReady() { return m_Client.m_TurnPending; } +private: + CNetClient& m_Client; +}; //----------------------------------------------------------------------------- // Name: CServerPlayer() @@ -84,6 +95,9 @@ void CServerPlayer::ScriptingInit( void ) CNetClient::CNetClient( CGame* pGame, CGameAttributes* pGameAttribs ) : m_JsPlayers( &m_Players ) { + m_TurnManager = new CClientTurnManager(*this); + m_ClientTurnManager = NULL; + m_pLocalPlayerSlot = NULL; m_pGame = pGame; m_pGameAttributes = pGameAttribs; @@ -91,7 +105,8 @@ CNetClient::CNetClient( CGame* pGame, CGameAttributes* pGameAttribs ) //ONCE( ScriptingInit(); ); - m_pGame->GetSimulation()->SetTurnManager(this); + if (!g_UseSimulation2) + m_pGame->GetSimulation()->SetTurnManager(m_TurnManager); g_ScriptingHost.SetGlobal("g_NetClient", OBJECT_TO_JSVAL(GetScript())); } @@ -173,7 +188,7 @@ bool CNetClient::SetupSession( CNetSession* pSession ) // Validate parameters if ( !pSession ) return false; - FsmActionCtx* pContext = new FsmActionCtx; + FsmActionCtx* pContext = new FsmActionCtx; // XXX: this gets leaked if ( !pContext ) return false; pContext->pHost = this; @@ -193,7 +208,7 @@ bool CNetClient::SetupSession( CNetSession* pSession ) pSession->AddTransition( NCS_PREGAME, ( uint )NMT_ASSIGN_PLAYER_SLOT, NCS_PREGAME, (void*)&OnPreGame, pContext ); pSession->AddTransition( NCS_PREGAME, ( uint )NMT_PLAYER_CONFIG, NCS_PREGAME, (void*)&OnPreGame, pContext ); pSession->AddTransition( NCS_PREGAME, ( uint )NMT_PLAYER_JOIN, NCS_PREGAME, (void*)&OnPlayerJoin, pContext ); - pSession->AddTransition( NCS_PREGAME, ( uint )NMT_GAME_START, NCS_INGAME, (void*)&OnStartGame, pContext ); + pSession->AddTransition( NCS_PREGAME, ( uint )NMT_GAME_START, NCS_INGAME, (void*)&OnStartGame_, pContext ); pSession->AddTransition( NCS_INGAME, ( uint )NMT_CHAT, NCS_INGAME, (void*)&OnChat, pContext ); pSession->AddTransition( NCS_INGAME, ( uint )NMT_GOTO, NCS_INGAME, (void*)&OnInGame, pContext ); @@ -208,6 +223,7 @@ bool CNetClient::SetupSession( CNetSession* pSession ) pSession->AddTransition( NCS_INGAME, ( uint )NMT_NOTIFY_REQUEST, NCS_INGAME, (void*)&OnInGame, pContext ); pSession->AddTransition( NCS_INGAME, ( uint )NMT_FORMATION_GOTO, NCS_INGAME, (void*)&OnInGame, pContext ); pSession->AddTransition( NCS_INGAME, ( uint )NMT_FORMATION_CONTACT_ACTION, NCS_INGAME, (void*)&OnInGame, pContext ); + pSession->AddTransition( NCS_INGAME, ( uint )NMT_SIMULATION_COMMAND, NCS_INGAME, (void*)&OnInGame, pContext ); pSession->AddTransition( NCS_INGAME, ( uint )NMT_END_COMMAND_BATCH, NCS_INGAME, (void*)&OnInGame, pContext ); // Set first state @@ -253,7 +269,7 @@ bool CNetClient::OnError( void* pContext, CFsmEvent* pEvent ) if ( pEvent->GetType() != (uint)NMT_ERROR ) return true; CNetClient* pClient = ( CNetClient* )( ( FsmActionCtx* )pContext )->pHost; - assert( pClient ); + debug_assert( pClient ); CErrorMessage* pMessage = ( CErrorMessage* )pEvent->GetParamRef(); if ( pMessage ) @@ -283,7 +299,7 @@ bool CNetClient::OnPlayerJoin( void* pContext, CFsmEvent* pEvent ) if ( pEvent->GetType() != NMT_PLAYER_JOIN ) return true; CNetClient* pClient = ( CNetClient* )( ( FsmActionCtx* )pContext )->pHost; - assert( pClient ); + debug_assert( pClient ); CPlayerJoinMessage* pMessage = ( CPlayerJoinMessage* )pEvent->GetParamRef(); if ( pMessage ) @@ -292,17 +308,26 @@ bool CNetClient::OnPlayerJoin( void* pContext, CFsmEvent* pEvent ) { pClient->OnPlayer( pMessage->m_Clients[ i ].m_SessionID, pMessage->m_Clients[ i ].m_Name ); } - - if ( pClient->m_OnConnectComplete.Defined() ) - { - CConnectCompleteEvent connectComplete( ( CStrW )PS_OK, true ); - pClient->m_OnConnectComplete.DispatchEvent( pClient->GetScript(), &connectComplete ); - } + + pClient->OnConnectComplete(); } return true; } +//----------------------------------------------------------------------------- +// Name: OnConnectComplete() +// Desc: +//----------------------------------------------------------------------------- +void CNetClient::OnConnectComplete( ) +{ + if ( m_OnConnectComplete.Defined() ) + { + CConnectCompleteEvent connectComplete( ( CStrW )PS_OK, true ); + m_OnConnectComplete.DispatchEvent( GetScript(), &connectComplete ); + } +} + //----------------------------------------------------------------------------- // Name: OnHandshake() // Desc: @@ -315,8 +340,8 @@ bool CNetClient::OnHandshake( void* pContext, CFsmEvent* pEvent ) CNetClient* pClient = ( CNetClient* )( ( FsmActionCtx* )pContext )->pHost; CNetSession* pSession = ( ( FsmActionCtx* )pContext )->pSession; - assert( pClient ); - assert( pSession ); + debug_assert( pClient ); + debug_assert( pSession ); switch ( pEvent->GetType() ) { @@ -331,7 +356,7 @@ bool CNetClient::OnHandshake( void* pContext, CFsmEvent* pEvent ) handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE; handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; - ( ( CNetHost* )pClient )->SendMessage( pSession, &handshake ); + pClient->SendMessage( pSession, &handshake ); } break; @@ -340,7 +365,7 @@ bool CNetClient::OnHandshake( void* pContext, CFsmEvent* pEvent ) CAuthenticateMessage authenticate; authenticate.m_Name = pClient->m_Nickname; authenticate.m_Password = pClient->m_Password; - ( ( CNetHost* )pClient )->SendMessage( pSession, &authenticate ); + pClient->SendMessage( pSession, &authenticate ); } break; } @@ -361,8 +386,8 @@ bool CNetClient::OnAuthenticate( void* pContext, CFsmEvent* pEvent ) UNUSED2(pClient); CNetSession* pSession = ( ( FsmActionCtx* )pContext )->pSession; - assert( pClient ); - assert( pSession ); + debug_assert( pClient ); + debug_assert( pSession ); if ( pEvent->GetType() == (uint)NMT_ERROR ) { @@ -395,8 +420,8 @@ bool CNetClient::OnPreGame( void* pContext, CFsmEvent* pEvent ) CNetClient* pClient = ( CNetClient* )( ( FsmActionCtx* )pContext )->pHost; CNetSession* pSession = ( CNetSession* )( ( FsmActionCtx* )pContext )->pSession; - assert( pClient ); - assert( pSession ); + debug_assert( pClient ); + debug_assert( pSession ); switch ( pEvent->GetType() ) { @@ -499,6 +524,13 @@ bool CNetClient::OnInGame( void *pContext, CFsmEvent* pEvent ) CNetMessage* pMessage = ( CNetMessage* )pEvent->GetParamRef(); if ( pMessage ) { + if (pMessage->GetType() == NMT_SIMULATION_COMMAND) + { + CSimulationMessage* simMessage = static_cast (pMessage); + pClient->m_ClientTurnManager->OnSimulationMessage(simMessage); + return true; + } + if ( pMessage->GetType() >= NMT_COMMAND_FIRST && pMessage->GetType() < NMT_COMMAND_LAST ) { pClient->QueueIncomingMessage( pMessage ); @@ -511,7 +543,13 @@ bool CNetClient::OnInGame( void *pContext, CFsmEvent* pEvent ) CEndCommandBatchMessage* pMessage = ( CEndCommandBatchMessage* )pEvent->GetParamRef(); if ( !pMessage ) return false; - pClient->SetTurnLength( 1, pMessage->m_TurnLength ); + if (g_UseSimulation2) + { + CEndCommandBatchMessage* endMessage = static_cast (pMessage); + pClient->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn); + } + + pClient->m_TurnManager->SetTurnLength( 1, pMessage->m_TurnLength ); pClient->m_TurnPending = true; @@ -563,19 +601,35 @@ bool CNetClient::OnChat( void* pContext, CFsmEvent* pEvent ) // Name: OnStartGame() // Desc: //----------------------------------------------------------------------------- -bool CNetClient::OnStartGame( void* pContext, CFsmEvent* pEvent ) +void CNetClient::OnStartGame( void ) +{ + if ( m_OnStartGame.Defined() ) + { + CStartGameEvent event; + m_OnStartGame.DispatchEvent( GetScript(), &event ); + } +} + +//----------------------------------------------------------------------------- +// Name: OnStartGame_() +// Desc: +//----------------------------------------------------------------------------- +bool CNetClient::OnStartGame_( void* pContext, CFsmEvent* pEvent ) { // Validate parameters if ( !pEvent || !pContext ) return false; CNetClient* pClient = ( CNetClient* )( ( FsmActionCtx* )pContext )->pHost; + CNetSession* pSession = ( ( FsmActionCtx* )pContext )->pSession; - if ( pClient->m_OnStartGame.Defined() ) + if (g_UseSimulation2) { - CStartGameEvent event; - pClient->m_OnStartGame.DispatchEvent( pClient->GetScript(), &event ); + pClient->m_ClientTurnManager = new CNetClientTurnManager(*pClient->m_pGame->GetSimulation2(), *pClient, pClient->GetLocalPlayer()->GetPlayerID(), pSession->GetID()); + pClient->m_pGame->SetTurnManager(pClient->m_ClientTurnManager); } + pClient->OnStartGame(); + return true; } @@ -635,12 +689,15 @@ int CNetClient::StartGame( void ) if ( m_pGame->StartGame( m_pGameAttributes ) != PSRETURN_OK ) return -1; - // Send an end-of-batch message for turn 0 to signal that we're ready. - CEndCommandBatchMessage endBatch; - endBatch.m_TurnLength = 1000 / g_frequencyFilter->StableFrequency(); + if (!g_UseSimulation2) + { + // Send an end-of-batch message for turn 0 to signal that we're ready. + CEndCommandBatchMessage endBatch; + endBatch.m_TurnLength = 1000 / g_frequencyFilter->StableFrequency(); - CNetSession* pSession = GetSession( 0 ); - CNetHost::SendMessage( pSession, &endBatch ); + CNetSession* pSession = GetSession( 0 ); + SendMessage( pSession, &endBatch ); + } return 0; } @@ -658,33 +715,33 @@ CPlayer* CNetClient::GetLocalPlayer() // Name: NewTurn() // Desc: //----------------------------------------------------------------------------- -void CNetClient::NewTurn() +void CClientTurnManager::NewTurn() { - CScopeLock lock(m_Mutex); + CScopeLock lock(m_Client.m_Mutex); RotateBatches(); ClearBatch(2); - m_TurnPending = false; + m_Client.m_TurnPending = false; //debug_printf(L"In NewTurn - sending ack\n"); CEndCommandBatchMessage* pMsg = new CEndCommandBatchMessage; pMsg->m_TurnLength=1000/g_frequencyFilter->StableFrequency(); // JW: it'd probably be nicer to get the FPS as a parameter - CNetSession* pSession = GetSession( 0 ); - CNetHost::SendMessage( pSession, pMsg ); + CNetSession* pSession = m_Client.GetSession( 0 ); + m_Client.SendMessage( pSession, pMsg ); } //----------------------------------------------------------------------------- // Name: QueueLocalCommand() // Desc: //----------------------------------------------------------------------------- -void CNetClient::QueueLocalCommand( CNetMessage* pMessage ) +void CClientTurnManager::QueueLocalCommand( CNetMessage* pMessage ) { if ( !pMessage ) return; // Don't save these locally, since they'll be bounced by the server anyway - CNetSession* pSession = GetSession( 0 ); - CNetHost::SendMessage( pSession, pMessage ); + CNetSession* pSession = m_Client.GetSession( 0 ); + m_Client.SendMessage( pSession, pMessage ); } //----------------------------------------------------------------------------- @@ -695,5 +752,5 @@ void CNetClient::QueueIncomingMessage( CNetMessage* pMessage ) { CScopeLock lock( m_Mutex ); - QueueMessage( 2, pMessage ); + m_TurnManager->QueueMessage( 2, pMessage ); } diff --git a/source/network/NetClient.h b/source/network/NetClient.h index 53b3191c99..224842738a 100644 --- a/source/network/NetClient.h +++ b/source/network/NetClient.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -28,6 +28,7 @@ // INCLUDES #include "NetSession.h" +#include "NetTurnManager.h" #include "ps/CStr.h" #include "simulation/TurnManager.h" #include "simulation/ScriptObject.h" @@ -88,9 +89,10 @@ private: */ class CNetClient: public CNetHost, - public CJSObject, - protected CTurnManager + public CJSObject { + NONCOPYABLE(CNetClient); + public: CNetClient( CGame* pGame, CGameAttributes* pGameAttributes ); @@ -139,23 +141,21 @@ public: static void ScriptingInit( void ); int StartGame( void ); + CNetTurnManager* GetTurnManager() { debug_assert(m_ClientTurnManager); return m_ClientTurnManager; } + protected: virtual bool SetupSession ( CNetSession* pSession ); virtual bool HandleConnect ( CNetSession* pSession ); virtual bool HandleDisconnect ( CNetSession *pSession ); - virtual void NewTurn ( void ); - virtual bool NewTurnReady ( void ) { return m_TurnPending; } - virtual void QueueLocalCommand ( CNetMessage* pMessage ); - void QueueIncomingMessage ( CNetMessage* pMessage ); + void QueueIncomingMessage ( CNetMessage* pMessage ); + + virtual void OnConnectComplete ( void ); + virtual void OnStartGame ( void ); private: - // Not implemented - CNetClient( const CNetClient& ); - CNetClient& operator=( const CNetClient& ); - static bool OnError ( void* pContext, CFsmEvent* pEvent ); static bool OnPlayerJoin ( void* pContext, CFsmEvent* pEvent ); static bool OnHandshake ( void* pContext, CFsmEvent* pEvent ); @@ -163,13 +163,16 @@ private: static bool OnPreGame ( void* pContext, CFsmEvent* pEvent ); static bool OnInGame ( void* pContext, CFsmEvent* pEvent ); static bool OnChat ( void* pContext, CFsmEvent* pEvent ); - static bool OnStartGame ( void* pContext, CFsmEvent* pEvent ); + static bool OnStartGame_ ( void* pContext, CFsmEvent* pEvent ); bool SetupConnection( JSContext *cx, uintN argc, jsval *argv ); //CNetSession* m_Session; // Server session PlayerMap m_Players; // List of online players + + CTurnManager* m_TurnManager; + CNetClientTurnManager* m_ClientTurnManager; }; extern CNetClient *g_NetClient; diff --git a/source/network/NetMessage.cpp b/source/network/NetMessage.cpp index fbd6ab9013..d6bf83be76 100644 --- a/source/network/NetMessage.cpp +++ b/source/network/NetMessage.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -31,6 +31,9 @@ #include "Network.h" #include "NetMessage.h" +#include "ps/Game.h" +#include "simulation2/Simulation2.h" + #undef ALLNETMSGS_DONT_CREATE_NMTS #define ALLNETMSGS_IMPLEMENT #include "NetMessages.h" @@ -41,10 +44,6 @@ // DEFINES #define LOG_CATEGORY L"net" -// Please don't modify the deserializer map outside the ONCE-block in DeserializeMessage -//typedef std::map< NetMessageType, NetMessageDeserializer > MessageDeserializerMap; -//MessageDeserializerMap g_DeserializerMap; - //----------------------------------------------------------------------------- // Name: CNetMessage() // Desc: Constructor @@ -110,7 +109,7 @@ const u8* CNetMessage::Deserialize( const u8* pStart, const u8* pEnd ) m_Type = *( ( NetMessageType* )pStart ); uint size = *( ( uint* )( pStart + sizeof( NetMessageType ) ) ); - assert( pStart + size == pEnd ); + debug_assert( pStart + size == pEnd ); return pStart + sizeof( NetMessageType ) + sizeof( size ); } @@ -1054,6 +1053,10 @@ CNetMessage* CNetMessageFactory::CreateMessage(const void* pData, pNewMessage = new CFormationContactActionMessage; break; + case NMT_SIMULATION_COMMAND: + pNewMessage = new CSimulationMessage(g_Game->GetSimulation2()->GetScriptInterface()); + break; + default: LOG(CLogger::Error, LOG_CATEGORY, L"CNetMessageFactory::CreateMessage(): Unknown message received" ); break; diff --git a/source/network/NetMessage.h b/source/network/NetMessage.h index 215abda674..f6f6c5dc51 100644 --- a/source/network/NetMessage.h +++ b/source/network/NetMessage.h @@ -47,6 +47,8 @@ class CNetMessage : public ISerializable { + NONCOPYABLE(CNetMessage); + friend class CNetSession; public: @@ -119,7 +121,7 @@ public: /** * Retrieves the size in bytes of the serialized message. Before calling - * Serialize,the memory size for the buffer where to serialize the message + * Serialize, the memory size for the buffer where to serialize the message * object can be found by calling this method. * * @return The size of serialized message @@ -132,18 +134,10 @@ public: * @return The message as a string */ virtual CStr ToString( void ) const; - // virtual CStr GetString( void ) const; - // operator CStr() const { return GetString(); } private: - - // Not implemented - CNetMessage( const CNetMessage& ); - CNetMessage& operator=( const CNetMessage& ); - bool m_Dirty; // Message has been modified NetMessageType m_Type; // Message type - //u16 m_SerializeSize; // Serialized message size in bytes public: @@ -214,6 +208,28 @@ private: CNetMessageFactory& operator=( const CNetMessageFactory& ); }; +/** + * Special message type for simulation commands. + * These commands are exposed as arbitrary JS objects, associated with a specific player. + */ +class CSimulationMessage : public CNetMessage +{ +public: + CSimulationMessage(ScriptInterface& scriptInterface); + CSimulationMessage(ScriptInterface& scriptInterface, u32 client, i32 player, u32 turn, jsval data); + virtual u8* Serialize(u8* pBuffer) const; + virtual const u8* Deserialize(const u8* pStart, const u8* pEnd); + virtual size_t GetSerializedLength() const; + virtual CStr ToString() const; + + u32 m_Client; + i32 m_Player; + u32 m_Turn; + CScriptValRooted m_Data; +private: + ScriptInterface& m_ScriptInterface; +}; + // This time, the classes are created #include "NetMessages.h" diff --git a/source/network/NetMessageSim.cpp b/source/network/NetMessageSim.cpp new file mode 100644 index 0000000000..f18aa76b7b --- /dev/null +++ b/source/network/NetMessageSim.cpp @@ -0,0 +1,126 @@ +/* Copyright (C) 2010 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "NetMessage.h" + +#include "scriptinterface/ScriptInterface.h" +#include "simulation2/serialization/BinarySerializer.h" +#include "simulation2/serialization/StdDeserializer.h" + +/** + * Serializer instance that writes directly to a buffer (which must be long enough). + */ +class CBufferBinarySerializer : public CBinarySerializer +{ +public: + CBufferBinarySerializer(ScriptInterface& scriptInterface, u8* buffer) : + CBinarySerializer(scriptInterface), m_Buffer(buffer) + { + } + + void Put(const char* UNUSED(name), const u8* data, size_t len) + { + memcpy(m_Buffer, data, len); + m_Buffer += len; + } + + u8* m_Buffer; +}; + +/** + * Serializer instance that simply counts how many bytes would be written. + */ +class CLengthBinarySerializer : public CBinarySerializer +{ +public: + CLengthBinarySerializer(ScriptInterface& scriptInterface) : + CBinarySerializer(scriptInterface), m_Length(0) + { + } + + void Put(const char* UNUSED(name), const u8* UNUSED(data), size_t len) + { + m_Length += len; + } + + size_t m_Length; +}; + +CSimulationMessage::CSimulationMessage(ScriptInterface& scriptInterface) : + CNetMessage(NMT_SIMULATION_COMMAND), m_ScriptInterface(scriptInterface) +{ +} + +CSimulationMessage::CSimulationMessage(ScriptInterface& scriptInterface, u32 client, i32 player, u32 turn, jsval data) : + CNetMessage(NMT_SIMULATION_COMMAND), m_ScriptInterface(scriptInterface), + m_Client(client), m_Player(player), m_Turn(turn), m_Data(scriptInterface.GetContext(), data) +{ +} + +u8* CSimulationMessage::Serialize(u8* pBuffer) const +{ + // TODO: ought to handle serialization exceptions + // TODO: ought to represent common commands more efficiently + + u8* pos = CNetMessage::Serialize(pBuffer); + CBufferBinarySerializer serializer(m_ScriptInterface, pos); + serializer.NumberU32_Unbounded("client", m_Client); + serializer.NumberI32_Unbounded("player", m_Player); + serializer.NumberU32_Unbounded("turn", m_Turn); + serializer.ScriptVal("command", m_Data); + return serializer.m_Buffer; +} + +const u8* CSimulationMessage::Deserialize(const u8* pStart, const u8* pEnd) +{ + // TODO: ought to handle serialization exceptions + // TODO: ought to represent common commands more efficiently + + const u8* pos = CNetMessage::Deserialize(pStart, pEnd); + std::istringstream stream(std::string(pos, pEnd)); + CStdDeserializer deserializer(m_ScriptInterface, stream); + deserializer.NumberU32_Unbounded(m_Client); + deserializer.NumberI32_Unbounded(m_Player); + deserializer.NumberU32_Unbounded(m_Turn); + deserializer.ScriptVal(m_Data); + return pEnd; +} + +size_t CSimulationMessage::GetSerializedLength() const +{ + // TODO: serializing twice is stupidly inefficient - we should just + // do it once, store the result, and use it here and in Serialize + + CLengthBinarySerializer serializer(m_ScriptInterface); + serializer.NumberU32_Unbounded("client", m_Client); + serializer.NumberI32_Unbounded("player", m_Player); + serializer.NumberU32_Unbounded("turn", m_Turn); + serializer.ScriptVal("command", m_Data); + return CNetMessage::GetSerializedLength() + serializer.m_Length; +} + +CStr CSimulationMessage::ToString() const +{ + std::string source; + + if (!m_ScriptInterface.CallFunction(m_Data.get(), "toSource", source)) + source = "ERROR"; + + std::stringstream stream; + stream << "CSimulationMessage { m_Client: " << m_Client << ", m_Player: " << m_Player << ", m_Turn: " << m_Turn << ", m_Data: " << source << " }"; + return stream.str(); +} diff --git a/source/network/NetMessages.h b/source/network/NetMessages.h index 7920ee029c..494c38419b 100644 --- a/source/network/NetMessages.h +++ b/source/network/NetMessages.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -29,6 +29,7 @@ // INCLUDES #include "ps/CStr.h" #include "scripting/JSSerialization.h" +#include "scriptinterface/ScriptVal.h" #include "simulation/EntityHandles.h" // DEFINES @@ -79,6 +80,7 @@ enum NetMessageType NMT_NOTIFY_REQUEST, NMT_FORMATION_GOTO, NMT_FORMATION_CONTACT_ACTION, + NMT_SIMULATION_COMMAND, NMT_COMMAND_LAST, NMT_LAST // Last message in the list }; @@ -190,6 +192,7 @@ START_NMT_CLASS_(GameStart, NMT_GAME_START) END_NMT_CLASS() START_NMT_CLASS_(EndCommandBatch, NMT_END_COMMAND_BATCH) + NMT_FIELD_INT(m_Turn, u32, 4) NMT_FIELD_INT(m_TurnLength, u32, 2) END_NMT_CLASS() diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index 06656e15b9..a21f3dc9d7 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -31,12 +31,24 @@ #include "NetJsEvents.h" #include "NetSession.h" #include "NetServer.h" +#include "simulation2/Simulation2.h" #define LOG_CATEGORY L"net" // DECLARATIONS CNetServer* g_NetServer = NULL; +class CServerTurnManager : public CTurnManager +{ +public: + CServerTurnManager(CNetServer& server) : m_Server(server) { } + virtual void QueueLocalCommand(CNetMessage* pMessage); + virtual void NewTurn(); + virtual bool NewTurnReady(); +private: + CNetServer& m_Server; +}; + //----------------------------------------------------------------------------- // Name: CNetServer() // Desc: Constructor @@ -44,7 +56,8 @@ CNetServer* g_NetServer = NULL; CNetServer::CNetServer( CGame *pGame, CGameAttributes *pGameAttributes ) : m_JsSessions( &m_IDSessions ) { - //ONCE( ScriptingInit(); ); + m_TurnManager = new CServerTurnManager(*this); + m_ServerTurnManager = NULL; m_Game = pGame; m_GameAttributes = pGameAttributes; @@ -60,15 +73,14 @@ CNetServer::CNetServer( CGame *pGame, CGameAttributes *pGameAttributes ) m_GameAttributes->SetPlayerUpdateCallback( PlayerAttributeUpdate, this ); m_GameAttributes->SetPlayerSlotAssignmentCallback( PlayerSlotAssignment, this ); - // Set turn manager - CSimulation* pSimulation = m_Game->GetSimulation(); - if ( pSimulation ) pSimulation->SetTurnManager( this ); + if (!g_UseSimulation2) + m_Game->GetSimulation()->SetTurnManager(m_TurnManager); // Set an incredibly long turn length for debugging // (e.g. less command batch spam that way) for ( uint i = 0; i < 3; i++ ) { - CTurnManager::SetTurnLength( i, DEFAULT_TURN_LENGTH ); + m_TurnManager->SetTurnLength( i, CTurnManager::DEFAULT_TURN_LENGTH ); } g_ScriptingHost.SetGlobal( "g_NetServer", OBJECT_TO_JSVAL( GetScript() ) ); @@ -167,6 +179,7 @@ bool CNetServer::SetupSession( CNetSession* pSession ) pSession->AddTransition( NSS_INGAME, ( uint )NMT_NOTIFY_REQUEST, NSS_INGAME, (void*)&OnInGame, pContext ); pSession->AddTransition( NSS_INGAME, ( uint )NMT_FORMATION_GOTO, NSS_INGAME, (void*)&OnInGame, pContext ); pSession->AddTransition( NSS_INGAME, ( uint )NMT_FORMATION_CONTACT_ACTION, NSS_INGAME, (void*)&OnInGame, pContext ); + pSession->AddTransition( NSS_INGAME, ( uint )NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnInGame, pContext ); pSession->AddTransition( NSS_INGAME, ( uint )NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnInGame, pContext ); // Set first state @@ -189,7 +202,7 @@ bool CNetServer::HandleConnect( CNetSession* pSession ) handshake.m_Magic = PS_PROTOCOL_MAGIC; handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; - CNetHost::SendMessage( pSession, &handshake ); + SendMessage( pSession, &handshake ); // Store new session into the map m_IDSessions[ pSession->GetID() ] = pSession; @@ -230,11 +243,11 @@ void CNetServer::SetupPlayer( CNetSession* pSession ) // Validate parameters if ( !pSession ) return; - assert( m_GameAttributes ); + debug_assert( m_GameAttributes ); // Send a new config message to the connected client BuildGameSetupMessage( &gameSetup ); - CNetHost::SendMessage( pSession, &gameSetup ); + SendMessage( pSession, &gameSetup ); // Add information for already connected clients and the server playerJoin.m_Clients.resize( GetSessionCount() ); @@ -252,7 +265,7 @@ void CNetServer::SetupPlayer( CNetSession* pSession ) playerJoin.m_Clients[ i + 1 ].m_Name = pCurrSession->GetName(); } - CNetHost::SendMessage( pSession, &playerJoin ); + SendMessage( pSession, &playerJoin ); // TODO: Handle observers @@ -268,7 +281,7 @@ void CNetServer::SetupPlayer( CNetSession* pSession ) // Skip the player being setup if ( !pCurrSession || pCurrSession == pSession ) continue; - CNetHost::SendMessage( pCurrSession, &playerJoin ); + SendMessage( pCurrSession, &playerJoin ); } //Broadcast( &playerJoin ); @@ -298,13 +311,13 @@ void CNetServer::SetupPlayer( CNetSession* pSession ) break; } - CNetHost::SendMessage( pSession, &assignSlot ); + SendMessage( pSession, &assignSlot ); if ( pCurrSlot->GetAssignment() == SLOT_SESSION ) { // Setup player BuildPlayerConfigMessage( &playerConfig, pCurrSlot->GetPlayer() ); - CNetHost::SendMessage( pSession, &playerConfig ); + SendMessage( pSession, &playerConfig ); } } } @@ -358,7 +371,7 @@ void CNetServer::OnPlayerLeave( CNetSession* pSession ) // TODO Set everything up for re-connect and resume if ( pPlayerSlot ) { - SetClientPipe( pPlayerSlot->GetSlotID(), NULL ); + m_TurnManager->SetClientPipe( pPlayerSlot->GetSlotID(), NULL ); pPlayerSlot->AssignClosed(); } break; @@ -435,8 +448,8 @@ bool CNetServer::OnHandshake( void* pContext, CFsmEvent* pEvent ) CNetServer* pServer = ( CNetServer* )( ( FsmActionCtx* )pContext )->pHost; CNetSession* pSession = ( ( FsmActionCtx* )pContext )->pSession; - assert( pServer ); - assert( pSession ); + debug_assert( pServer ); + debug_assert( pSession ); CCliHandshakeMessage* pMessage = ( CCliHandshakeMessage* )pEvent->GetParamRef(); if ( pMessage ) @@ -444,7 +457,7 @@ bool CNetServer::OnHandshake( void* pContext, CFsmEvent* pEvent ) if ( pMessage->m_ProtocolVersion != PS_PROTOCOL_VERSION ) { CCloseRequestMessage closeRequest; - ( ( CNetHost* )pServer )->SendMessage( pSession, &closeRequest ); + pServer->SendMessage( pSession, &closeRequest ); CErrorMessage error( PS_OK, SS_UNCONNECTED ); pSession->Update( ( uint )NMT_ERROR, &error ); @@ -455,7 +468,7 @@ bool CNetServer::OnHandshake( void* pContext, CFsmEvent* pEvent ) handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION; handshakeResponse.m_Message = pServer->m_WelcomeMessage; handshakeResponse.m_Flags = 0; - ( ( CNetHost* )pServer )->SendMessage( pSession, &handshakeResponse ); + pServer->SendMessage( pSession, &handshakeResponse ); } } @@ -494,7 +507,7 @@ bool CNetServer::OnAuthenticate( void* pContext, CFsmEvent* pEvent ) authenticateResult.m_Code = ARC_OK; authenticateResult.m_SessionID = pSession->GetID(); authenticateResult.m_Message = L"Logged in"; - ( ( CNetHost* )pServer )->SendMessage( pSession, &authenticateResult ); + pServer->SendMessage( pSession, &authenticateResult ); //pServer->AddSession( pSession ); @@ -518,7 +531,7 @@ bool CNetServer::OnAuthenticate( void* pContext, CFsmEvent* pEvent ) authenticateResult.m_Code = ARC_PASSWORD_INVALID; authenticateResult.m_SessionID = 0; authenticateResult.m_Message = L"Invalid Password"; - ( ( CNetHost* )pServer )->SendMessage( pSession, &authenticateResult ); + pServer->SendMessage( pSession, &authenticateResult ); } } @@ -552,6 +565,13 @@ bool CNetServer::OnInGame( void* pContext, CFsmEvent* pEvent ) CNetMessage* pMessage = ( CNetMessage* )pEvent->GetParamRef(); if ( pMessage ) { + if (pMessage->GetType() == NMT_SIMULATION_COMMAND) + { + CSimulationMessage* simMessage = static_cast (pMessage); + pServer->m_ServerTurnManager->OnSimulationMessage(simMessage); + return true; + } + if ( pMessage->GetType() >= NMT_COMMAND_FIRST && pMessage->GetType() < NMT_COMMAND_LAST ) { //pSession->m_pPlayer->ValidateCommand(pMsg); @@ -562,6 +582,12 @@ bool CNetServer::OnInGame( void* pContext, CFsmEvent* pEvent ) if ( pMessage->GetType() == NMT_END_COMMAND_BATCH ) { + if (g_UseSimulation2) + { + CEndCommandBatchMessage* endMessage = static_cast (pMessage); + pServer->m_ServerTurnManager->NotifyFinishedClientCommands(pSession->GetID(), endMessage->m_Turn); + } + // TODO Update client timing information and recalculate turn length pSession->SetReadyForTurn( true ); } @@ -585,8 +611,8 @@ bool CNetServer::OnChat( void* pContext, CFsmEvent* pEvent ) CNetServer* pServer = ( CNetServer* )( ( FsmActionCtx* )pContext )->pHost; CNetSession* pSession = ( ( FsmActionCtx* )pContext )->pSession; - assert( pSession ); - assert( pServer ); + debug_assert( pSession ); + debug_assert( pServer ); CChatMessage* pMessage = ( CChatMessage* )pEvent->GetParamRef(); if ( pMessage ) @@ -734,12 +760,18 @@ int CNetServer::StartGame( void ) { uint i; - assert( m_Game ); - assert( m_GameAttributes ); + debug_assert( m_Game ); + debug_assert( m_GameAttributes ); if ( m_Game->StartGame( m_GameAttributes ) != PSRETURN_OK ) return -1; - CTurnManager::Initialize( m_GameAttributes->GetSlotCount() ); + if (g_UseSimulation2) + { + m_ServerTurnManager = new CNetServerTurnManager(*m_Game->GetSimulation2(), *this, m_Game->GetLocalPlayer()->GetPlayerID(), SERVER_SESSIONID); + m_Game->SetTurnManager(m_ServerTurnManager); + } + + m_TurnManager->Initialize( m_GameAttributes->GetSlotCount() ); for ( i = 0; i < m_GameAttributes->GetSlotCount(); i++ ) { @@ -747,7 +779,11 @@ int CNetServer::StartGame( void ) if ( !pCurrSlot ) continue; if ( pCurrSlot->GetAssignment() == SLOT_SESSION ) - CTurnManager::SetClientPipe( i, pCurrSlot->GetSession() ); + { + m_TurnManager->SetClientPipe( i, pCurrSlot->GetSession() ); + if (g_UseSimulation2) + m_ServerTurnManager->InitialiseClient(pCurrSlot->GetSessionID()); + } } m_State = SERVER_STATE_INGAME; @@ -778,7 +814,7 @@ void CNetServer::BuildGameSetupMessage( CGameSetupMessage* pMessage ) // Validate parameters if ( !pMessage ) return; - assert( m_GameAttributes ); + debug_assert( m_GameAttributes ); // Iterate through game properties and load them into message m_GameAttributes->IterateSynchedProperties( GameSetupMessageCallback, pMessage ); @@ -964,12 +1000,12 @@ bool CNetServer::AllowObserver( CNetSession* UNUSED( pSession ) ) // Name: NewTurnReady() // Desc: //----------------------------------------------------------------------------- -bool CNetServer::NewTurnReady() +bool CServerTurnManager::NewTurnReady() { // Check whether all sessions are ready for the next turn - for ( uint i = 0; i < GetSessionCount(); i++ ) + for ( uint i = 0; i < m_Server.GetSessionCount(); i++ ) { - CNetSession* pCurrSession = GetSession( i ); + CNetSession* pCurrSession = m_Server.GetSession( i ); if ( !pCurrSession ) continue; if ( !pCurrSession->IsReadyForTurn() ) @@ -983,14 +1019,14 @@ bool CNetServer::NewTurnReady() // Name: NewTurn() // Desc: //----------------------------------------------------------------------------- -void CNetServer::NewTurn() +void CServerTurnManager::NewTurn() { - CScopeLock lock(m_Mutex); + CScopeLock lock(m_Server.m_Mutex); // Reset session ready for next turn flag - for ( uint i = 0; i < GetSessionCount(); i++ ) + for ( uint i = 0; i < m_Server.GetSessionCount(); i++ ) { - CNetSession* pCurrSession = GetSession( i ); + CNetSession* pCurrSession = m_Server.GetSession( i ); if ( !pCurrSession ) continue; pCurrSession->SetReadyForTurn( false ); @@ -999,7 +1035,7 @@ void CNetServer::NewTurn() RecordBatch( 2 ); RotateBatches(); ClearBatch( 2 ); - IterateBatch( 1, CSimulation::GetMessageMask, m_Game->GetSimulation() ); + IterateBatch( 1, CSimulation::GetMessageMask, m_Server.m_Game->GetSimulation() ); SendBatch( 1 ); //IterateBatch( 1, SendToObservers, this ); } @@ -1008,9 +1044,14 @@ void CNetServer::NewTurn() // Name: QueueLocalCommand() // Desc: //----------------------------------------------------------------------------- -void CNetServer::QueueLocalCommand( CNetMessage *pMessage ) +void CServerTurnManager::QueueLocalCommand( CNetMessage *pMessage ) { - QueueIncomingCommand( pMessage ); + // Validate parameters + if ( !pMessage ) return; + + //LOG( NORMAL, LOG_CATEGORY, L"CServerTurnManager::QueueLocalCommand(): %hs.", pMessage->ToString().c_str() ); + + QueueMessage( 2, pMessage ); } //----------------------------------------------------------------------------- @@ -1024,7 +1065,7 @@ void CNetServer::QueueIncomingCommand( CNetMessage* pMessage ) //LOG( NORMAL, LOG_CATEGORY, L"CNetServer::QueueIncomingCommand(): %hs.", pMessage->ToString().c_str() ); - QueueMessage( 2, pMessage ); + m_TurnManager->QueueMessage( 2, pMessage ); } //----------------------------------------------------------------------------- @@ -1069,5 +1110,3 @@ void CNetServer::BuildPlayerSlotAssignmentMessage( break; } } - - diff --git a/source/network/NetServer.h b/source/network/NetServer.h index ca997695b5..c405f5bbc8 100644 --- a/source/network/NetServer.h +++ b/source/network/NetServer.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -29,6 +29,7 @@ // INCLUDES #include "Network.h" #include "NetSession.h" +#include "NetTurnManager.h" #include "simulation/TurnManager.h" #include "scripting/ScriptableObject.h" #include "ps/GameAttributes.h" @@ -45,7 +46,6 @@ #define CLIENT_MIN_SESSIONID 100 #define MAX_CLIENTS 8 #define MAX_OBSERVERS 5 -#define DEFAULT_SERVER_SESSION_ID 1 #define DEFAULT_SERVER_NAME L"Noname Server" #define DEFAULT_PLAYER_NAME L"Noname Player" #define DEFAULT_WELCOME_MESSAGE L"Noname Server Welcome Message" @@ -98,9 +98,9 @@ typedef std::vector< CNetSession* > SessionList; */ class CNetServer : public CNetHost, - public CJSObject, - public CTurnManager + public CJSObject { + NONCOPYABLE(CNetServer); public: CNetServer( CGame* pGame, CGameAttributes* pGameAttributes ); @@ -154,6 +154,8 @@ public: */ CNetSession* GetSessionByID( uint sessionID ); + CNetTurnManager* GetTurnManager() { debug_assert(m_ServerTurnManager); return m_ServerTurnManager; } + protected: virtual bool SetupSession ( CNetSession* pSession ); @@ -162,10 +164,6 @@ protected: private: - // Not implemented - CNetServer( const CNetServer& ); - CNetServer& operator=( const CNetServer& ); - //void ClientConnect ( ENetPeer* pPeer ); //void ClientDisconnect ( ENetPeer* pPeer ); //void ClientReceive ( ENetPeer* pPeer, ENetPacket* pPacket ); @@ -251,7 +249,6 @@ private: //CScriptObject m_ScriptConnect; // Script client connect dispatch //CScriptObject m_ScriptDisconnect; // Script client disconnect dispatch //CScriptObject m_ScriptChat; // Script client chat dispatch - CPlayer* m_Player; // Server player public: @@ -288,8 +285,8 @@ protected: // Call the JS callback for incoming events void OnPlayerChat ( const CStrW& from, const CStrW& message ); - void OnPlayerJoin ( CNetSession* pSession ); - void OnPlayerLeave ( CNetSession* pSession ); + virtual void OnPlayerJoin ( CNetSession* pSession ); + virtual void OnPlayerLeave ( CNetSession* pSession ); void SetupPlayer ( CNetSession* pSession ); //static bool OnPlayerJoin ( void* pContext, CFsmEvent* pEvent ); @@ -303,11 +300,6 @@ protected: // OVERRIDES FROM CServerSocket //virtual void OnAccept( const CSocketAddress& address ); - // OVERRIDES FROM CTurnManager - virtual void NewTurn( void ); - virtual bool NewTurnReady( void ); - virtual void QueueLocalCommand( CNetMessage* pMessage ); - // Will only be called from the Network Thread, by the OnAccept handler //virtual CNetServerSession* CreateSession( CSocketInternal* pSocketInternal); @@ -318,13 +310,16 @@ protected: // false otherwise virtual bool AllowObserver( CNetSession* pSession ); +public: + CMutex m_Mutex; // Synchronization object for batches + CGame* m_Game; // Pointer to actual game + private: CGameAttributes* m_GameAttributes; // Stores game attributes //int m_LastSessionID; // Stores the last session ID //SessionMap m_Sessions; // Managed sessions CJSMap< IDSessionMap > m_JsSessions; - CMutex m_Mutex; // Synchronization object for batches /* All sessions that have observer status (observer as in watcher - simple @@ -334,7 +329,6 @@ private: */ SessionList m_Observers; uint m_MaxObservers; // Maximum number of observers - CGame* m_Game; // Pointer to actual game NetServerState m_State; // Holds server state CStrW m_Name; // Server name CStrW m_WelcomeMessage; // Nice welcome message @@ -353,10 +347,11 @@ private: static void AttributeUpdate ( const CStrW& name, const CStrW& newValue, void* pData); static void PlayerAttributeUpdate ( const CStrW& name, const CStrW& value, CPlayer* pPlayer, void* pData ); static void PlayerSlotAssignment ( void* pData, CPlayerSlot* pPlayerSlot ); + + CTurnManager* m_TurnManager; + CNetServerTurnManager* m_ServerTurnManager; }; extern CNetServer *g_NetServer; #endif // NETSERVER_H - - diff --git a/source/network/NetSession.cpp b/source/network/NetSession.cpp index cb0a370a71..990dacbb4b 100644 --- a/source/network/NetSession.cpp +++ b/source/network/NetSession.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -139,7 +139,7 @@ bool CNetHost::Connect( const CStr& host, uint port ) ENetAddress addr; PeerSession item; - assert( m_Host ); + debug_assert( m_Host ); // Bind to specified host addr.port = port; @@ -196,8 +196,8 @@ bool CNetHost::Disconnect( CNetSession* pSession ) // Validate parameters if ( !pSession ) return false; - assert( m_Host ); - assert( pSession->m_Peer ); + debug_assert( m_Host ); + debug_assert( pSession->m_Peer ); // Disconnect peer enet_peer_disconnect( pSession->m_Peer, 0 ); @@ -233,7 +233,7 @@ bool CNetHost::Disconnect( CNetSession* pSession ) //----------------------------------------------------------------------------- /*bool CNetHost::Run( void ) { - assert( m_Host ); + debug_assert( m_Host ); // Host created? if ( !m_Host ) return false; @@ -354,7 +354,7 @@ bool CNetHost::Poll( void ) PeerSession item; PeerSessionList::iterator it; - assert( m_Host ); + debug_assert( m_Host ); // Poll host for events while ( enet_host_service( m_Host, &event, 0 ) > 0 ) @@ -416,20 +416,24 @@ bool CNetHost::Poll( void ) // Is this our session? if ( it->pPeer == event.peer ) { + bool ok = false; + // Create message from raw data CNetMessage* pNewMessage = CNetMessageFactory::CreateMessage( event.packet->data, event.packet->dataLength ); - if ( !pNewMessage ) return false; + if ( pNewMessage ) + { + NET_LOG4( "Message %s of size %lu was received from %p", pNewMessage->ToString().c_str(), (unsigned long)pNewMessage->GetSerializedLength(), event.peer->data ); - NET_LOG4( "Message %s of size %lu was received from %p", pNewMessage->ToString().c_str(), (unsigned long)pNewMessage->GetSerializedLength(), event.peer->data ); + ok = HandleMessageReceive( pNewMessage, it->pSession ); - // Successfully handled? - if ( !HandleMessageReceive( pNewMessage, it->pSession ) ) { - enet_packet_destroy( event.packet ); - return false; + delete pNewMessage; } // Done using the packet enet_packet_destroy( event.packet ); + + if (! ok) + return false; } } @@ -443,8 +447,6 @@ bool CNetHost::Poll( void ) //----------------------------------------------------------------------------- // Name: Broadcast() // Desc: Broadcast the specified message to connected clients -// Note: Reference counting for a sending message requires multithreading -// locking mechanisms so a clone of the message is made and sent out //----------------------------------------------------------------------------- void CNetHost::Broadcast( const CNetMessage* pMessage ) { @@ -501,8 +503,8 @@ bool CNetHost::SendMessage( // Validate parameters if ( !pMessage || !pSession ) return false; - assert( pSession->m_Peer ); - assert( m_Host ); + debug_assert( pSession->m_Peer ); + debug_assert( m_Host ); size_t size = pMessage->GetSerializedLength(); @@ -544,7 +546,7 @@ CNetMessage* CNetHost::ReceiveMessage( const CNetSession* pSession ) // Validate parameters if ( !pSession ) return NULL; - assert( pSession->m_Peer ); + debug_assert( pSession->m_Peer ); // Let ENet receive a message from peer ENetPacket* pPacket = enet_peer_receive( pSession->m_Peer, ENET_DEFAULT_CHANNEL ); @@ -705,7 +707,7 @@ void CNetSession::Push( CNetMessage* pMessage ) // Validate parameters if ( !pMessage ) return; - assert( m_Host ); + debug_assert( m_Host ); m_Host->SendMessage( this, pMessage ); } @@ -716,7 +718,7 @@ void CNetSession::Push( CNetMessage* pMessage ) //----------------------------------------------------------------------------- CNetMessage* CNetSession::TryPop( void ) { - assert( m_Host ); + debug_assert( m_Host ); return m_Host->ReceiveMessage( this ); } @@ -839,8 +841,8 @@ bool CNetServerSession::BaseHandler( CNetMessage* pMessage, CNetSession* pSession ) { - assert( pMessage ); - assert( pSession ); + debug_assert( pMessage ); + debug_assert( pSession ); // Validate parameters if ( !pMessage || !pSession ) return false; @@ -882,9 +884,9 @@ bool CNetServerSession::HandshakeHandler( CNetMessage* pMessage, CNetSession* pSession ) { - assert( pMessage ); - assert( pSession ); - assert( m_Server ); + debug_assert( pMessage ); + debug_assert( pSession ); + debug_assert( m_Server ); // Validate parameters if ( !pMessage || !pSession ) return false; diff --git a/source/network/NetSession.h b/source/network/NetSession.h index d40c9e7e10..81444766c0 100644 --- a/source/network/NetSession.h +++ b/source/network/NetSession.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -28,7 +28,6 @@ // INCLUDES #include "Network.h" -#include "ps/Singleton.h" #include "ps/GameAttributes.h" #include "ps/Player.h" #include "fsm.h" @@ -61,11 +60,11 @@ typedef std::vector< PeerSession > PeerSessionList; /* CLASS : CNetHost - DESCRIPTION : CNetHost is a wrapper around ENet host conecept + DESCRIPTION : CNetHost is a wrapper around ENet host concept NOTES : */ -class CNetHost : public Singleton< CNetHost > +class CNetHost { public: @@ -198,7 +197,7 @@ private: which represents a peer from a network connection. A network session is spawned by CNetServer each time a client connects and destroyed when it disconnects. When a - new message is received fom a client, its representing + new message is received from a client, its representing session object's message handler is called for processing that message. CNetSession is also a state machine. All client requests @@ -212,6 +211,8 @@ class CNetSession : public CFsm, public CJSObject< CNetSession >, public IMessagePipeEnd { + NONCOPYABLE(CNetSession); + friend class CNetHost; public: @@ -289,11 +290,6 @@ private: // Only the hosts can create sessions CNetSession( CNetHost* pHost, ENetPeer* pPeer ); - // Not implemented - CNetSession( void ); - CNetSession( const CNetSession& ); - CNetSession& operator=( const CNetSession& ); - CNetHost* m_Host; // The associated local host ENetPeer* m_Peer; // Represents the peer host uint m_ID; // Session ID diff --git a/source/network/NetTurnManager.cpp b/source/network/NetTurnManager.cpp new file mode 100644 index 0000000000..7bbf642def --- /dev/null +++ b/source/network/NetTurnManager.cpp @@ -0,0 +1,265 @@ +/* Copyright (C) 2010 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "NetTurnManager.h" + +#include "NetServer.h" +#include "NetClient.h" +#include "maths/MathUtil.h" +#include "simulation2/Simulation2.h" + +static const int TURN_LENGTH = 200; // TODO: this should be a variable controlled by the server depending on latency + +static const int COMMAND_DELAY = 2; + +//#define NETTURN_LOG debug_printf + +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) +{ + // 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). + // We know we have not yet finished scheduling commands for n+2. + // Hence other clients can be on turn n-1, n, n+1, and no other. + // So they can be sending us commands scheduled for n+1, n+2, n+3. + // So we need a 3-element buffer: + m_QueuedCommands.resize(COMMAND_DELAY + 1); +} + +bool CNetTurnManager::Update(float frameLength) +{ + m_DeltaTime += frameLength; + + // If we haven't reached the next turn yet, do nothing + if (m_DeltaTime < 0) + return false; + +#ifdef NETTURN_LOG + NETTURN_LOG(L"Update current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn); +#endif + + // Check that the next turn is ready for execution + if (m_ReadyTurn > m_CurrentTurn) + { + NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY); + m_CurrentTurn += 1; + + // Put all the client commands into a single list, in a globally consistent order + std::vector commands; + for (std::map >::iterator it = m_QueuedCommands[0].begin(); it != m_QueuedCommands[0].end(); ++it) + { + commands.insert(commands.end(), it->second.begin(), it->second.end()); + } + m_QueuedCommands.pop_front(); + m_QueuedCommands.resize(m_QueuedCommands.size() + 1); + +#ifdef NETTURN_LOG + NETTURN_LOG(L"Running %d cmds\n", commands.size()); +#endif + + m_Simulation2.Update(TURN_LENGTH, commands); + + // TODO: Compute state hash, send SyncCheck message + + // Set the time for the next turn update + m_DeltaTime -= TURN_LENGTH / 1000.f; + + return true; + } + else + { + // Oops, we wanted to start the next turn but it's not ready yet - + // there must be too much network lag. + // TODO: complain to the user. + // TODO: send feedback to the server to increase the turn length. + + // Reset the next-turn timer to 0 so we try again next update but + // so we don't rush to catch up in subsequent turns. + // TODO: we should do clever rate adjustment instead of just pausing like this. + m_DeltaTime = 0; + + return false; + } +} + +void CNetTurnManager::Interpolate(float frameLength) +{ + float offset = clamp(m_DeltaTime / (TURN_LENGTH / 1000.f) + 1.0, 0.0, 1.0); + m_Simulation2.Interpolate(frameLength, offset); +} + +void CNetTurnManager::AddCommand(int client, int player, CScriptValRooted data, u32 turn) +{ +#ifdef NETTURN_LOG + NETTURN_LOG(L"AddCommand(client=%d player=%d turn=%d)\n", client, player, turn); +#endif + + if (!(m_CurrentTurn < turn && turn <= m_CurrentTurn + COMMAND_DELAY + 1)) + { + debug_warn(L"Received command for invalid turn"); + return; + } + + SimulationCommand cmd; + cmd.player = player; + cmd.data = data; + m_QueuedCommands[turn - (m_CurrentTurn+1)][client].push_back(cmd); +} + +void CNetTurnManager::FinishedAllCommands(u32 turn) +{ +#ifdef NETTURN_LOG + NETTURN_LOG(L"FinishedAllCommands(%d)\n", turn); +#endif + + debug_assert(turn == m_ReadyTurn + 1); + m_ReadyTurn = turn; +} + +void CNetClientTurnManager::PostCommand(CScriptValRooted data) +{ +#ifdef NETTURN_LOG + NETTURN_LOG(L"PostCommand()\n"); +#endif + + // Transmit command to server + CSimulationMessage msg(m_Simulation2.GetScriptInterface(), m_ClientId, m_PlayerId, m_CurrentTurn + COMMAND_DELAY, data.get()); + CNetSession* session = m_NetClient.GetSession(0); + m_NetClient.SendMessage(session, &msg); + + // Add to our local queue + //AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + COMMAND_DELAY); + // TODO: we should do this when the server stops sending our commands back to us +} + +void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn) +{ +#ifdef NETTURN_LOG + NETTURN_LOG(L"NotifyFinishedOwnCommands(%d)\n", turn); +#endif + + CEndCommandBatchMessage msg; + msg.m_TurnLength = TURN_LENGTH; + msg.m_Turn = turn; + 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 + AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn); +} + + + +void CNetServerTurnManager::PostCommand(CScriptValRooted data) +{ +#ifdef NETTURN_LOG + NETTURN_LOG(L"PostCommand()\n"); +#endif + + // Transmit command to all clients + CSimulationMessage msg(m_Simulation2.GetScriptInterface(), m_ClientId, m_PlayerId, m_CurrentTurn + COMMAND_DELAY, data.get()); + m_NetServer.Broadcast(&msg); + + // Add to our local queue + AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + COMMAND_DELAY); +} + +void CNetServerTurnManager::NotifyFinishedOwnCommands(u32 turn) +{ +#ifdef NETTURN_LOG + NETTURN_LOG(L"NotifyFinishedOwnCommands(%d)\n", turn); +#endif + + NotifyFinishedClientCommands(m_ClientId, turn); +} + +void CNetServerTurnManager::NotifyFinishedClientCommands(int client, u32 turn) +{ +#ifdef NETTURN_LOG + NETTURN_LOG(L"NotifyFinishedClientCommands(client=%d, turn=%d)\n", client, turn); +#endif + + // Must be a client we've already heard of + debug_assert(m_ClientsReady.find(client) != m_ClientsReady.end()); + + // Clients must advance one turn at a time + debug_assert(turn == m_ClientsReady[client] + 1); + m_ClientsReady[client] = turn; + + // See if all clients (including self) are ready for a new turn + for (std::map::iterator it = m_ClientsReady.begin(); it != m_ClientsReady.end(); ++it) + { +#ifdef NETTURN_LOG + NETTURN_LOG(L" %d: %d <=? %d\n", it->first, it->second, m_ReadyTurn); +#endif + if (it->second <= m_ReadyTurn) + return; + } + + // Tell all clients that the next turn is ready + CEndCommandBatchMessage msg; + msg.m_TurnLength = TURN_LENGTH; + msg.m_Turn = turn; + m_NetServer.Broadcast(&msg); + + // Move ourselves to the next turn + FinishedAllCommands(m_ReadyTurn + 1); +} + +void CNetServerTurnManager::InitialiseClient(int client) +{ + debug_assert(m_ClientsReady.find(client) == m_ClientsReady.end()); + m_ClientsReady[client] = 1; + + // TODO: do we need some kind of UninitialiseClient in case they leave? +} + +void CNetServerTurnManager::OnSimulationMessage(CSimulationMessage* msg) +{ + // Send it back to all clients immediately + m_NetServer.Broadcast(msg); + + // TODO: we should do some validation of ownership (clients can't send commands on behalf of opposing players) + + // TODO: we shouldn't send the message back to the client that first sent it + + // Process it ourselves + AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn); +} + +void CNetLocalTurnManager::PostCommand(CScriptValRooted data) +{ + // Add directly to the next turn, ignoring COMMAND_DELAY, + // because we don't need to compensate for network latency + AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + 1); +} + +void CNetLocalTurnManager::NotifyFinishedOwnCommands(u32 turn) +{ + FinishedAllCommands(turn); +} + +void CNetLocalTurnManager::OnSimulationMessage(CSimulationMessage* UNUSED(msg)) +{ + debug_warn(L"This should never be called"); +} diff --git a/source/network/NetTurnManager.h b/source/network/NetTurnManager.h new file mode 100644 index 0000000000..1adde20e93 --- /dev/null +++ b/source/network/NetTurnManager.h @@ -0,0 +1,177 @@ +/* Copyright (C) 2010 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_NETTURNMANAGER +#define INCLUDED_NETTURNMANAGER + +#include "simulation2/helpers/SimulationCommand.h" + +class CNetServer; +class CNetClient; +class CSimulationMessage; +class CSimulation2; + +/* + * This file deals with the logic of the network turn system. The basic idea is as in + * http://www.gamasutra.com/view/feature/3094/1500_archers_on_a_288_network_.php?print=1 + * + * Each player performs the simulation for turn N. + * User input is translated into commands scheduled for execution in turn N+2 which are + * distributed to all other clients. + * After a while, a player want to perform the simulation for turn N+1, + * which first requires that it has all the other clients' commands for turn N+1. + * In that case, it does the simulation and tells all the other clients it has finished + * sending commands for turn N+2, and it starts sending commands for turn N+3. + * + * Commands are redistributed immediately by the server. + * To ensure a consistent execution of commands, they are each associated with a + * client session ID (which is globally unique and consistent), which is used to sort them. + */ + +/** + * Common network turn system (used by clients, servers, and offline games). + */ +class CNetTurnManager +{ + NONCOPYABLE(CNetTurnManager); +public: + /** + * Construct for a given player ID, and a given network session ID. + */ + CNetTurnManager(CSimulation2& simulation, int playerId, int clientId); + + virtual ~CNetTurnManager() { } + + /** + * Advance the simulation by a certain time. If this brings us past the current + * turn length, the next turn is processed and the function returns true. + * Otherwise, nothing happens and it returns false. + * @param frameLength length of the previous frame in seconds + */ + bool Update(float frameLength); + + /** + * Advance the graphics by a certain time. + * @param frameLength length of the previous frame in seconds + */ + void Interpolate(float frameLength); + + /** + * Called by networking code when a simulation message is received. + */ + virtual void OnSimulationMessage(CSimulationMessage* msg) = 0; + + /** + * Called by simulation code, to add a new command to be distributed to all clients and executed soon. + */ + virtual void PostCommand(CScriptValRooted data) = 0; + + /** + * Called when all commands for a given turn have been received. + * This allows Update to progress to that turn. + */ + void FinishedAllCommands(u32 turn); + +protected: + /** + * Store a command to be executed at a given turn. + */ + void AddCommand(int client, int player, CScriptValRooted data, u32 turn); + + /** + * Called when this client has finished sending all its commands scheduled for the given turn. + */ + virtual void NotifyFinishedOwnCommands(u32 turn) = 0; + + CSimulation2& m_Simulation2; + + /// The turn that we have most recently executed + u32 m_CurrentTurn; + + /// The latest turn for which we have received all commands from all clients + u32 m_ReadyTurn; + + /// Commands queued at each turn (index 0 is for m_CurrentTurn+1) + std::deque > > m_QueuedCommands; + + int m_PlayerId; + uint m_ClientId; + + /// Time remaining until we ought to execute the next turn + float m_DeltaTime; +}; + +class CNetClientTurnManager : public CNetTurnManager +{ +public: + CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int playerId, int clientId) : + CNetTurnManager(simulation, playerId, clientId), m_NetClient(client) + { + } + + virtual void OnSimulationMessage(CSimulationMessage* msg); + + virtual void PostCommand(CScriptValRooted data); + +protected: + virtual void NotifyFinishedOwnCommands(u32 turn); + + CNetClient& m_NetClient; +}; + +class CNetServerTurnManager : public CNetTurnManager +{ +public: + CNetServerTurnManager(CSimulation2& simulation, CNetServer& server, int playerId, int clientId) : + CNetTurnManager(simulation, playerId, clientId), m_NetServer(server) + { + } + + virtual void OnSimulationMessage(CSimulationMessage* msg); + + virtual void PostCommand(CScriptValRooted data); + + void NotifyFinishedClientCommands(int client, u32 turn); + + 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) + std::map m_ClientsReady; + + CNetServer& m_NetServer; +}; + +class CNetLocalTurnManager : public CNetTurnManager +{ +public: + CNetLocalTurnManager(CSimulation2& simulation) : + CNetTurnManager(simulation, 1, 0) + { + } + + virtual void OnSimulationMessage(CSimulationMessage* msg); + + virtual void PostCommand(CScriptValRooted data); + +protected: + virtual void NotifyFinishedOwnCommands(u32 turn); +}; + +#endif // INCLUDED_NETTURNMANAGER diff --git a/source/ps/Game.cpp b/source/ps/Game.cpp index 7e63457cc2..dbe840ab6d 100644 --- a/source/ps/Game.cpp +++ b/source/ps/Game.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -84,6 +84,8 @@ CGame::CGame(): if (g_UseSimulation2) { + m_TurnManager = new CNetLocalTurnManager(*m_Simulation2); // this will get replaced if we're a net server/client + m_Simulation2->LoadDefaultScripts(); m_Simulation2->ResetState(); @@ -105,12 +107,19 @@ CGame::~CGame() // Again, the in-game call tree is going to be different to the main menu one. g_Profiler.StructuralReset(); + delete m_TurnManager; delete m_GameView; delete m_Simulation2; delete m_Simulation; delete m_World; } +void CGame::SetTurnManager(CNetTurnManager* turnManager) +{ + if (m_TurnManager) + delete m_TurnManager; + m_TurnManager = turnManager; +} /** @@ -145,12 +154,13 @@ PSRETURN CGame::RegisterInit(CGameAttributes* pAttribs) PSRETURN CGame::ReallyStartGame() { // Call the reallyStartGame GUI function, but only if it exists - jsval fval, rval; - JSBool ok = JS_GetProperty(g_ScriptingHost.getContext(), g_GUI->GetScriptObject(), "reallyStartGame", &fval); - debug_assert(ok); - if (ok && !JSVAL_IS_VOID(fval)) + if (g_GUI->HasPages()) { - ok = JS_CallFunctionValue(g_ScriptingHost.getContext(), g_GUI->GetScriptObject(), fval, 0, NULL, &rval); + jsval fval, rval; + JSBool ok = JS_GetProperty(g_ScriptingHost.getContext(), g_GUI->GetScriptObject(), "reallyStartGame", &fval); + debug_assert(ok); + if (ok && !JSVAL_IS_VOID(fval)) + ok = JS_CallFunctionValue(g_ScriptingHost.getContext(), g_GUI->GetScriptObject(), fval, 0, NULL, &rval); } debug_printf(L"GAME STARTED, ALL INIT COMPLETE\n"); @@ -162,9 +172,6 @@ PSRETURN CGame::ReallyStartGame() // Mark terrain as modified so the minimap can repaint (is there a cleaner way of handling this?) g_GameRestarted = true; - - g_GUI->SendEventToAll("sessionstart"); - return 0; } @@ -240,20 +247,25 @@ bool CGame::Update(double deltaTime, bool doInterpolate) deltaTime *= m_SimRate; bool ok = true; - if (g_UseSimulation2) + if (deltaTime) { - if (m_Simulation2->Update(deltaTime)) - g_GUI->SendEventToAll("SimulationUpdate"); - } - else - { - ok = m_Simulation->Update(deltaTime); + if (g_UseSimulation2) + { + PROFILE("update"); + if (m_TurnManager->Update(deltaTime)) + g_GUI->SendEventToAll("SimulationUpdate"); + } + else + { + ok = m_Simulation->Update(deltaTime); + } } if (doInterpolate) { + PROFILE("interpolate"); if (g_UseSimulation2) - m_Simulation2->Interpolate(deltaTime); + m_TurnManager->Interpolate(deltaTime); else m_Simulation->Interpolate(deltaTime); } @@ -272,6 +284,12 @@ bool CGame::Update(double deltaTime, bool doInterpolate) return ok; } +void CGame::Interpolate(float frameLength) +{ + m_TurnManager->Interpolate(frameLength); +} + + /** * Test player statistics and update game status as required. * diff --git a/source/ps/Game.h b/source/ps/Game.h index 7ec00848d1..33c7c5bc8f 100644 --- a/source/ps/Game.h +++ b/source/ps/Game.h @@ -34,6 +34,7 @@ class CGameView; class CSimulation; class CPlayer; class CGameAttributes; +class CNetTurnManager; /** * Default player limit (not counting the Gaia player) @@ -97,6 +98,8 @@ class CGame EOG_LOSE, /// Game is over, local player loses EOG_WIN /// Game is over, local player wins } GameStatus; + + CNetTurnManager* m_TurnManager; public: CGame(); @@ -121,6 +124,8 @@ public: */ bool Update(double deltaTime, bool doInterpolate = true); + void Interpolate(float frameLength); + void UpdateGameStatus(); void EndGame(); @@ -217,6 +222,17 @@ public: inline float GetSimRate() const { return m_SimRate; } + /** + * Replace the current turn manager. + * This class will take ownership of the pointer. + */ + void SetTurnManager(CNetTurnManager* turnManager); + + CNetTurnManager* GetTurnManager() const + { + return m_TurnManager; + } + private: PSRETURN RegisterInit(CGameAttributes* pAttribs); }; diff --git a/source/ps/GameSetup/Config.cpp b/source/ps/GameSetup/Config.cpp index 1665454904..fa7c3d4e4a 100644 --- a/source/ps/GameSetup/Config.cpp +++ b/source/ps/GameSetup/Config.cpp @@ -122,9 +122,6 @@ static void ProcessCommandLineArgs(const CmdLineArgs& args) // TODO: all these options (and the ones processed elsewhere) should // be documented somewhere for users. - if (args.Has("autostart")) - g_AutostartMap = args.Get("autostart"); - if (args.Has("buildarchive")) { // note: VFS init is sure to have been completed by now diff --git a/source/ps/GameSetup/Config.h b/source/ps/GameSetup/Config.h index 9ed1e868a0..e11d4f2d15 100644 --- a/source/ps/GameSetup/Config.h +++ b/source/ps/GameSetup/Config.h @@ -63,9 +63,6 @@ extern bool g_VSync; extern bool g_Quickstart; -// If non-empty, specified map will be automatically loaded -extern CStr g_AutostartMap; - extern CStrW g_CursorName; class CmdLineArgs; diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index d31a88ec0b..6d9aba884c 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/source/ps/GameSetup/GameSetup.cpp @@ -550,66 +550,62 @@ static void InitVfs(const CmdLineArgs& args) } -static void InitPs(bool setup_gui) +static void InitPs(bool setup_gui, const CStrW& gui_page) { - if (setup_gui) - { - // The things here aren't strictly GUI, but they're unnecessary when in Atlas - // because the game doesn't draw any text or handle keys or anything - - { - // console - TIMER(L"ps_console"); - - g_Console->UpdateScreenSize(g_xres, g_yres); - - // Calculate and store the line spacing - CFont font(L"console"); - g_Console->m_iFontHeight = font.GetLineSpacing(); - g_Console->m_iFontWidth = font.GetCharacterWidth(L'C'); - g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth); - // Offset by an arbitrary amount, to make it fit more nicely - g_Console->m_iFontOffset = 9; - } - - // language and hotkeys - { - TIMER(L"ps_lang_hotkeys"); - - std::string lang = "english"; - CFG_GET_SYS_VAL("language", String, lang); - I18n::LoadLanguage(lang.c_str()); - - LoadHotkeys(); - } - - // GUI uses VFS, so this must come after VFS init. - if (g_AutostartMap.empty()) - g_GUI->SwitchPage(L"page_pregame.xml", JSVAL_VOID); - else - g_GUI->SwitchPage(L"page_session_new.xml", JSVAL_VOID); - - // Warn nicely about missing S3TC support - if (!ogl_tex_has_s3tc()) - { - g_GUI->DisplayMessageBox(600, 350, L"Warning", - L"Performance warning:\n\n" - L"Your graphics drivers do not support S3TC compressed textures. This will significantly reduce performance and increase memory usage.\n\n" -#if !(OS_WIN || OS_MACOSX) - L"See http://dri.freedesktop.org/wiki/S3TC for details. " - L"Installing the libtxc_dxtn library will fix these problems. " - L"Alternatively, running 'driconf' and setting force_s3tc_enable will fix the performance but may cause rendering bugs." -#else - L"Please try updating your graphics drivers to ensure you have full hardware acceleration." -#endif - ); - } - } - else + if (!setup_gui) { // We do actually need *some* kind of GUI loaded, so use the // (currently empty) Atlas one g_GUI->SwitchPage(L"page_atlas.xml", JSVAL_VOID); + return; + } + + // The things here aren't strictly GUI, but they're unnecessary when in Atlas + // because the game doesn't draw any text or handle keys or anything + + { + // console + TIMER(L"ps_console"); + + g_Console->UpdateScreenSize(g_xres, g_yres); + + // Calculate and store the line spacing + CFont font(L"console"); + g_Console->m_iFontHeight = font.GetLineSpacing(); + g_Console->m_iFontWidth = font.GetCharacterWidth(L'C'); + g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth); + // Offset by an arbitrary amount, to make it fit more nicely + g_Console->m_iFontOffset = 9; + } + + // language and hotkeys + { + TIMER(L"ps_lang_hotkeys"); + + std::string lang = "english"; + CFG_GET_SYS_VAL("language", String, lang); + I18n::LoadLanguage(lang.c_str()); + + LoadHotkeys(); + } + + // GUI uses VFS, so this must come after VFS init. + g_GUI->SwitchPage(gui_page, JSVAL_VOID); + + // Warn nicely about missing S3TC support + if (!ogl_tex_has_s3tc()) + { + g_GUI->DisplayMessageBox(600, 350, L"Warning", + L"Performance warning:\n\n" + L"Your graphics drivers do not support S3TC compressed textures. This will significantly reduce performance and increase memory usage.\n\n" +#if !(OS_WIN || OS_MACOSX) + L"See http://dri.freedesktop.org/wiki/S3TC for details. " + L"Installing the libtxc_dxtn library will fix these problems. " + L"Alternatively, running 'driconf' and setting force_s3tc_enable will fix the performance but may cause rendering bugs." +#else + L"Please try updating your graphics drivers to ensure you have full hardware acceleration." +#endif + ); } } @@ -643,8 +639,7 @@ static void ShutdownPs() { SAFE_DELETE(g_GUI); - delete g_Console; - g_Console = 0; + SAFE_DELETE(g_Console); // disable the special Windows cursor, or free textures for OGL cursors cursor_draw(0, g_mouse_x, g_mouse_y); @@ -716,19 +711,9 @@ static void InitSDL() void EndGame() { - if (g_NetServer) - { - delete g_NetServer; - g_NetServer=NULL; - } - else if (g_NetClient) - { - delete g_NetClient; - g_NetClient=NULL; - } - - delete g_Game; - g_Game=NULL; + SAFE_DELETE(g_NetServer); + SAFE_DELETE(g_NetClient); + SAFE_DELETE(g_Game); } @@ -851,6 +836,8 @@ void EarlyInit() srand(time(NULL)); // NOTE: this rand should *not* be used for simulation! } +static bool Autostart(const CmdLineArgs& args); + void Init(const CmdLineArgs& args, int flags) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; @@ -1005,9 +992,6 @@ void Init(const CmdLineArgs& args, int flags) pwglSwapIntervalEXT(g_VSync? 1 : 0); #endif - MICROLOG(L"init ps"); - InitPs(setup_gui); - ogl_WarnIfError(); InitRenderer(); @@ -1040,31 +1024,142 @@ void Init(const CmdLineArgs& args, int flags) ogl_WarnIfError(); - if (! g_AutostartMap.empty()) + if (!Autostart(args)) { - // Code copied mostly from atlas/GameInterface/Handlers/Map.cpp - - // maybe should be refactored to avoid duplication - g_GameAttributes.m_MapFile = g_AutostartMap+".pmp"; - - // Make the whole world visible - g_GameAttributes.m_LOSSetting = LOS_SETTING_ALL_VISIBLE; - g_GameAttributes.m_FogOfWar = false; - - for (int i = 1; i < 8; ++i) - g_GameAttributes.GetSlot(i)->AssignLocal(); - - g_Game = new CGame(); - - PSRETURN ret = g_Game->StartGame(&g_GameAttributes); - debug_assert(ret == PSRETURN_OK); - LDR_NonprogressiveLoad(); - ret = g_Game->ReallyStartGame(); - debug_assert(ret == PSRETURN_OK); + InitPs(setup_gui, L"page_pregame.xml"); } - } void RenderGui(bool RenderingState) { g_DoRenderGui = RenderingState; } + + +// Network autostart: + +class AutostartNetServer : public CNetServer +{ +public: + AutostartNetServer(CGame *pGame, CGameAttributes *pGameAttributes, int maxPlayers) : + CNetServer(pGame, pGameAttributes), m_NumPlayers(1), m_MaxPlayers(maxPlayers) + { + } +protected: + virtual void OnPlayerJoin(CNetSession* pSession) + { + for (size_t slot = 0; slot < g_GameAttributes.GetSlotCount(); ++slot) + { + if (g_GameAttributes.GetSlot(slot)->GetAssignment() == SLOT_OPEN) + { + g_GameAttributes.GetSlot(slot)->AssignToSession(pSession); + break; + } + } + + m_NumPlayers++; + + debug_printf(L"# player joined (got %d, need %d)\n", (int)m_NumPlayers, (int)m_MaxPlayers); + + if (m_NumPlayers >= m_MaxPlayers) + { + g_GUI->SwitchPage(L"page_loading.xml", JSVAL_VOID); + int ret = StartGame(); + debug_assert(ret == 0); + } + } + + virtual void OnPlayerLeave(CNetSession* UNUSED(pSession)) + { + debug_warn(L"client left?!"); + m_NumPlayers--; + } + +private: + size_t m_NumPlayers; + size_t m_MaxPlayers; +}; + +class AutostartNetClient : public CNetClient +{ +public: + AutostartNetClient(CGame *pGame, CGameAttributes *pGameAttributes) : + CNetClient(pGame, pGameAttributes) + { + } +protected: + virtual void OnConnectComplete() + { + debug_printf(L"# connect complete\n"); + } + + virtual void OnStartGame() + { + g_GUI->SwitchPage(L"page_loading.xml", JSVAL_VOID); + int ret = StartGame(); + debug_assert(ret == 0); + } +}; + +static bool Autostart(const CmdLineArgs& args) +{ + /* + * Handle various command-line options, for quick testing of various features: + * -autostart=mapname -- single-player + * -autostart=mapname -autostart-playername=Player -autostart-host -autostart-players=2 -- multiplayer host, wait for 2 players + * -autostart=mapname -autostart-playername=Player -autostart-client -autostart-ip=127.0.0.1 -- multiplayer client, connect to 127.0.0.1 + */ + + CStr autostartMap = args.Get("autostart"); + if (autostartMap.empty()) + return false; + + g_Game = new CGame(); + + g_GameAttributes.m_MapFile = autostartMap + ".pmp"; + + // Make the whole world visible + g_GameAttributes.m_LOSSetting = LOS_SETTING_ALL_VISIBLE; + g_GameAttributes.m_FogOfWar = false; + + if (args.Has("autostart-host")) + { + InitPs(true, L"page_loading.xml"); + + size_t maxPlayers = 2; + if (args.Has("autostart-players")) + maxPlayers = args.Get("autostart-players").ToUInt(); + + g_NetServer = new AutostartNetServer(g_Game, &g_GameAttributes, maxPlayers); + // TODO: player name, etc + bool ok = g_NetServer->Start(NULL, 0, NULL); + debug_assert(ok); + } + else if (args.Has("autostart-client")) + { + InitPs(true, L"page_loading.xml"); + + bool ok; + g_NetClient = new AutostartNetClient(g_Game, &g_GameAttributes); + // TODO: player name, etc + ok = g_NetClient->Create(); + debug_assert(ok); + ok = g_NetClient->Connect(args.Get("autostart-ip"), DEFAULT_HOST_PORT); + debug_assert(ok); + } + else + { + for (int i = 1; i < 8; ++i) + g_GameAttributes.GetSlot(i)->AssignLocal(); + + PSRETURN ret = g_Game->StartGame(&g_GameAttributes); + debug_assert(ret == PSRETURN_OK); + LDR_NonprogressiveLoad(); + ret = g_Game->ReallyStartGame(); + debug_assert(ret == PSRETURN_OK); + + InitPs(true, g_UseSimulation2 ? L"page_session_new.xml" : L"page_session.xml"); + } + + return true; +} diff --git a/source/scripting/ScriptingHost.cpp b/source/scripting/ScriptingHost.cpp index caf859e266..6c1ed6f591 100644 --- a/source/scripting/ScriptingHost.cpp +++ b/source/scripting/ScriptingHost.cpp @@ -166,15 +166,6 @@ jsval ScriptingHost::ExecuteScript(const CStrW& script, const CStrW& calledFrom, return rval; } -// unused -void ScriptingHost::RegisterFunction(const std::string & functionName, JSNative function, int numArgs) -{ - JSFunction * func = JS_DefineFunction(m_Context, m_GlobalObject, functionName.c_str(), function, numArgs, 0); - - if (func == NULL) - throw PSERROR_Scripting_RegisterFunctionFailed(); -} - void ScriptingHost::DefineConstant(const std::string & name, int value) { // First remove this constant if it already exists diff --git a/source/scripting/ScriptingHost.h b/source/scripting/ScriptingHost.h index c1359dc58d..c22566e411 100644 --- a/source/scripting/ScriptingHost.h +++ b/source/scripting/ScriptingHost.h @@ -109,8 +109,6 @@ public: jsval ExecuteScript(const CStrW& script, const CStrW& calledFrom = L"Console", JSObject* contextObject = NULL ); - void RegisterFunction(const std::string & functionName, JSNative function, int numArgs); - void DefineConstant(const std::string & name, int value); void DefineCustomObjectType(JSClass *clasp, JSNative constructor, uintN nargs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs); diff --git a/source/scriptinterface/ScriptTypes.h b/source/scriptinterface/ScriptTypes.h index d38b04e10c..e29ce40087 100644 --- a/source/scriptinterface/ScriptTypes.h +++ b/source/scriptinterface/ScriptTypes.h @@ -37,5 +37,6 @@ class ScriptInterface; class CScriptVal; +class CScriptValRooted; #endif // INCLUDED_SCRIPTTYPES diff --git a/source/simulation/TurnManager.cpp b/source/simulation/TurnManager.cpp index 7f733df056..426341a05f 100644 --- a/source/simulation/TurnManager.cpp +++ b/source/simulation/TurnManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -84,15 +84,15 @@ void CTurnManager::SendBatch(uintptr_t batch) MsgVector::iterator it=messages.begin(); while (it != messages.end()) { - SendMessage(it->m_pMessage, it->m_ClientMask); + SendMessageMasked(it->m_pMessage, it->m_ClientMask); ++it; } CEndCommandBatchMessage *pMsg=new CEndCommandBatchMessage(); pMsg->m_TurnLength=m_Batches[batch].m_TurnLength; - SendMessage(pMsg, (size_t)-1); + SendMessageMasked(pMsg, (size_t)-1); } -void CTurnManager::SendMessage(CNetMessage *pMsg, size_t clientMask) +void CTurnManager::SendMessageMasked(CNetMessage *pMsg, size_t clientMask) { for (size_t i=0;i {1, 2, 0} void RotateBatches(); @@ -97,7 +97,7 @@ protected: // void UpdateTimingData(size_t client, int fps, int currentLatency); void SetTurnLength(uintptr_t batch, int turnLength); - void SendMessage(CNetMessage *pMsg, uintptr_t clientMask); + void SendMessageMasked(CNetMessage *pMsg, uintptr_t clientMask); // Add the message to the specified batch. The message is assumed to be // validated before passed here, and will be blindly trusted. diff --git a/source/simulation2/Simulation2.cpp b/source/simulation2/Simulation2.cpp index 4d63d8af74..64dae8a2bd 100644 --- a/source/simulation2/Simulation2.cpp +++ b/source/simulation2/Simulation2.cpp @@ -27,9 +27,14 @@ #include "simulation2/components/ICmpCommandQueue.h" #include "lib/file/file_system_util.h" +#include "lib/utf8.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" +#include "ps/Profile.h" +#include "ps/Pyrogenesis.h" + +#include bool g_UseSimulation2 = true; @@ -37,12 +42,14 @@ class CSimulation2Impl { public: CSimulation2Impl(CUnitManager* unitManager, CTerrain* terrain) : - m_SimContext(), m_ComponentManager(m_SimContext) + m_SimContext(), m_ComponentManager(m_SimContext), m_EnableOOSLog(false) { m_SimContext.m_UnitManager = unitManager; m_SimContext.m_Terrain = terrain; m_ComponentManager.LoadComponentTypes(); +// m_EnableOOSLog = true; // TODO: this should be a command-line flag or similar + // (can't call ResetState here since the scripts haven't been loaded yet) } @@ -86,8 +93,10 @@ public: bool LoadScripts(const VfsPath& path); LibError ReloadChangedFile(const VfsPath& path); - bool Update(float frameTime); - void Interpolate(float frameTime); + bool Update(int turnLength, const std::vector& commands); + void Interpolate(float frameLength, float frameOffset); + + void DumpState(); CSimContext m_SimContext; CComponentManager m_ComponentManager; @@ -99,7 +108,7 @@ public: uint32_t m_TurnNumber; - static const int TURN_LENGTH = 300; // TODO: Use CTurnManager + bool m_EnableOOSLog; }; bool CSimulation2Impl::LoadScripts(const VfsPath& path) @@ -141,51 +150,65 @@ LibError CSimulation2Impl::ReloadChangedFile(const VfsPath& path) return INFO::OK; } -bool CSimulation2Impl::Update(float frameTime) +bool CSimulation2Impl::Update(int turnLength, const std::vector& commands) { - // TODO: Use CTurnManager - m_DeltaTime += frameTime; - if (m_DeltaTime >= 0.0) - { - double turnLength = TURN_LENGTH / 1000.0; - fixed turnLengthFixed = fixed::FromInt(TURN_LENGTH) / 1000; - m_DeltaTime -= turnLength; + fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000; - CMessageTurnStart msgTurnStart; - m_ComponentManager.BroadcastMessage(msgTurnStart); + CMessageTurnStart msgTurnStart; + m_ComponentManager.BroadcastMessage(msgTurnStart); - if (m_TurnNumber == 0 && !m_StartupScript.empty()) - m_ComponentManager.GetScriptInterface().LoadScript(L"map startup script", m_StartupScript); + if (m_TurnNumber == 0 && !m_StartupScript.empty()) + m_ComponentManager.GetScriptInterface().LoadScript(L"map startup script", m_StartupScript); - CmpPtr cmpCommandQueue(m_SimContext, SYSTEM_ENTITY); - if (!cmpCommandQueue.null()) - cmpCommandQueue->ProcessCommands(); + CmpPtr cmpCommandQueue(m_SimContext, SYSTEM_ENTITY); + if (!cmpCommandQueue.null()) + cmpCommandQueue->FlushTurn(commands); - CMessageUpdate msgUpdate(turnLengthFixed); - m_ComponentManager.BroadcastMessage(msgUpdate); + CMessageUpdate msgUpdate(turnLengthFixed); + m_ComponentManager.BroadcastMessage(msgUpdate); - // Clean up any entities destroyed during the simulation update - m_ComponentManager.FlushDestroyedComponents(); + // Clean up any entities destroyed during the simulation update + m_ComponentManager.FlushDestroyedComponents(); -// if (m_TurnNumber == 0) -// m_ComponentManager.GetScriptInterface().DumpHeap(); +// if (m_TurnNumber == 0) +// m_ComponentManager.GetScriptInterface().DumpHeap(); - ++m_TurnNumber; + if (m_EnableOOSLog) + DumpState(); - return true; - } - return false; + ++m_TurnNumber; + + return true; // TODO: don't bother with bool return } -void CSimulation2Impl::Interpolate(float frameTime) +void CSimulation2Impl::Interpolate(float frameLength, float frameOffset) { - // TODO: Use CTurnManager - double turnLength = TURN_LENGTH / 1000.0; - float offset = clamp(m_DeltaTime / turnLength + 1.0, 0.0, 1.0); - CMessageInterpolate msg(frameTime, offset); + CMessageInterpolate msg(frameLength, frameOffset); m_ComponentManager.BroadcastMessage(msg); } +void CSimulation2Impl::DumpState() +{ + PROFILE("DumpState"); + + std::wstringstream name; + name << L"sim_log/" << getpid() << L"/" << std::setw(5) << std::setfill(L'0') << m_TurnNumber << L".txt"; + fs::wpath path (psLogDir()/name.str()); + CreateDirectories(path.branch_path(), 0700); + std::ofstream file (path.external_file_string().c_str(), std::ofstream::out | std::ofstream::trunc); + + file << "State hash: " << std::hex; + std::string hashRaw; + m_ComponentManager.ComputeStateHash(hashRaw); + for (size_t i = 0; i < hashRaw.size(); ++i) + file << std::setfill('0') << std::setw(2) << (int)(unsigned char)hashRaw[i]; + file << std::dec << "\n"; + + file << "\n"; + + m_ComponentManager.DumpDebugState(file); +} + //////////////////////////////////////////////////////////////// CSimulation2::CSimulation2(CUnitManager* unitManager, CTerrain* terrain) : @@ -200,6 +223,11 @@ CSimulation2::~CSimulation2() // Forward all method calls to the appropriate CSimulation2Impl/CComponentManager methods: +void CSimulation2::EnableOOSLog() +{ + m->m_EnableOOSLog = true; +} + entity_id_t CSimulation2::AddEntity(const std::wstring& templateName) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity()); @@ -261,14 +289,20 @@ void CSimulation2::InitGame(const CScriptVal& data) GetScriptInterface().CallFunction(GetScriptInterface().GetGlobalObject(), "InitGame", data, ret); } -bool CSimulation2::Update(float frameTime) +bool CSimulation2::Update(int turnLength) { - return m->Update(frameTime); + std::vector commands; + return m->Update(turnLength, commands); } -void CSimulation2::Interpolate(float frameTime) +bool CSimulation2::Update(int turnLength, const std::vector& commands) { - m->Interpolate(frameTime); + return m->Update(turnLength, commands); +} + +void CSimulation2::Interpolate(float frameLength, float frameOffset) +{ + m->Interpolate(frameLength, frameOffset); } void CSimulation2::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) diff --git a/source/simulation2/Simulation2.h b/source/simulation2/Simulation2.h index 69ed77229a..f918566aca 100644 --- a/source/simulation2/Simulation2.h +++ b/source/simulation2/Simulation2.h @@ -20,6 +20,8 @@ #include "simulation2/system/CmpPtr.h" #include "simulation2/system/Components.h" +#include "simulation2/helpers/SimulationCommand.h" +#include "scriptinterface/ScriptVal.h" #include "lib/file/vfs/vfs_path.h" @@ -32,7 +34,6 @@ class CTerrain; class IComponent; class ScriptInterface; class CMessage; -class CScriptVal; class SceneCollector; class CFrustum; @@ -41,6 +42,7 @@ extern bool g_UseSimulation2; /** * Public API for simulation system. + * Most code should interact with the simulation only through this API. */ class CSimulation2 { @@ -50,6 +52,8 @@ public: CSimulation2(CUnitManager*, CTerrain*); ~CSimulation2(); + void EnableOOSLog(); + /** * Load all scripts in the specified directory (non-recursively), * so they can register new component types and functions. This @@ -89,8 +93,9 @@ public: */ void InitGame(const CScriptVal& data); - bool Update(float frameTime); - void Interpolate(float frameTime); + bool Update(int turnLength); + bool Update(int turnLength, const std::vector& commands); + void Interpolate(float frameLength, float frameOffset); void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling); /** diff --git a/source/simulation2/components/CCmpCommandQueue.cpp b/source/simulation2/components/CCmpCommandQueue.cpp index 39f1ac0feb..bafb27e435 100644 --- a/source/simulation2/components/CCmpCommandQueue.cpp +++ b/source/simulation2/components/CCmpCommandQueue.cpp @@ -21,6 +21,8 @@ #include "ICmpCommandQueue.h" #include "ps/CLogger.h" +#include "ps/Game.h" +#include "network/NetTurnManager.h" class CCmpCommandQueue : public ICmpCommandQueue { @@ -31,13 +33,7 @@ public: DEFAULT_COMPONENT_ALLOCATOR(CommandQueue) - struct Command - { - int player; - CScriptValRooted data; - }; - - std::vector m_CmdQueue; + std::vector m_LocalQueue; static std::string GetSchema() { @@ -54,11 +50,11 @@ public: virtual void Serialize(ISerializer& serialize) { - serialize.NumberU32_Unbounded("num commands", (u32)m_CmdQueue.size()); - for (size_t i = 0; i < m_CmdQueue.size(); ++i) + serialize.NumberU32_Unbounded("num commands", (u32)m_LocalQueue.size()); + for (size_t i = 0; i < m_LocalQueue.size(); ++i) { - serialize.NumberI32_Unbounded("player", m_CmdQueue[i].player); - serialize.ScriptVal("data", m_CmdQueue[i].data.get()); + serialize.NumberI32_Unbounded("player", m_LocalQueue[i].player); + serialize.ScriptVal("data", m_LocalQueue[i].data.get()); } } @@ -74,31 +70,47 @@ public: jsval data; deserialize.NumberI32_Unbounded(player); deserialize.ScriptVal(data); - Command c = { player, CScriptValRooted(cx, data) }; - m_CmdQueue.push_back(c); + SimulationCommand c = { player, CScriptValRooted(cx, data) }; + m_LocalQueue.push_back(c); } } - virtual void PushClientCommand(int player, CScriptVal cmd) + virtual void PushLocalCommand(int player, CScriptVal cmd) { JSContext* cx = GetSimContext().GetScriptInterface().GetContext(); - Command c = { player, CScriptValRooted(cx, cmd) }; - m_CmdQueue.push_back(c); + SimulationCommand c = { player, CScriptValRooted(cx, cmd) }; + m_LocalQueue.push_back(c); } - virtual void ProcessCommands() + virtual void PostNetworkCommand(CScriptVal cmd) + { + JSContext* cx = GetSimContext().GetScriptInterface().GetContext(); + + // TODO: would be nicer to not use globals + g_Game->GetTurnManager()->PostCommand(CScriptValRooted(cx, cmd)); + } + + virtual void FlushTurn(const std::vector& commands) { ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); - for (size_t i = 0; i < m_CmdQueue.size(); ++i) + std::vector localCommands; + m_LocalQueue.swap(localCommands); + + for (size_t i = 0; i < localCommands.size(); ++i) { - bool ok = scriptInterface.CallFunctionVoid(scriptInterface.GetGlobalObject(), "ProcessCommand", m_CmdQueue[i].player, m_CmdQueue[i].data); + bool ok = scriptInterface.CallFunctionVoid(scriptInterface.GetGlobalObject(), "ProcessCommand", localCommands[i].player, localCommands[i].data); if (!ok) LOGERROR(L"Failed to call ProcessCommand() global script function"); } - m_CmdQueue.clear(); + for (size_t i = 0; i < commands.size(); ++i) + { + bool ok = scriptInterface.CallFunctionVoid(scriptInterface.GetGlobalObject(), "ProcessCommand", commands[i].player, commands[i].data); + if (!ok) + LOGERROR(L"Failed to call ProcessCommand() global script function"); + } } }; diff --git a/source/simulation2/components/ICmpCommandQueue.cpp b/source/simulation2/components/ICmpCommandQueue.cpp index 213963c103..535e09db54 100644 --- a/source/simulation2/components/ICmpCommandQueue.cpp +++ b/source/simulation2/components/ICmpCommandQueue.cpp @@ -22,6 +22,7 @@ #include "simulation2/system/InterfaceScripted.h" BEGIN_INTERFACE_WRAPPER(CommandQueue) -DEFINE_INTERFACE_METHOD_2("PushClientCommand", void, ICmpCommandQueue, PushClientCommand, int, CScriptVal) -// Excluded: ProcessCommands (doesn't make sense for scripts to call it) +DEFINE_INTERFACE_METHOD_2("PushLocalCommand", void, ICmpCommandQueue, PushLocalCommand, int, CScriptVal) +DEFINE_INTERFACE_METHOD_1("PostNetworkCommand", void, ICmpCommandQueue, PostNetworkCommand, CScriptVal) +// Excluded: FlushTurn (doesn't make sense for scripts to call it) END_INTERFACE_WRAPPER(CommandQueue) diff --git a/source/simulation2/components/ICmpCommandQueue.h b/source/simulation2/components/ICmpCommandQueue.h index 2fd29adfa6..1a1776e87b 100644 --- a/source/simulation2/components/ICmpCommandQueue.h +++ b/source/simulation2/components/ICmpCommandQueue.h @@ -20,35 +20,38 @@ #include "simulation2/system/Interface.h" +#include "simulation2/helpers/SimulationCommand.h" + /** * Command queue, for sending orders to entities. * Each command is associated with a player ID (who triggered the command, in some sense) * and an arbitrary script value. - * Commands can be added to the queue at any time, and will all be executed at the start - * of the next turn. * - * Typically commands will be sent by the GUI to the networking system, which will eventually - * add them to this queue; the player ID identifies the client who initiated the action, and - * the command processing code ought to check the player has permission to perform that command - * (e.g. make sure they only move their own units). The networking system will add a few turns of - * latency before the commands reach the queue. + * Commands can be added to the local queue at any time, and will all be executed at the start + * of the next turn. (This will typically be used by AI scripts.) * - * Alternatively, commands may be added directly to the queue by AI scripts, to emulate a player, - * in which case the ID identifies the AI player and the network is not involved. + * Alternatively, commands can be sent to the networking system, and they will be executed + * at the start of some later turn by all players simultaneously. (This will typically be + * used for user inputs.) */ class ICmpCommandQueue : public IComponent { public: /** - * Pushes a new command onto the queue. @p cmd does not need to be rooted. + * Pushes a new command onto the local queue. @p cmd does not need to be rooted. */ - virtual void PushClientCommand(int player, CScriptVal cmd) = 0; + virtual void PushLocalCommand(int player, CScriptVal cmd) = 0; /** - * Calls the ProcessCommand(player, cmd) global script function for each command in the queue, - * in order of adding to the queue, and empties the queue. + * Send a command associated with the current player to the networking system. */ - virtual void ProcessCommands() = 0; + virtual void PostNetworkCommand(CScriptVal cmd) = 0; + + /** + * Calls the ProcessCommand(player, cmd) global script function for each command in the + * local queue and in @p commands, and empties the local queue. + */ + virtual void FlushTurn(const std::vector& commands) = 0; DECLARE_INTERFACE_TYPE(CommandQueue) }; diff --git a/source/simulation2/components/tests/test_CommandQueue.h b/source/simulation2/components/tests/test_CommandQueue.h index 15313f2b08..ec0998c9ce 100644 --- a/source/simulation2/components/tests/test_CommandQueue.h +++ b/source/simulation2/components/tests/test_CommandQueue.h @@ -36,6 +36,8 @@ public: { ComponentTestHelper test; + std::vector empty; + ICmpCommandQueue* cmp = test.Add(CID_CommandQueue, ""); TS_ASSERT(test.GetScriptInterface().Eval("var cmds = []; function ProcessCommand(player, cmd) { cmds.push([player, cmd]); }")); @@ -43,24 +45,24 @@ public: CScriptVal cmd; TS_ASSERT(test.GetScriptInterface().Eval("([1,2,3])", cmd)); - cmp->PushClientCommand(1, cmd); + cmp->PushLocalCommand(1, cmd); TS_ASSERT(test.GetScriptInterface().Eval("({x:4})", cmd)); - cmp->PushClientCommand(-1, cmd); + cmp->PushLocalCommand(-1, cmd); test.Roundtrip(); // Process the first two commands - cmp->ProcessCommands(); + cmp->FlushTurn(empty); TS_ASSERT(test.GetScriptInterface().Eval("({y:5})", cmd)); - cmp->PushClientCommand(10, cmd); + cmp->PushLocalCommand(10, cmd); // Process the next command - cmp->ProcessCommands(); + cmp->FlushTurn(empty); // Process no commands - cmp->ProcessCommands(); + cmp->FlushTurn(empty); test.Roundtrip(); diff --git a/source/simulation2/helpers/SimulationCommand.h b/source/simulation2/helpers/SimulationCommand.h new file mode 100644 index 0000000000..db9ea3cdf6 --- /dev/null +++ b/source/simulation2/helpers/SimulationCommand.h @@ -0,0 +1,32 @@ +/* Copyright (C) 2010 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_SIMULATIONCOMMAND +#define INCLUDED_SIMULATIONCOMMAND + +#include "scriptinterface/ScriptVal.h" + +/** + * Simulation command, typically received over the network in multiplayer games. + */ +struct SimulationCommand +{ + int player; + CScriptValRooted data; +}; + +#endif // INCLUDED_SIMULATIONCOMMAND diff --git a/source/simulation2/serialization/ISerializer.cpp b/source/simulation2/serialization/ISerializer.cpp index 89b46f85b0..467117d0e7 100644 --- a/source/simulation2/serialization/ISerializer.cpp +++ b/source/simulation2/serialization/ISerializer.cpp @@ -116,6 +116,11 @@ void ISerializer::ScriptVal(const char* name, CScriptVal value) PutScriptVal(name, value.get()); } +void ISerializer::ScriptVal(const char* name, CScriptValRooted value) +{ + PutScriptVal(name, value.get()); +} + void ISerializer::RawBytes(const char* name, const u8* data, size_t len) { Put(name, data, len); diff --git a/source/simulation2/serialization/ISerializer.h b/source/simulation2/serialization/ISerializer.h index 60668bdd1a..8e336d9068 100644 --- a/source/simulation2/serialization/ISerializer.h +++ b/source/simulation2/serialization/ISerializer.h @@ -184,6 +184,12 @@ public: */ void ScriptVal(const char* name, CScriptVal value); + /** + * Serialize a CScriptValRooted. + * The value must not contain any unserializable values (like functions). + */ + void ScriptVal(const char* name, CScriptValRooted value); + /** * Serialize a stream of bytes. * It is the caller's responsibility to deal with portability (padding, endianness, etc); diff --git a/source/simulation2/serialization/StdDeserializer.cpp b/source/simulation2/serialization/StdDeserializer.cpp index 8bc182b908..4bcd520918 100644 --- a/source/simulation2/serialization/StdDeserializer.cpp +++ b/source/simulation2/serialization/StdDeserializer.cpp @@ -185,6 +185,11 @@ void CStdDeserializer::ScriptVal(CScriptVal& out) out = ReadScriptVal(NULL); } +void CStdDeserializer::ScriptVal(CScriptValRooted& out) +{ + out = CScriptValRooted(m_ScriptInterface.GetContext(), ReadScriptVal(NULL)); +} + void CStdDeserializer::ScriptObjectAppend(jsval& obj) { if (!JSVAL_IS_OBJECT(obj)) diff --git a/source/simulation2/serialization/StdDeserializer.h b/source/simulation2/serialization/StdDeserializer.h index 958141e423..6cde89f4b1 100644 --- a/source/simulation2/serialization/StdDeserializer.h +++ b/source/simulation2/serialization/StdDeserializer.h @@ -31,6 +31,7 @@ public: virtual void ScriptVal(jsval& out); virtual void ScriptVal(CScriptVal& out); + virtual void ScriptVal(CScriptValRooted& out); virtual void ScriptObjectAppend(jsval& obj); virtual void ScriptString(JSString*& out); diff --git a/source/tools/atlas/GameInterface/ActorViewer.cpp b/source/tools/atlas/GameInterface/ActorViewer.cpp index bb3ac764e4..485b8f93f5 100644 --- a/source/tools/atlas/GameInterface/ActorViewer.cpp +++ b/source/tools/atlas/GameInterface/ActorViewer.cpp @@ -305,8 +305,8 @@ void ActorViewer::Render() void ActorViewer::Update(float dt) { - m.Simulation2.Update(dt); - m.Simulation2.Interpolate(dt); + m.Simulation2.Update((int)(dt*1000)); + m.Simulation2.Interpolate(dt, 0); if (m.WalkEnabled && m.CurrentSpeed) { diff --git a/source/tools/atlas/GameInterface/View.cpp b/source/tools/atlas/GameInterface/View.cpp index 58e0d2de03..4650de3e1e 100644 --- a/source/tools/atlas/GameInterface/View.cpp +++ b/source/tools/atlas/GameInterface/View.cpp @@ -179,7 +179,7 @@ void ViewGame::Update(float frameLength) { // Update unit interpolation if (g_UseSimulation2) - g_Game->GetSimulation2()->Interpolate(0.0); + g_Game->Interpolate(0.0); else g_Game->GetSimulation()->Interpolate(0.0); } @@ -205,7 +205,7 @@ void ViewGame::Update(float frameLength) // not in every call to g_Game->Update if (g_UseSimulation2) { - g_Game->GetSimulation2()->Interpolate(actualFrameLength); + g_Game->Interpolate(actualFrameLength); } else {