1
0
forked from 0ad/0ad

Run network server in a separate thread, to minimise latency.

Make some parts of logger and console thread-safe.
Simplify console: remove special commands; remove prefix for script
commands.
Remove LOG_ONCE, to simplify logger.
Change some code to use the new logger interface.
Fix OOS when revealing map.
Clean up some formatting.

This was SVN commit r8511.
This commit is contained in:
Ykkrosh 2010-10-31 22:00:28 +00:00
parent 034504f536
commit 5d764f1435
37 changed files with 862 additions and 789 deletions

View File

@ -31,26 +31,27 @@ function init(data)
var ldTitleBar = getGUIObjectByName ("ldTitleBar");
var ldText = getGUIObjectByName ("ldText");
switch(data.attribs.mapType)
if (data)
{
case "scenario":
ldTitleBar.caption = "Loading Scenario";
ldText.caption = "Loading " + mapName + "\nPlease wait...";
break;
case "random":
ldTitleBar.caption = "Loading Random Map";
ldText.caption = "Generating " + mapName + "\nPlease wait...";
break;
default:
error("Unkown map type: "+data.attribs.mapType);
switch (data.attribs.mapType)
{
case "scenario":
ldTitleBar.caption = "Loading Scenario";
ldText.caption = "Loading " + mapName + "\nPlease wait...";
break;
case "random":
ldTitleBar.caption = "Loading Random Map";
ldText.caption = "Generating " + mapName + "\nPlease wait...";
break;
default:
error("Unkown map type: "+data.attribs.mapType);
}
}
getGUIObjectByName ("ldProgressBarText").caption = "";
getGUIObjectByName ("ldProgressBar").caption = 0;
getGUIObjectByName("ldProgressBarText").caption = "";
getGUIObjectByName("ldProgressBar").caption = 0;
// Pick a random tip of the day (each line is a separate tip).
var tipArray = readFileLines("gui/text/tips.txt");
@ -73,5 +74,5 @@ function reallyStartGame()
Engine.SwitchGuiPage("page_session_new.xml", g_Data);
// Restore default cursor.
setCursor ("arrow-default");
setCursor("arrow-default");
}

View File

@ -137,7 +137,7 @@
<object size="0 112 100%-18 128" type="text" style="devCommandsText">Reveal map</object>
<object size="100%-16 112 100% 128" type="checkbox" style="wheatCrossBox">
<action on="Press">Engine.SetRevealMap(this.checked);</action>
<action on="Press">Engine.PostNetworkCommand({"type": "reveal-map", "enable": this.checked});</action>
</object>
</object>

View File

@ -263,7 +263,6 @@ GuiInterface.prototype.SetStatusBars = function(player, cmd)
}
};
/**
/**
* Displays the rally point of a building
*/

View File

@ -11,6 +11,11 @@ function ProcessCommand(player, cmd)
print(cmd.message);
break;
case "reveal-map":
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetLosRevealAll(cmd.enable);
break;
case "walk":
var cmpUnitAI = GetFormationUnitAI(cmd.entities);
if (cmpUnitAI)

View File

@ -26,8 +26,6 @@
#include "ps/DllLoader.h"
#include "ps/Filesystem.h"
#define LOG_CATEGORY L"collada"
namespace Collada
{
#include "collada/DLL.h"
@ -37,8 +35,12 @@ namespace
{
void ColladaLog(int severity, const char* text)
{
const CLogger::ELogMethod method = severity == LOG_INFO ? CLogger::Normal : severity == LOG_WARNING ? CLogger::Warning : CLogger::Error;
LOG(method, LOG_CATEGORY, L"%hs", text);
if (severity == LOG_INFO)
LOGMESSAGE(L"Collada message: %hs", text);
else if (severity == LOG_WARNING)
LOGWARNING(L"Collada warning: %hs", text);
else
LOGERROR(L"Collada error: %hs", text);
}
void ColladaOutput(void* cb_data, const char* data, unsigned int length)
@ -81,7 +83,7 @@ public:
{
if (! dll.LoadDLL())
{
LOG_ONCE(CLogger::Error, LOG_CATEGORY, L"Failed to load COLLADA conversion DLL");
LOGERROR(L"Failed to load COLLADA conversion DLL");
return false;
}
@ -94,7 +96,7 @@ public:
}
catch (PSERROR_DllLoader&)
{
LOG(CLogger::Error, LOG_CATEGORY, L"Failed to load symbols from COLLADA conversion DLL");
LOGERROR(L"Failed to load symbols from COLLADA conversion DLL");
dll.Unload();
return false;
}
@ -104,7 +106,7 @@ public:
CVFSFile skeletonFile;
if (skeletonFile.Load(g_VFS, L"art/skeletons/skeletons.xml") != PSRETURN_OK)
{
LOG(CLogger::Error, LOG_CATEGORY, L"Failed to read skeleton definitions");
LOGERROR(L"Failed to read skeleton definitions");
dll.Unload();
return false;
}
@ -112,7 +114,7 @@ public:
int ok = set_skeleton_definitions((const char*)skeletonFile.GetBuffer(), (int)skeletonFile.GetBufferSize());
if (ok < 0)
{
LOG(CLogger::Error, LOG_CATEGORY, L"Failed to load skeleton definitions");
LOGERROR(L"Failed to load skeleton definitions");
dll.Unload();
return false;
}
@ -214,7 +216,7 @@ VfsPath CColladaManager::GetLoadableFilename(const VfsPath& pathnameNoExtension,
if (g_VFS->GetFileInfo(dae, &fileInfo) < 0)
{
// This shouldn't occur for any sensible reasons
LOG(CLogger::Error, LOG_CATEGORY, L"Failed to stat DAE file '%ls'", dae.string().c_str());
LOGERROR(L"Failed to stat DAE file '%ls'", dae.string().c_str());
return VfsPath();
}

View File

@ -66,8 +66,6 @@
* If now > target time, switch to 'STATIONARY, NO TOOLTIP'
*/
#define LOG_CATEGORY L"gui"
enum
{
ST_IN_MOTION,
@ -87,9 +85,11 @@ const double CooldownTime = 0.25; // TODO: Don't hard-code this value
bool GUITooltip::GetTooltip(IGUIObject* obj, CStr& style)
{
if (obj && obj->SettingExists("_icon_tooltip_style") && obj->MouseOverIcon())
{ //Special code to handle icon tooltips in text objects
if(GUI<CStr>::GetSetting(obj, "_icon_tooltip_style", style) == PSRETURN_OK)
{ //Check if icon tooltip text exists
{
// Special code to handle icon tooltips in text objects
if (GUI<CStr>::GetSetting(obj, "_icon_tooltip_style", style) == PSRETURN_OK)
{
// Check if icon tooltip text exists
CStr text;
if (GUI<CStr>::GetSetting(obj, "_icon_tooltip", text) == PSRETURN_OK)
{
@ -100,15 +100,17 @@ bool GUITooltip::GetTooltip(IGUIObject* obj, CStr& style)
}
if (style.empty())
{
// Text, but no style - use default
style = "default";
}
m_IsIconTooltip = true;
return true;
}
}
} else if (obj && obj->SettingExists("tooltip_style")
}
else if (obj && obj->SettingExists("tooltip_style")
&& GUI<CStr>::GetSetting(obj, "tooltip_style", style) == PSRETURN_OK)
{
CStr text;
@ -121,13 +123,16 @@ bool GUITooltip::GetTooltip(IGUIObject* obj, CStr& style)
}
if (style.empty())
{
// Text, but no style - use default
style = "default";
}
m_IsIconTooltip = false;
return true;
}
}
// Failed while retrieving tooltip_style or tooltip
return false;
}
@ -144,7 +149,7 @@ void GUITooltip::ShowTooltip(IGUIObject* obj, CPos pos, const CStr& style, CGUI*
IGUIObject* tooltipobj = gui->FindObjectByName("__tooltip_"+style);
if (! tooltipobj)
{
LOG_ONCE(CLogger::Error, LOG_CATEGORY, L"Cannot find tooltip named '%hs'", style.c_str());
LOGERROR(L"Cannot find tooltip named '%hs'", style.c_str());
return;
}
@ -157,7 +162,7 @@ void GUITooltip::ShowTooltip(IGUIObject* obj, CPos pos, const CStr& style, CGUI*
usedobj = gui->FindObjectByName(usedObjectName);
if (! usedobj)
{
LOG_ONCE(CLogger::Error, LOG_CATEGORY, L"Cannot find object named '%hs' used by tooltip '%hs'", usedObjectName.c_str(), style.c_str());
LOGERROR(L"Cannot find object named '%hs' used by tooltip '%hs'", usedObjectName.c_str(), style.c_str());
return;
}
@ -178,12 +183,14 @@ void GUITooltip::ShowTooltip(IGUIObject* obj, CPos pos, const CStr& style, CGUI*
// Retrieve object's 'tooltip' setting
CStr text;
if (m_IsIconTooltip)
{ //Use icon tooltip property
{
// Use icon tooltip property
if (GUI<CStr>::GetSetting(obj, "_icon_tooltip", text) != PSRETURN_OK)
debug_warn(L"Failed to retrieve icon tooltip text"); // shouldn't fail
}
else
{ //use normal tooltip property
{
// Use normal tooltip property
if (GUI<CStr>::GetSetting(obj, "tooltip", text) != PSRETURN_OK)
debug_warn(L"Failed to retrieve tooltip text"); // shouldn't fail
}
@ -208,7 +215,7 @@ void GUITooltip::HideTooltip(const CStr& style, CGUI* gui)
IGUIObject* tooltipobj = gui->FindObjectByName("__tooltip_"+style);
if (! tooltipobj)
{
LOG_ONCE(CLogger::Error, LOG_CATEGORY, L"Cannot find tooltip named '%hs'", style.c_str());
LOGERROR(L"Cannot find tooltip named '%hs'", style.c_str());
return;
}
@ -219,7 +226,7 @@ void GUITooltip::HideTooltip(const CStr& style, CGUI* gui)
IGUIObject* usedobj = gui->FindObjectByName(usedObjectName);
if (! usedobj)
{
LOG_ONCE(CLogger::Error, LOG_CATEGORY, L"Cannot find object named '%hs' used by tooltip '%hs'", usedObjectName.c_str(), style.c_str());
LOGERROR(L"Cannot find object named '%hs' used by tooltip '%hs'", usedObjectName.c_str(), style.c_str());
return;
}
@ -249,7 +256,7 @@ static int GetTooltipDelay(CStr& style, CGUI* gui)
IGUIObject* tooltipobj = gui->FindObjectByName("__tooltip_"+style);
if (! tooltipobj)
{
LOG_ONCE(CLogger::Error, LOG_CATEGORY, L"Cannot find tooltip object named '%hs'", style.c_str());
LOGERROR(L"Cannot find tooltip object named '%hs'", style.c_str());
return delay;
}
GUI<int>::GetSetting(tooltipobj, "delay", delay);
@ -386,8 +393,6 @@ void GUITooltip::Update(IGUIObject* Nearest, CPos MousePos, CGUI* GUI)
m_State = nextstate;
}
m_PreviousMousePos = MousePos;
m_PreviousObject = Nearest;
}

View File

@ -188,11 +188,7 @@ void SetNetworkGameAttributes(void* cbdata, CScriptVal attribs)
debug_assert(g_NetServer);
// Convert from GUI script context to net server script context
CScriptValRooted gameAttribs (g_NetServer->GetScriptInterface().GetContext(),
g_NetServer->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), attribs.get()));
g_NetServer->UpdateGameAttributes(gameAttribs);
g_NetServer->UpdateGameAttributes(attribs, guiManager->GetScriptInterface());
}
void StartNetworkHost(void* cbdata, std::wstring playerName)
@ -305,18 +301,6 @@ CScriptVal LoadMapSettings(void* cbdata, std::wstring pathname)
return reader.GetMapSettings(guiManager->GetScriptInterface()).get();
}
void SetRevealMap(void* UNUSED(cbdata), bool enabled)
{
if (!g_Game)
return;
CSimulation2* sim = g_Game->GetSimulation2();
debug_assert(sim);
CmpPtr<ICmpRangeManager> cmpRangeManager(*sim, SYSTEM_ENTITY);
if (!cmpRangeManager.null())
cmpRangeManager->SetLosRevealAll(enabled);
}
/**
* Start / stop camera following mode
* @param entityid unit id to follow. If zero, stop following mode
@ -406,7 +390,6 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
scriptInterface.RegisterFunction<void, &RestartInAtlas>("RestartInAtlas");
scriptInterface.RegisterFunction<bool, &AtlasIsAvailable>("AtlasIsAvailable");
scriptInterface.RegisterFunction<CScriptVal, std::wstring, &LoadMapSettings>("LoadMapSettings");
scriptInterface.RegisterFunction<void, bool, &SetRevealMap>("SetRevealMap");
scriptInterface.RegisterFunction<void, entity_id_t, &CameraFollow>("CameraFollow");
// Development/debugging functions

View File

@ -305,8 +305,6 @@ static void Frame()
}
PROFILE_START("network poll");
if (g_NetServer)
g_NetServer->Poll();
if (g_NetClient)
g_NetClient->Poll();
PROFILE_END("network poll");
@ -354,8 +352,6 @@ static void Frame()
// Immediately flush any messages produced by simulation code
PROFILE_START("network flush");
if (g_NetServer)
g_NetServer->Flush();
if (g_NetClient)
g_NetClient->Flush();
PROFILE_END("network flush");

View File

@ -27,121 +27,81 @@
#define ALLNETMSGS_IMPLEMENT
#include "NetMessages.h"
//-----------------------------------------------------------------------------
// Name: CNetMessage()
// Desc: Constructor
//-----------------------------------------------------------------------------
CNetMessage::CNetMessage( void )
CNetMessage::CNetMessage()
{
m_Type = NMT_INVALID;
}
//-----------------------------------------------------------------------------
// Name: CNetMessage()
// Desc: Constructor
//-----------------------------------------------------------------------------
CNetMessage::CNetMessage( NetMessageType type )
CNetMessage::CNetMessage(NetMessageType type)
{
m_Type = type;
}
//-----------------------------------------------------------------------------
// Name: ~CNetMessage()
// Desc: Destructor
//-----------------------------------------------------------------------------
CNetMessage::~CNetMessage( void )
CNetMessage::~CNetMessage()
{
}
//-----------------------------------------------------------------------------
// Name: Serialize()
// Desc: Serializes the message into the buffer parameter
//-----------------------------------------------------------------------------
u8* CNetMessage::Serialize( u8* pBuffer ) const
u8* CNetMessage::Serialize(u8* pBuffer) const
{
// Validate parameters
if ( !pBuffer ) return NULL;
size_t size = GetSerializedLength();
Serialize_int_1( pBuffer, m_Type );
Serialize_int_2( pBuffer, size );
Serialize_int_1(pBuffer, m_Type);
Serialize_int_2(pBuffer, size);
return pBuffer;
}
//-----------------------------------------------------------------------------
// Name: Deserialize()
// Desc: Loads this message from the specified buffer parameter
//-----------------------------------------------------------------------------
const u8* CNetMessage::Deserialize( const u8* pStart, const u8* pEnd )
const u8* CNetMessage::Deserialize(const u8* pStart, const u8* pEnd)
{
// Validate parameters
if ( !pStart || !pEnd ) return NULL;
debug_assert( pStart + 3 <= pEnd );
if (pStart + 3 > pEnd)
{
LOGERROR(L"CNetMessage: Corrupt packet (smaller than header)");
return NULL;
}
const u8* pBuffer = pStart;
int type;
size_t size;
Deserialize_int_1( pBuffer, type );
Deserialize_int_2( pBuffer, size );
Deserialize_int_1(pBuffer, type);
Deserialize_int_2(pBuffer, size);
m_Type = (NetMessageType)type;
debug_assert( pStart + size == pEnd );
if (pStart + size != pEnd)
{
LOGERROR(L"CNetMessage: Corrupt packet (incorrect size)");
return NULL;
}
return pBuffer;
}
//-----------------------------------------------------------------------------
// Name: GetSerializedLength()
// Desc: Returns the size of the serialized message
//-----------------------------------------------------------------------------
size_t CNetMessage::GetSerializedLength( void ) const
size_t CNetMessage::GetSerializedLength() const
{
// By default, return header size
return 3;
}
//-----------------------------------------------------------------------------
// Name: ToString()
// Desc: Returns a string representation for the message
//-----------------------------------------------------------------------------
CStr CNetMessage::ToString( void ) const
CStr CNetMessage::ToString() const
{
CStr ret;
// This is called only when the subclass doesn't override it
// Not defined yet?
if ( GetType() == NMT_INVALID )
{
ret = "MESSAGE_TYPE_NONE { Undefined Message }";
}
if (GetType() == NMT_INVALID)
return "MESSAGE_TYPE_NONE { Undefined Message }";
else
{
ret = " Unknown Message " + CStr( GetType() );
}
return ret;
return "Unknown Message " + CStr(GetType());
}
//-----------------------------------------------------------------------------
// Name: CreateMessage()
// Desc: Creates the appropriate message based on the given data
//-----------------------------------------------------------------------------
CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
size_t dataSize,
ScriptInterface& scriptInterface)
{
CNetMessage* pNewMessage = NULL;
CNetMessage header;
// Validate parameters
if ( !pData ) return NULL;
CNetMessage* pNewMessage = NULL;
CNetMessage header;
// Figure out message type
header.Deserialize( ( const u8* )pData, ( const u8* )pData + dataSize );
header.Deserialize((const u8*)pData, (const u8*)pData + dataSize);
switch ( header.GetType() )
switch (header.GetType())
{
case NMT_GAME_SETUP:
pNewMessage = new CGameSetupMessage(scriptInterface);
@ -204,34 +164,8 @@ CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
break;
}
if ( pNewMessage )
pNewMessage->Deserialize( ( const u8* )pData, ( const u8* )pData + dataSize );
if (pNewMessage)
pNewMessage->Deserialize((const u8*)pData, (const u8*)pData + dataSize);
return pNewMessage;
}
CNetMessage* CNetMessageFactory::CloneMessage( const CNetMessage* message, ScriptInterface& scriptInterface )
{
// TODO: maybe this could be implemented more efficiently,
// particularly for script messages where serialisation is
// relatively expensive
size_t len = message->GetSerializedLength();
u8* buffer = new u8[len];
u8* bufend = message->Serialize(buffer);
if (!bufend)
{
delete[] buffer;
return NULL;
}
debug_assert(bufend == buffer+len);
CNetMessage* ret = CreateMessage(buffer, len, scriptInterface);
delete[] buffer;
return ret;
}

View File

@ -37,15 +37,15 @@ class CNetMessage : public ISerializable
public:
CNetMessage( void );
CNetMessage( NetMessageType type );
virtual ~CNetMessage( void );
CNetMessage();
CNetMessage(NetMessageType type);
virtual ~CNetMessage();
/**
* Retrieves the message type.
* @return Message type
*/
NetMessageType GetType( void ) const { return m_Type; }
NetMessageType GetType() const { return m_Type; }
/**
* Serialize the message into the specified buffer parameter. The size
@ -58,7 +58,7 @@ public:
* @return The position in the buffer right after the
* serialized message
*/
virtual u8* Serialize( u8* pBuffer ) const;
virtual u8* Serialize(u8* pBuffer) const;
/**
* Deserializes the message from the specified buffer.
@ -68,7 +68,7 @@ public:
* @return The position in the buffer right after the
* message or NULL if an error occured
*/
virtual const u8* Deserialize( const u8* pStart, const u8* pEnd );
virtual const u8* Deserialize(const u8* pStart, const u8* pEnd);
/**
* Retrieves the size in bytes of the serialized message. Before calling
@ -84,17 +84,15 @@ public:
*
* @return The message as a string
*/
virtual CStr ToString( void ) const;
virtual CStr ToString() const;
private:
NetMessageType m_Type; // Message type
};
/*
CLASS : CNetMessageFactory
DESCRIPTION : Creates messages from data received through the network
NOTES : It follows the factory method pattern implementation
*/
/**
* Creates messages from data received through the network.
*/
class CNetMessageFactory
{
public:
@ -107,15 +105,7 @@ public:
* @param scriptInterface Script instance to use when constructing scripted messages
* @return The new message created
*/
static CNetMessage* CreateMessage( const void* pData, size_t dataSize, ScriptInterface& scriptInterface );
/**
* Clone a message object into a new scripting context.
* @param message message to clone (can come from any script context)
* @param scriptInterface script context to use for the new message
* @return new message, or NULL on failure
*/
static CNetMessage* CloneMessage( const CNetMessage* message, ScriptInterface& scriptInterface );
static CNetMessage* CreateMessage(const void* pData, size_t dataSize, ScriptInterface& scriptInterface);
};
/**

View File

@ -25,6 +25,7 @@
#include "NetStats.h"
#include "NetTurnManager.h"
#include "lib/utf8.h"
#include "lib/external_libraries/enet.h"
#include "ps/CLogger.h"
#include "scriptinterface/ScriptInterface.h"
@ -34,6 +35,13 @@
#define DEFAULT_WELCOME_MESSAGE L"Welcome"
#define MAX_CLIENTS 8
/**
* enet_host_service timeout (msecs).
* Smaller numbers may hurt performance; larger numbers will
* hurt latency responding to messages from game thread.
*/
static const int HOST_SERVICE_TIMEOUT = 50;
CNetServer* g_NetServer = NULL;
static CStr DebugName(CNetServerSession* session)
@ -45,8 +53,17 @@ static CStr DebugName(CNetServerSession* session)
return "[" + session->GetGUID().substr(0, 8) + "...]";
}
CNetServer::CNetServer() :
m_ScriptInterface(new ScriptInterface("Engine", "Net server")), m_NextHostID(1), m_Host(NULL), m_Stats(NULL)
/*
* XXX: We use some non-threadsafe functions from the worker thread.
* See http://trac.wildfiregames.com/ticket/654
* and http://trac.wildfiregames.com/ticket/653
*/
CNetServerWorker::CNetServerWorker(int autostartPlayers) :
m_AutostartPlayers(autostartPlayers),
m_Shutdown(false),
m_ScriptInterface(NULL),
m_NextHostID(1), m_Host(NULL), m_Stats(NULL)
{
m_State = SERVER_STATE_UNCONNECTED;
@ -56,8 +73,22 @@ CNetServer::CNetServer() :
m_WelcomeMessage = DEFAULT_WELCOME_MESSAGE;
}
CNetServer::~CNetServer()
CNetServerWorker::~CNetServerWorker()
{
if (m_State != SERVER_STATE_UNCONNECTED)
{
// Tell the thread to shut down
{
CScopeLock lock(m_WorkerMutex);
m_Shutdown = true;
}
// Wait for it to shut down cleanly
pthread_join(m_WorkerThread, NULL);
}
// Clean up resources
delete m_Stats;
for (size_t i = 0; i < m_Sessions.size(); ++i)
@ -72,12 +103,9 @@ CNetServer::~CNetServer()
}
delete m_ServerTurnManager;
m_GameAttributes = CScriptValRooted(); // clear root before deleting its context
delete m_ScriptInterface;
}
bool CNetServer::SetupConnection()
bool CNetServerWorker::SetupConnection()
{
debug_assert(m_State == SERVER_STATE_UNCONNECTED);
debug_assert(!m_Host);
@ -95,16 +123,20 @@ bool CNetServer::SetupConnection()
return false;
}
m_Stats = new CNetStatsTable(m_Host);
m_Stats = new CNetStatsTable();
if (CProfileViewer::IsInitialised())
g_ProfileViewer.AddRootTable(m_Stats);
m_State = SERVER_STATE_PREGAME;
// Launch the worker thread
int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
debug_assert(ret == 0);
return true;
}
bool CNetServer::SendMessage(ENetPeer* peer, const CNetMessage* message)
bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
{
debug_assert(m_Host);
@ -113,7 +145,7 @@ bool CNetServer::SendMessage(ENetPeer* peer, const CNetMessage* message)
return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
}
bool CNetServer::Broadcast(const CNetMessage* message)
bool CNetServerWorker::Broadcast(const CNetMessage* message)
{
debug_assert(m_Host);
@ -135,103 +167,178 @@ bool CNetServer::Broadcast(const CNetMessage* message)
return ok;
}
void CNetServer::Poll()
void* CNetServerWorker::RunThread(void* data)
{
debug_assert(m_Host);
debug_SetThreadName("NetServer");
// Poll host for events
ENetEvent event;
while (enet_host_service(m_Host, &event, 0) > 0)
static_cast<CNetServerWorker*>(data)->Run();
return NULL;
}
void CNetServerWorker::Run()
{
// To avoid the need for JS_SetContextThread, we create and use and destroy
// the script interface entirely within this network thread
m_ScriptInterface = new ScriptInterface("Engine", "Net server");
while (true)
{
switch (event.type)
{
case ENET_EVENT_TYPE_CONNECT:
{
// Report the client address
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE(L"Net server: Received connection from %hs:%u", hostname, event.peer->address.port);
if (m_State != SERVER_STATE_PREGAME)
{
enet_peer_disconnect(event.peer, NDR_SERVER_ALREADY_IN_GAME);
break;
}
// Set up a session object for this peer
CNetServerSession* session = new CNetServerSession(*this, event.peer);
m_Sessions.push_back(session);
SetupSession(session);
debug_assert(event.peer->data == NULL);
event.peer->data = session;
HandleConnect(session);
if (!RunStep())
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
{
// If there is an active session with this peer, then reset and delete it
// Implement autostart mode
if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers)
StartGame();
CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
if (session)
{
LOGMESSAGE(L"Net server: Disconnected %hs", DebugName(session).c_str());
// Remove the session first, so we won't send player-update messages to it
// when updating the FSM
m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end());
session->Update((uint)NMT_CONNECTION_LOST, NULL);
delete session;
event.peer->data = NULL;
}
break;
}
case ENET_EVENT_TYPE_RECEIVE:
{
// If there is an active session with this peer, then process the message
CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
if (session)
{
// Create message from raw data
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
if (msg)
{
LOGMESSAGE(L"Net server: Received message %hs of size %lu from %hs", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
HandleMessageReceive(msg, session);
delete msg;
}
}
// Done using the packet
enet_packet_destroy(event.packet);
break;
}
}
// Update profiler stats
m_Stats->LatchHostState(m_Host);
}
// Clear root before deleting its context
m_GameAttributes = CScriptValRooted();
SAFE_DELETE(m_ScriptInterface);
}
void CNetServer::Flush()
bool CNetServerWorker::RunStep()
{
debug_assert(m_Host);
// Check for messages from the game thread.
// (Do as little work as possible while the mutex is held open,
// to avoid performance problems and deadlocks.)
enet_host_flush(m_Host);
std::vector<std::pair<int, CStr> > newAssignPlayer;
std::vector<bool> newStartGame;
std::vector<std::string> newGameAttributes;
std::vector<u32> newTurnLength;
{
CScopeLock lock(m_WorkerMutex);
if (m_Shutdown)
return false;
newStartGame.swap(m_StartGameQueue);
newAssignPlayer.swap(m_AssignPlayerQueue);
newGameAttributes.swap(m_GameAttributesQueue);
newTurnLength.swap(m_TurnLengthQueue);
}
for (size_t i = 0; i < newAssignPlayer.size(); ++i)
AssignPlayer(newAssignPlayer[i].first, newAssignPlayer[i].second);
if (!newGameAttributes.empty())
UpdateGameAttributes(GetScriptInterface().ParseJSON(newGameAttributes.back()));
if (!newTurnLength.empty())
SetTurnLength(newTurnLength.back());
// Do StartGame last, so we have the most up-to-date game attributes when we start
if (!newStartGame.empty())
StartGame();
// Process network events:
ENetEvent event;
int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
if (status < 0)
{
LOGERROR(L"CNetServerWorker: enet_host_service failed (%d)", status);
// TODO: notify game that the server has shut down
return false;
}
if (status == 0)
{
// Reached timeout with no events - try again
return true;
}
// Process the event:
switch (event.type)
{
case ENET_EVENT_TYPE_CONNECT:
{
// Report the client address
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE(L"Net server: Received connection from %hs:%u", hostname, event.peer->address.port);
if (m_State != SERVER_STATE_PREGAME)
{
enet_peer_disconnect(event.peer, NDR_SERVER_ALREADY_IN_GAME);
break;
}
// Set up a session object for this peer
CNetServerSession* session = new CNetServerSession(*this, event.peer);
m_Sessions.push_back(session);
SetupSession(session);
debug_assert(event.peer->data == NULL);
event.peer->data = session;
HandleConnect(session);
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
{
// If there is an active session with this peer, then reset and delete it
CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
if (session)
{
LOGMESSAGE(L"Net server: Disconnected %hs", DebugName(session).c_str());
// Remove the session first, so we won't send player-update messages to it
// when updating the FSM
m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end());
session->Update((uint)NMT_CONNECTION_LOST, NULL);
delete session;
event.peer->data = NULL;
}
break;
}
case ENET_EVENT_TYPE_RECEIVE:
{
// If there is an active session with this peer, then process the message
CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
if (session)
{
// Create message from raw data
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
if (msg)
{
LOGMESSAGE(L"Net server: Received message %ls of size %lu from %hs", CStrW(msg->ToString()).c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
HandleMessageReceive(msg, session);
delete msg;
}
}
// Done using the packet
enet_packet_destroy(event.packet);
break;
}
}
return true;
}
void CNetServer::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
{
// Update FSM
bool ok = session->Update(message->GetType(), (void*)message);
@ -239,7 +346,7 @@ void CNetServer::HandleMessageReceive(const CNetMessage* message, CNetServerSess
LOGERROR(L"Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
}
void CNetServer::SetupSession(CNetServerSession* session)
void CNetServerWorker::SetupSession(CNetServerSession* session)
{
void* context = session;
@ -267,7 +374,7 @@ void CNetServer::SetupSession(CNetServerSession* session)
session->SetFirstState(NSS_HANDSHAKE);
}
bool CNetServer::HandleConnect(CNetServerSession* session)
bool CNetServerWorker::HandleConnect(CNetServerSession* session)
{
CSrvHandshakeMessage handshake;
handshake.m_Magic = PS_PROTOCOL_MAGIC;
@ -276,7 +383,7 @@ bool CNetServer::HandleConnect(CNetServerSession* session)
return session->SendMessage(&handshake);
}
void CNetServer::OnUserJoin(CNetServerSession* session)
void CNetServerWorker::OnUserJoin(CNetServerSession* session)
{
AddPlayer(session->GetGUID(), session->GetUserName());
@ -287,18 +394,14 @@ void CNetServer::OnUserJoin(CNetServerSession* session)
CPlayerAssignmentMessage assignMessage;
ConstructPlayerAssignmentMessage(assignMessage);
session->SendMessage(&assignMessage);
OnAddPlayer();
}
void CNetServer::OnUserLeave(CNetServerSession* session)
void CNetServerWorker::OnUserLeave(CNetServerSession* session)
{
RemovePlayer(session->GetGUID());
OnRemovePlayer();
}
void CNetServer::AddPlayer(const CStr& guid, const CStrW& name)
void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
{
// Find the first free player ID
@ -322,16 +425,14 @@ void CNetServer::AddPlayer(const CStr& guid, const CStrW& name)
SendPlayerAssignments();
}
void CNetServer::RemovePlayer(const CStr& guid)
void CNetServerWorker::RemovePlayer(const CStr& guid)
{
m_PlayerAssignments.erase(guid);
SendPlayerAssignments();
OnRemovePlayer();
}
void CNetServer::AssignPlayer(int playerID, const CStr& guid)
void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
{
// Remove anyone who's already assigned to this player
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
@ -347,7 +448,7 @@ void CNetServer::AssignPlayer(int playerID, const CStr& guid)
SendPlayerAssignments();
}
void CNetServer::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
{
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
@ -359,30 +460,30 @@ void CNetServer::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& mess
}
}
void CNetServer::SendPlayerAssignments()
void CNetServerWorker::SendPlayerAssignments()
{
CPlayerAssignmentMessage message;
ConstructPlayerAssignmentMessage(message);
Broadcast(&message);
}
ScriptInterface& CNetServer::GetScriptInterface()
ScriptInterface& CNetServerWorker::GetScriptInterface()
{
return *m_ScriptInterface;
}
void CNetServer::SetTurnLength(u32 msecs)
void CNetServerWorker::SetTurnLength(u32 msecs)
{
if (m_ServerTurnManager)
m_ServerTurnManager->SetTurnLength(msecs);
}
bool CNetServer::OnClientHandshake(void* context, CFsmEvent* event)
bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
{
debug_assert(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServer& server = session->GetServer();
CNetServerWorker& server = session->GetServer();
if (server.m_State != SERVER_STATE_PREGAME)
{
@ -406,12 +507,12 @@ bool CNetServer::OnClientHandshake(void* context, CFsmEvent* event)
return true;
}
bool CNetServer::OnAuthenticate(void* context, CFsmEvent* event)
bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
{
debug_assert(event->GetType() == (uint)NMT_AUTHENTICATE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServer& server = session->GetServer();
CNetServerWorker& server = session->GetServer();
if (server.m_State != SERVER_STATE_PREGAME)
{
@ -442,12 +543,12 @@ bool CNetServer::OnAuthenticate(void* context, CFsmEvent* event)
return true;
}
bool CNetServer::OnInGame(void* context, CFsmEvent* event)
bool CNetServerWorker::OnInGame(void* context, CFsmEvent* event)
{
// TODO: should split each of these cases into a separate method
CNetServerSession* session = (CNetServerSession*)context;
CNetServer& server = session->GetServer();
CNetServerWorker& server = session->GetServer();
CNetMessage* message = (CNetMessage*)event->GetParamRef();
if (message->GetType() == (uint)NMT_SIMULATION_COMMAND)
@ -475,12 +576,12 @@ bool CNetServer::OnInGame(void* context, CFsmEvent* event)
return true;
}
bool CNetServer::OnChat(void* context, CFsmEvent* event)
bool CNetServerWorker::OnChat(void* context, CFsmEvent* event)
{
debug_assert(event->GetType() == (uint)NMT_CHAT);
CNetServerSession* session = (CNetServerSession*)context;
CNetServer& server = session->GetServer();
CNetServerWorker& server = session->GetServer();
CChatMessage* message = (CChatMessage*)event->GetParamRef();
@ -491,31 +592,31 @@ bool CNetServer::OnChat(void* context, CFsmEvent* event)
return true;
}
bool CNetServer::OnLoadedGame(void* context, CFsmEvent* event)
bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
{
debug_assert(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* session = (CNetServerSession*)context;
CNetServer& server = session->GetServer();
CNetServerWorker& server = session->GetServer();
server.CheckGameLoadStatus(session);
return true;
}
bool CNetServer::OnDisconnect(void* context, CFsmEvent* event)
bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event)
{
debug_assert(event->GetType() == (uint)NMT_CONNECTION_LOST);
CNetServerSession* session = (CNetServerSession*)context;
CNetServer& server = session->GetServer();
CNetServerWorker& server = session->GetServer();
server.OnUserLeave(session);
return true;
}
void CNetServer::CheckGameLoadStatus(CNetServerSession* changedSession)
void CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
{
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
@ -529,7 +630,7 @@ void CNetServer::CheckGameLoadStatus(CNetServerSession* changedSession)
m_State = SERVER_STATE_INGAME;
}
void CNetServer::StartGame()
void CNetServerWorker::StartGame()
{
m_ServerTurnManager = new CNetServerTurnManager(*this);
@ -546,7 +647,7 @@ void CNetServer::StartGame()
Broadcast(&gameStart);
}
void CNetServer::UpdateGameAttributes(const CScriptValRooted& attrs)
void CNetServerWorker::UpdateGameAttributes(const CScriptValRooted& attrs)
{
m_GameAttributes = attrs;
@ -558,7 +659,7 @@ void CNetServer::UpdateGameAttributes(const CScriptValRooted& attrs)
Broadcast(&gameSetupMessage);
}
CStrW CNetServer::SanitisePlayerName(const CStrW& original)
CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
{
const size_t MAX_LENGTH = 32;
@ -580,7 +681,7 @@ CStrW CNetServer::SanitisePlayerName(const CStrW& original)
return name;
}
CStrW CNetServer::DeduplicatePlayerName(const CStrW& original)
CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
{
CStrW name = original;
@ -604,3 +705,49 @@ CStrW CNetServer::DeduplicatePlayerName(const CStrW& original)
name = original + L" (" + CStrW((unsigned long)id++) + L")";
}
}
CNetServer::CNetServer(int autostartPlayers) :
m_Worker(new CNetServerWorker(autostartPlayers))
{
}
CNetServer::~CNetServer()
{
delete m_Worker;
}
bool CNetServer::SetupConnection()
{
return m_Worker->SetupConnection();
}
void CNetServer::AssignPlayer(int playerID, const CStr& guid)
{
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_AssignPlayerQueue.push_back(std::make_pair(playerID, guid));
}
void CNetServer::StartGame()
{
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_StartGameQueue.push_back(true);
}
void CNetServer::UpdateGameAttributes(const CScriptVal& attrs, ScriptInterface& scriptInterface)
{
// Pass the attributes as JSON, since that's the easiest safe
// cross-thread way of passing script data
std::string attrsJSON = scriptInterface.StringifyJSON(attrs.get(), false);
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_GameAttributesQueue.push_back(attrsJSON);
}
void CNetServer::SetTurnLength(u32 msecs)
{
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_TurnLengthQueue.push_back(msecs);
}

View File

@ -20,6 +20,7 @@
#include "NetHost.h"
#include "ps/ThreadUtil.h"
#include "scriptinterface/ScriptVal.h"
#include <vector>
@ -31,10 +32,12 @@ class ScriptInterface;
class CPlayerAssignmentMessage;
class CNetStatsTable;
class CNetServerWorker;
enum NetServerState
{
// We haven't opened the port yet, we're just setting some stuff up.
// This is probably equivalent to the first "Start Network Game" screen
// The worker thread has not been started.
SERVER_STATE_UNCONNECTED,
// The server is open and accepting connections. This is the screen where
@ -80,43 +83,80 @@ enum NetServerSessionState
};
/**
* Network server.
* Handles all the coordination between players.
* Network server interface. Handles all the coordination between players.
* One person runs this object, and every player (including the host) connects their CNetClient to it.
*
* TODO: ideally the ENet server would run in a separate thread so it can receive
* and forward messages with minimal latency. But that's not supported now.
* The actual work is performed by CNetServerWorker in a separate thread.
*/
class CNetServer
{
NONCOPYABLE(CNetServer);
public:
CNetServer();
virtual ~CNetServer();
/**
* Get the current server's connection/game state.
* Construct a new network server.
* @param autostartPlayers if positive then StartGame will be called automatically
* once this many players are connected (intended for the command-line testing mode).
*/
NetServerState GetState() const { return m_State; }
CNetServer(int autostartPlayers = -1);
~CNetServer();
/**
* Begin listening for network connections.
* This function is synchronous (it won't return until the connection is established).
* @return true on success, false on error (e.g. port already in use)
*/
bool SetupConnection();
/**
* Poll the connections for messages from clients and process them, and send
* any queued messages.
* This must be called frequently (i.e. once per frame).
* Call from the GUI to update the player assignments.
* The given GUID will be (re)assigned to the given player ID.
* Any player currently using that ID will be unassigned.
* The changes will be asynchronously propagated to all clients.
*/
virtual void Poll();
void AssignPlayer(int playerID, const CStr& guid);
/**
* Flush any queued outgoing network messages.
* This should be called soon after sending a group of messages that may be batched together.
* Call from the GUI to asynchronously notify all clients that they should start loading the game.
*/
void Flush();
void StartGame();
/**
* Call from the GUI to update the game setup attributes.
* This must be called at least once before starting the game.
* The changes will be asynchronously propagated to all clients.
* @param attrs game attributes, in the script context of scriptInterface
*/
void UpdateGameAttributes(const CScriptVal& attrs, ScriptInterface& scriptInterface);
/**
* Set the turn length to a fixed value.
* TODO: we should replace this with some adapative lag-dependent computation.
*/
void SetTurnLength(u32 msecs);
private:
CNetServerWorker* m_Worker;
};
/**
* Network server worker thread.
* (This is run in a thread so that client/server communication is not delayed
* by the host player's framerate - the only delay should be the network latency.)
*
* Thread-safety:
* - SetupConnection and constructor/destructor must be called from the main thread.
* - The main thread may push commands onto the Queue members,
* while holding the m_WorkerMutex lock.
* - Public functions (SendMessage, Broadcast) must be called from the network
* server thread.
*/
class CNetServerWorker
{
NONCOPYABLE(CNetServerWorker);
public:
// Public functions for CNetSession/CNetServerTurnManager to use:
/**
* Send a message to the given network peer.
@ -129,6 +169,18 @@ public:
*/
bool Broadcast(const CNetMessage* message);
private:
friend class CNetServer;
CNetServerWorker(int autostartPlayers);
~CNetServerWorker();
/**
* Begin listening for network connections.
* @return true on success, false on error (e.g. port already in use)
*/
bool SetupConnection();
/**
* Call from the GUI to update the player assignments.
* The given GUID will be (re)assigned to the given player ID.
@ -171,19 +223,9 @@ public:
*/
void SetTurnLength(u32 msecs);
protected:
/// Callback for autostart; called when a player has finished connecting
virtual void OnAddPlayer() { }
/// Callback for autostart; called when a player has left the game
virtual void OnRemovePlayer() { }
private:
void AddPlayer(const CStr& guid, const CStrW& name);
void RemovePlayer(const CStr& guid);
void SendPlayerAssignments();
PlayerAssignmentMap m_PlayerAssignments;
CScriptValRooted m_GameAttributes;
void SetupSession(CNetServerSession* session);
bool HandleConnect(CNetServerSession* session);
@ -204,13 +246,21 @@ private:
void HandleMessageReceive(const CNetMessage* message, CNetServerSession* session);
/**
* Internal script context for (de)serializing script messages.
* Internal script context for (de)serializing script messages,
* and for storing game attributes.
* (TODO: we shouldn't bother deserializing (except for debug printing of messages),
* we should just forward messages blindly and efficiently.)
*/
ScriptInterface* m_ScriptInterface;
PlayerAssignmentMap m_PlayerAssignments;
CScriptValRooted m_GameAttributes;
int m_AutostartPlayers;
ENetHost* m_Host;
std::vector<CNetServerSession*> m_Sessions;
@ -224,6 +274,24 @@ private:
u32 m_NextHostID;
CNetServerTurnManager* m_ServerTurnManager;
private:
// Thread-related stuff:
static void* RunThread(void* data);
void Run();
bool RunStep();
pthread_t m_WorkerThread;
CMutex m_WorkerMutex;
bool m_Shutdown; // protected by m_WorkerMutex
// Queues for messages sent by the game thread:
std::vector<std::pair<int, CStr> > m_AssignPlayerQueue; // protected by m_WorkerMutex
std::vector<bool> m_StartGameQueue; // protected by m_WorkerMutex
std::vector<std::string> m_GameAttributesQueue; // protected by m_WorkerMutex
std::vector<u32> m_TurnLengthQueue; // protected by m_WorkerMutex
};
/// Global network server for the standard game

View File

@ -161,7 +161,7 @@ bool CNetClientSession::SendMessage(const CNetMessage* message)
CNetServerSession::CNetServerSession(CNetServer& server, ENetPeer* peer) :
CNetServerSession::CNetServerSession(CNetServerWorker& server, ENetPeer* peer) :
m_Server(server), m_Peer(peer)
{
}

View File

@ -24,7 +24,7 @@
#include "scriptinterface/ScriptVal.h"
class CNetClient;
class CNetServer;
class CNetServerWorker;
class CNetStatsTable;
@ -84,15 +84,18 @@ private:
* The server's end of a network session.
* Represents an abstraction of the state of the client, storing all the per-client data
* needed by the server.
*
* Thread-safety:
* - This is constructed and used by CNetServerWorker in the network server thread.
*/
class CNetServerSession : public CFsm
{
NONCOPYABLE(CNetServerSession);
public:
CNetServerSession(CNetServer& server, ENetPeer* peer);
CNetServerSession(CNetServerWorker& server, ENetPeer* peer);
CNetServer& GetServer() { return m_Server; }
CNetServerWorker& GetServer() { return m_Server; }
const CStr& GetGUID() const { return m_GUID; }
void SetGUID(const CStr& guid) { m_GUID = guid; }
@ -124,7 +127,7 @@ public:
bool SendMessage(const CNetMessage* message);
private:
CNetServer& m_Server;
CNetServerWorker& m_Server;
ENetPeer* m_Peer;

View File

@ -38,12 +38,12 @@ enum
};
CNetStatsTable::CNetStatsTable(const ENetPeer* peer)
: m_Host(NULL), m_Peer(peer)
: m_Peer(peer)
{
}
CNetStatsTable::CNetStatsTable(const ENetHost* host)
: m_Host(host), m_Peer(NULL)
CNetStatsTable::CNetStatsTable()
: m_Peer(NULL)
{
}
@ -54,10 +54,10 @@ CStr CNetStatsTable::GetName()
CStr CNetStatsTable::GetTitle()
{
if (m_Host)
return "Network host statistics";
else
if (m_Peer)
return "Network client statistics";
else
return "Network host statistics";
}
size_t CNetStatsTable::GetNumberRows()
@ -69,25 +69,33 @@ const std::vector<ProfileColumn>& CNetStatsTable::GetColumns()
{
m_ColumnDescriptions.clear();
m_ColumnDescriptions.push_back(ProfileColumn("Name", 200));
if (m_Host)
{
for (size_t i = 0; i < m_Host->peerCount; ++i)
m_ColumnDescriptions.push_back(ProfileColumn("Peer "+CStr((unsigned long)i), 80));
}
else if (m_Peer)
if (m_Peer)
{
m_ColumnDescriptions.push_back(ProfileColumn("Value", 80));
}
else
{
CScopeLock lock(m_Mutex);
for (size_t i = 0; i < m_LatchedData.size(); ++i)
m_ColumnDescriptions.push_back(ProfileColumn("Peer "+CStr((unsigned long)i), 80));
}
return m_ColumnDescriptions;
}
CStr CNetStatsTable::GetCellText(size_t row, size_t col)
{
#define ROW(id, title, member) \
// Return latched data, if we have any
{
CScopeLock lock(m_Mutex);
if (col > 0 && m_LatchedData.size() > col-1 && m_LatchedData[col-1].size() > row)
return m_LatchedData[col-1][row];
}
#define ROW(id, title, member) \
case id: \
if (col == 0) return title; \
if (m_Peer) return CStr(m_Peer->member); \
if (m_Host && col <= m_Host->peerCount) return CStr(m_Host->peers[col-1].member); \
return "???"
switch(row)
@ -107,9 +115,38 @@ CStr CNetStatsTable::GetCellText(size_t row, size_t col)
default:
return "???";
}
#undef ROW
}
AbstractProfileTable* CNetStatsTable::GetChild(size_t UNUSED(row))
{
return 0;
}
void CNetStatsTable::LatchHostState(const ENetHost* host)
{
CScopeLock lock(m_Mutex);
#define ROW(id, title, member) \
m_LatchedData[i].push_back(CStr(host->peers[i].member));
m_LatchedData.clear();
m_LatchedData.resize(host->peerCount);
for (size_t i = 0; i < host->peerCount; ++i)
{
ROW(Row_InData, "incoming bytes", incomingDataTotal);
ROW(Row_OutData, "outgoing bytes", outgoingDataTotal);
ROW(Row_LastSendTime, "last send time", lastSendTime);
ROW(Row_LastRecvTime, "last receive time", lastReceiveTime);
ROW(Row_NextTimeout, "next timeout", nextTimeout);
ROW(Row_PacketsSent, "packets sent", packetsSent);
ROW(Row_PacketsLost, "packets lost", packetsLost);
ROW(Row_LastRTT, "last RTT", lastRoundTripTime);
ROW(Row_RTT, "mean RTT", roundTripTime);
ROW(Row_MTU, "MTU", mtu);
ROW(Row_ReliableInTransit, "reliable data in transit", reliableDataInTransit);
}
#undef ROW
}

View File

@ -19,15 +19,25 @@
#define INCLUDED_NETSTATS
#include "ps/ProfileViewer.h"
#include "ps/ThreadUtil.h"
typedef struct _ENetPeer ENetPeer;
typedef struct _ENetHost ENetHost;
/**
* ENet connection statistics profiler table.
*
* Thread-safety:
* - Must be constructed in the main thread (to match the profiler).
* - In host mode, the host can be running in a separate thread;
* call LatchHostState from that thread periodically to safely
* update our displayed copy of the data.
*/
class CNetStatsTable : public AbstractProfileTable
{
NONCOPYABLE(CNetStatsTable);
public:
CNetStatsTable(const ENetHost* host);
CNetStatsTable();
CNetStatsTable(const ENetPeer* peer);
virtual CStr GetName();
@ -37,10 +47,14 @@ public:
virtual CStr GetCellText(size_t row, size_t col);
virtual AbstractProfileTable* GetChild(size_t row);
void LatchHostState(const ENetHost* host);
private:
const ENetHost* m_Host;
const ENetPeer* m_Peer;
std::vector<ProfileColumn> m_ColumnDescriptions;
CMutex m_Mutex;
std::vector<std::vector<CStr> > m_LatchedData; // protected by m_Mutex
};
#endif // INCLUDED_NETSTATS

View File

@ -294,7 +294,7 @@ void CNetLocalTurnManager::OnSimulationMessage(CSimulationMessage* UNUSED(msg))
CNetServerTurnManager::CNetServerTurnManager(CNetServer& server) :
CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server) :
m_NetServer(server), m_ReadyTurn(1), m_TurnLength(DEFAULT_TURN_LENGTH_MP)
{
}

View File

@ -22,7 +22,7 @@
#include <map>
class CNetServer;
class CNetServerWorker;
class CNetClient;
class CSimulationMessage;
class CSimulation2;
@ -183,12 +183,15 @@ protected:
* The server-side counterpart to CNetClientTurnManager.
* Records the turn state of each client, and sends turn advancement messages
* when all clients are ready.
*
* Thread-safety:
* - This is constructed and used by CNetServerWorker in the network server thread.
*/
class CNetServerTurnManager
{
NONCOPYABLE(CNetServerTurnManager);
public:
CNetServerTurnManager(CNetServer& server);
CNetServerTurnManager(CNetServerWorker& server);
void NotifyFinishedClientCommands(int client, u32 turn);
@ -215,7 +218,7 @@ protected:
// Current turn length
u32 m_TurnLength;
CNetServer& m_NetServer;
CNetServerWorker& m_NetServer;
};
#endif // INCLUDED_NETTURNMANAGER

View File

@ -17,8 +17,10 @@
#include "lib/self_test.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/external_libraries/enet.h"
#include "lib/external_libraries/sdl.h"
#include "lib/tex/tex.h"
#include "network/NetServer.h"
#include "network/NetClient.h"
#include "network/NetTurnManager.h"
@ -40,6 +42,12 @@ public:
TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/L"_testcache"));
CXeromyces::Startup();
// Need some stuff for terrain movement costs:
// (TODO: this ought to be independent of any graphics code)
tex_codec_register_all();
new CTerrainTextureManager;
g_TexMan.LoadTerrainTextures();
enet_initialize();
}
@ -47,6 +55,9 @@ public:
{
enet_deinitialize();
delete &g_TexMan;
tex_codec_unregister_all();
CXeromyces::Terminate();
g_VFS.reset();
DeleteDirectory(DataDir()/L"_testcache");
@ -69,11 +80,10 @@ public:
for (size_t i = 0; ; ++i)
{
// debug_printf(L".");
server.Poll();
for (size_t j = 0; j < clients.size(); ++j)
clients[j]->Poll();
if (server.GetState() == SERVER_STATE_PREGAME && clients_are_all(clients, NCS_PREGAME))
if (clients_are_all(clients, NCS_PREGAME))
break;
if (i > 20)
@ -86,6 +96,7 @@ public:
}
}
#if 0
void disconnect(CNetServer& server, const std::vector<CNetClient*>& clients)
{
for (size_t i = 0; ; ++i)
@ -107,12 +118,12 @@ public:
SDL_Delay(100);
}
}
#endif
void wait(CNetServer& server, const std::vector<CNetClient*>& clients, size_t msecs)
void wait(const std::vector<CNetClient*>& clients, size_t msecs)
{
for (size_t i = 0; i < msecs/10; ++i)
{
server.Poll();
for (size_t j = 0; j < clients.size(); ++j)
clients[j]->Poll();
@ -125,6 +136,7 @@ public:
// This doesn't actually test much, it just runs a very quick multiplayer game
// and prints a load of debug output so you can see if anything funny's going on
ScriptInterface scriptInterface("Engine");
TestStdoutLogger logger;
std::vector<CNetClient*> clients;
@ -136,8 +148,8 @@ public:
CNetServer server;
CScriptValRooted attrs;
server.GetScriptInterface().Eval("({map:'_default',thing:'example'})", attrs);
server.UpdateGameAttributes(attrs);
scriptInterface.Eval("({mapType:'scenario',map:'_default',thing:'example'})", attrs);
server.UpdateGameAttributes(attrs.get(), scriptInterface);
CNetClient client1(&client1Game);
CNetClient client2(&client2Game);
@ -151,7 +163,6 @@ public:
debug_printf(L"%ls", client1.TestReadGuiMessages().c_str());
server.StartGame();
server.Poll();
SDL_Delay(100);
for (size_t j = 0; j < clients.size(); ++j)
{
@ -160,7 +171,7 @@ public:
clients[j]->LoadFinished();
}
wait(server, clients, 100);
wait(clients, 100);
{
CScriptValRooted cmd;
@ -174,14 +185,14 @@ public:
client2Game.GetTurnManager()->PostCommand(cmd);
}
wait(server, clients, 100);
wait(clients, 100);
client1Game.GetTurnManager()->Update(1.0f);
client2Game.GetTurnManager()->Update(1.0f);
client3Game.GetTurnManager()->Update(1.0f);
wait(server, clients, 100);
wait(clients, 100);
client1Game.GetTurnManager()->Update(1.0f);
client2Game.GetTurnManager()->Update(1.0f);
client3Game.GetTurnManager()->Update(1.0f);
wait(server, clients, 100);
wait(clients, 100);
}
};

View File

@ -37,6 +37,7 @@
#include "ps/Hotkey.h"
#include "ps/Pyrogenesis.h"
#include "scripting/ScriptingHost.h"
#include "scriptinterface/ScriptInterface.h"
#define LOG_CATEGORY L"Console"
@ -44,7 +45,6 @@ CConsole* g_Console = 0;
CConsole::CConsole()
{
m_bToggle = false;
m_bVisible = false;
@ -56,25 +56,8 @@ CConsole::CConsole()
m_iMsgHistPos = 1;
m_charsPerPage=0;
InsertMessage(L"[ 0 A.D. Console v0.12 ] type \"\\info\" for help");
InsertMessage(L"[ 0 A.D. Console v0.14 ]");
InsertMessage(L"");
if (FileExists(L"gui/text/help.txt"))
{
shared_ptr<u8> buf; size_t size;
if ( g_VFS->LoadFile(L"gui/text/help.txt", buf, size) < 0 )
{
LOG(CLogger::Error, LOG_CATEGORY, L"Help file not found for console");
return;
}
// TODO: read in text mode, or at least get rid of the \r\n somehow
// TODO: maybe the help file should be UTF-8 - we assume it's iso-8859-1 here
m_helpText = CStrW((const char*)buf.get());
}
else
{
InsertMessage(L"No help file found.");
}
}
CConsole::~CConsole()
@ -218,7 +201,7 @@ void CConsole::Render()
}
void CConsole::DrawWindow(void)
void CConsole::DrawWindow()
{
// TODO: Add texturing
glDisable(GL_TEXTURE_2D);
@ -255,11 +238,14 @@ void CConsole::DrawWindow(void)
}
void CConsole::DrawHistory(void) {
void CConsole::DrawHistory()
{
int i = 1;
std::deque<std::wstring>::iterator Iter; //History iterator
CScopeLock lock(m_Mutex); // needed for safe access to m_deqMsgHistory
glPushMatrix();
glColor3f(1.0f, 1.0f, 1.0f); //Set color of text
glTranslatef(9.0f, (float)m_iFontOffset, 0.0f); //move away from the border
@ -390,6 +376,8 @@ void CConsole::InsertChar(const int szChar, const wchar_t cooked )
case SDLK_HOME:
if (g_keys[SDLK_RCTRL] || g_keys[SDLK_LCTRL])
{
CScopeLock lock(m_Mutex); // needed for safe access to m_deqMsgHistory
int linesShown = (int)m_fHeight/m_iFontHeight - 4;
m_iMsgHistPos = clamp((int)m_deqMsgHistory.size() - linesShown, 1, (int)m_deqMsgHistory.size());
}
@ -487,8 +475,12 @@ void CConsole::InsertChar(const int szChar, const wchar_t cooked )
// BEGIN: Message History Lookup
case SDLK_PAGEUP:
{
CScopeLock lock(m_Mutex); // needed for safe access to m_deqMsgHistory
if (m_iMsgHistPos != (int)m_deqMsgHistory.size()) m_iMsgHistPos++;
return;
}
case SDLK_PAGEDOWN:
if (m_iMsgHistPos != 1) m_iMsgHistPos--;
@ -563,13 +555,17 @@ void CConsole::InsertMessageRaw(const CStrW& message)
// Split into lines and add each one individually
oldNewline = 0;
while ( (distance = wrapAround.find(newline, oldNewline)) != wrapAround.npos)
{
distance -= oldNewline;
m_deqMsgHistory.push_front(wrapAround.substr(oldNewline, distance));
oldNewline += distance+1;
CScopeLock lock(m_Mutex); // needed for safe access to m_deqMsgHistory
while ( (distance = wrapAround.find(newline, oldNewline)) != wrapAround.npos)
{
distance -= oldNewline;
m_deqMsgHistory.push_front(wrapAround.substr(oldNewline, distance));
oldNewline += distance+1;
}
m_deqMsgHistory.push_front(wrapAround.substr(oldNewline));
}
m_deqMsgHistory.push_front(wrapAround.substr(oldNewline));
}
const wchar_t* CConsole::GetBuffer()
@ -608,64 +604,11 @@ void CConsole::ProcessBuffer(const wchar_t* szLine)
SaveHistory(); // Do this each line for the moment; if a script causes
// a crash it's a useful record.
wchar_t szCommand[CONSOLE_BUFFER_SIZE] = { 0 };
// Process it as JavaScript
if (szLine[0] == '\\')
{
if (swscanf(szLine, L"\\%ls", szCommand) != 1)
return;
Trim(szCommand);
ToLower(szCommand);
if (!wcscmp(szCommand, L"info"))
{
InsertMessage(L"");
InsertMessage(L"[Information]");
InsertMessage(L" -View commands \"\\commands\"");
InsertMessage(L" -Call command \"\\<command>\"");
InsertMessage(L" -Say \"<string>\"");
InsertMessage(L" -Help - Lists functions usable from console");
InsertMessage(L"");
}
else if (!wcscmp(szCommand, L"commands"))
{
InsertMessage(L"");
InsertMessage(L"[Commands]");
InsertMessage(L" (none registered)");
InsertMessage(L"");
}
else if (! (wcscmp(szCommand, L"Help") && wcscmp(szCommand, L"help")) )
{
InsertMessage(L"");
InsertMessage(L"[Help]");
InsertMessageRaw(m_helpText);
}
else
{
InsertMessage(L"unknown command <%ls>", szCommand);
}
}
else if (szLine[0] == ':' || szLine[0] == '?')
{
// Process it as JavaScript
jsval rval = g_ScriptingHost.ExecuteScript( szLine+1, L"Console" );
if (szLine[0] == '?' && rval)
{
try {
InsertMessage( L"%ls", g_ScriptingHost.ValueToUCString( rval ).c_str() );
} catch (PSERROR_Scripting_ConversionFailed) {
InsertMessage( L"%hs", "<error converting return value to string>" );
}
}
}
else
{
SendChatMessage(szLine);
}
jsval rval = g_ScriptingHost.ExecuteScript(szLine, L"Console");
if (!JSVAL_IS_VOID(rval))
InsertMessage(L"%ls", g_ScriptingHost.GetScriptInterface().ToString(rval).c_str());
}
void CConsole::LoadHistory()
@ -716,15 +659,6 @@ void CConsole::SaveHistory()
g_VFS->CreateFile(m_sHistoryFile, buffer.Data(), buffer.Size());
}
void CConsole::SendChatMessage(const wchar_t *pText)
{
if (g_NetClient)
{
// TODO
// g_NetClient3->SendChatMessage(pText);
}
}
void CConsole::ReceivedChatMessage(const wchar_t *szSender, const wchar_t *szMessage)
{
InsertMessage(L"%ls: %ls", szSender, szMessage);

View File

@ -24,10 +24,10 @@
#include <deque>
#include <map>
#include "CStr.h"
#include "lib/input.h"
#include "lib/file/vfs/vfs_path.h"
#include "ps/CStr.h"
#include "ps/ThreadUtil.h"
#ifndef INCLUDED_CCONSOLE
#define INCLUDED_CCONSOLE
@ -37,6 +37,13 @@
#define CONSOLE_FONT L"mono-10"
/**
* In-game console.
*
* Thread-safety:
* - Expected to be constructed/destructed in the main thread.
* - InsertMessage may be called from any thread while the object is alive.
*/
class CConsole
{
public:
@ -56,7 +63,6 @@ public:
void InsertMessage(const wchar_t* szMessage, ...) WPRINTF_ARGS(2);
void InsertChar(const int szChar, const wchar_t cooked);
void SendChatMessage(const wchar_t *szMessage);
void ReceivedChatMessage(const wchar_t *pSender, const wchar_t *szMessage);
void SetBuffer(const wchar_t* szMessage);
@ -75,6 +81,9 @@ public:
size_t m_charsPerPage;
private:
// Lock for all state modified by InsertMessage
CMutex m_Mutex;
float m_fX;
float m_fY;
@ -85,42 +94,38 @@ private:
// allows implementing other animations than sliding, e.g. fading in/out.
float m_fVisibleFrac;
CStrW m_helpText;
std::deque<std::wstring> m_deqMsgHistory;
std::deque<std::wstring> m_deqMsgHistory; // protected by m_Mutex
std::deque<std::wstring> m_deqBufHistory;
int m_iMsgHistPos;
wchar_t* m_szBuffer;
wchar_t* m_szBuffer;
int m_iBufferPos;
int m_iBufferLength;
VfsPath m_sHistoryFile;
int m_MaxHistoryLines;
bool m_bFocus;
bool m_bFocus;
bool m_bVisible; // console is to be drawn
bool m_bToggle; // show/hide animation is currently active
void ToLower(wchar_t* szMessage, size_t iSize = 0);
void Trim(wchar_t* szMessage, const wchar_t cChar = 32, size_t iSize = 0);
void DrawHistory(void);
void DrawWindow(void);
void DrawBuffer(void);
void DrawCursor(void);
void DrawHistory();
void DrawWindow();
void DrawBuffer();
void DrawCursor();
bool IsEOB(void) {return (m_iBufferPos == m_iBufferLength);}; //Is end of Buffer?
bool IsBOB(void) {return (m_iBufferPos == 0);}; //Is beginning of Buffer?
bool IsFull(void) {return (m_iBufferLength == CONSOLE_BUFFER_SIZE);};
bool IsEmpty(void) {return (m_iBufferLength == 0);};
bool IsEOB() { return (m_iBufferPos == m_iBufferLength); } // Is end of Buffer?
bool IsBOB() { return (m_iBufferPos == 0); } // Is beginning of Buffer?
bool IsFull() { return (m_iBufferLength == CONSOLE_BUFFER_SIZE); }
bool IsEmpty() { return (m_iBufferLength == 0); }
void InsertBuffer(void){InsertMessage(L"%ls", m_szBuffer);};
void ProcessBuffer(const wchar_t* szLine);
void ProcessBuffer(const wchar_t* szLine);
// Insert message without printf-style formatting, and without
// length limits
// Insert message without printf-style formatting, and without length limits
void InsertMessageRaw(const CStrW& message);
void LoadHistory();

View File

@ -114,7 +114,7 @@ void CLogger::Init()
*m_InterestingLog << html_header0 << "Main log (warnings and errors only)" << html_header1;
}
CLogger::~CLogger ()
CLogger::~CLogger()
{
char buffer[128];
sprintf_s(buffer, ARRAY_SIZE(buffer), " with %d message(s), %d error(s) and %d warning(s).", m_NumberOfMessages,m_NumberOfErrors,m_NumberOfWarnings);
@ -154,6 +154,8 @@ void CLogger::WriteMessage(const wchar_t* message)
{
std::string cmessage = ToHTML(message);
CScopeLock lock(m_Mutex);
++m_NumberOfMessages;
// if (m_UseDebugPrintf)
// debug_printf(L"MESSAGE: %ls\n", message);
@ -169,6 +171,8 @@ void CLogger::WriteError(const wchar_t* message)
{
std::string cmessage = ToHTML(message);
CScopeLock lock(m_Mutex);
++m_NumberOfErrors;
if (m_UseDebugPrintf)
debug_printf(L"ERROR: %ls\n", message);
@ -187,6 +191,8 @@ void CLogger::WriteWarning(const wchar_t* message)
{
std::string cmessage = ToHTML(message);
CScopeLock lock(m_Mutex);
++m_NumberOfWarnings;
if (m_UseDebugPrintf)
debug_printf(L"WARNING: %ls\n", message);
@ -205,11 +211,11 @@ void CLogger::WriteWarning(const wchar_t* message)
// -- This function has not been removed because the build would break.
void CLogger::LogUsingMethod(ELogMethod method, const wchar_t* message)
{
if(method == Normal)
if (method == Normal)
WriteMessage(message);
else if(method == Error)
else if (method == Error)
WriteError(message);
else if(method == Warning)
else if (method == Warning)
WriteWarning(message);
else
WriteMessage(message);
@ -232,31 +238,6 @@ void CLogger::Log(ELogMethod method, const wchar_t* UNUSED(category), const wcha
LogUsingMethod(method, buffer);
}
// -- This function has not been removed because the build would break.
void CLogger::LogOnce(ELogMethod method, const wchar_t* UNUSED(category), const wchar_t* fmt, ...)
{
va_list argp;
wchar_t buffer[BUFFER_SIZE] = {0};
va_start(argp, fmt);
if (sys_vswprintf(buffer, ARRAY_SIZE(buffer), fmt, argp) == -1)
{
// Buffer too small - ensure the string is nicely terminated
wcscpy_s(buffer+ARRAY_SIZE(buffer)-4, 4, L"...");
}
va_end(argp);
std::wstring message(buffer);
// If this message has already been logged, ignore it
if (m_LoggedOnce.find(message) != m_LoggedOnce.end())
return;
// If not, mark it as having been logged and then log it
m_LoggedOnce.insert(message);
LogUsingMethod(method, buffer);
}
void CLogger::LogMessage(const wchar_t* fmt, ...)
{
va_list argp;
@ -321,6 +302,10 @@ void CLogger::Render()
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// (Lock must come after loading the CFont, since that might log error messages
// and attempt to lock the mutex recursively which is forbidden)
CScopeLock lock(m_Mutex);
for (std::deque<RenderedMessage>::iterator it = m_RenderMessages.begin(); it != m_RenderMessages.end(); ++it)
{
const wchar_t* type;
@ -357,7 +342,7 @@ void CLogger::Render()
void CLogger::PushRenderMessage(ELogMethod method, const wchar_t* message)
{
double now = timer_Time();
double now = timer_Time(); // XXX: this is not thread-safe (http://trac.wildfiregames.com/ticket/653)
// Add each message line separately
const wchar_t* pos = message;
@ -381,6 +366,8 @@ void CLogger::PushRenderMessage(ELogMethod method, const wchar_t* message)
void CLogger::CleanupRenderQueue()
{
CScopeLock lock(m_Mutex);
if (m_RenderMessages.empty())
return;

View File

@ -23,11 +23,12 @@
#include <set>
#include <sstream>
#include "ps/ThreadUtil.h"
class CLogger;
extern CLogger* g_Logger;
#define LOG (g_Logger->Log)
#define LOG_ONCE (g_Logger->LogOnce)
// Should become LOG_MESSAGE but this can only be changed once the LOG function is removed
// from all of the files. LOG_INFO, LOG_WARNING and LOG_ERROR are currently existing macros.
@ -36,6 +37,14 @@ extern CLogger* g_Logger;
#define LOGWARNING g_Logger->LogWarning
#define LOGERROR g_Logger->LogError
/**
* Error/warning/message logging class.
*
* Thread-safety:
* - Expected to be constructed/destructed in the main thread.
* - The message logging functions may be called from any thread
* while the object is alive.
*/
class CLogger
{
NONCOPYABLE(CLogger);
@ -65,10 +74,7 @@ public:
// Function to log stuff to file
// -- This function has not been removed because the build would break.
void Log(ELogMethod method, const wchar_t* category, const wchar_t* fmt, ...) WPRINTF_ARGS(4);
// Similar to Log, but only outputs each message once no matter how many times it's called
// -- This function has not been removed because the build would break.
void LogOnce(ELogMethod method, const wchar_t* category, const wchar_t* fmt, ...) WPRINTF_ARGS(4);
// Functions to write a message, warning or error to file.
void LogMessage(const wchar_t* fmt, ...) WPRINTF_ARGS(2);
void LogWarning(const wchar_t* fmt, ...) WPRINTF_ARGS(2);
@ -102,9 +108,6 @@ private:
int m_NumberOfErrors;
int m_NumberOfWarnings;
// Used to remember LogOnce messages
std::set<std::wstring> m_LoggedOnce;
// Used for Render()
struct RenderedMessage
{
@ -114,6 +117,9 @@ private:
};
std::deque<RenderedMessage> m_RenderMessages;
double m_RenderLastEraseTime;
// Lock for all state modified by logging commands
CMutex m_Mutex;
};
/**

View File

@ -55,7 +55,7 @@ CFont::CFont(const wchar_t* name)
return;
// Not found as a font -- give up and use the default.
LOG_ONCE(CLogger::Error, LOG_CATEGORY, L"Failed to find font '%ls'", name);
LOGERROR(L"Failed to find font '%ls'", name);
h = unifont_load(g_VFS, DefaultFont);
// Assume this worked
}

View File

@ -711,6 +711,8 @@ void EarlyInit()
// If you ever want to catch a particular allocation:
//_CrtSetBreakAlloc(232647);
ThreadUtil::SetMainThread();
debug_SetThreadName("main");
// add all debug_printf "tags" that we are interested in:
debug_filter_add(L"TIMER");
@ -895,54 +897,6 @@ void RenderGui(bool RenderingState)
}
// Network autostart:
class AutostartNetServer : public CNetServer
{
public:
AutostartNetServer(const CStr& map, int maxPlayers) :
CNetServer(), m_NeedsStart(false), m_NumPlayers(0), m_MaxPlayers(maxPlayers)
{
CScriptValRooted attrs;
GetScriptInterface().Eval("({})", attrs);
GetScriptInterface().SetProperty(attrs.get(), "map", std::string(map), false);
UpdateGameAttributes(attrs);
}
protected:
virtual void OnAddPlayer()
{
m_NumPlayers++;
debug_printf(L"# player joined (got %d, need %d)\n", (int)m_NumPlayers, (int)m_MaxPlayers);
if (m_NumPlayers >= m_MaxPlayers)
m_NeedsStart = true; // delay until next Poll, so the new player has been fully processed
}
virtual void OnRemovePlayer()
{
debug_warn(L"client left?!");
m_NumPlayers--;
}
virtual void Poll()
{
if (m_NeedsStart)
{
StartGame();
m_NeedsStart = false;
}
CNetServer::Poll();
}
private:
bool m_NeedsStart;
size_t m_NumPlayers;
size_t m_MaxPlayers;
};
static bool Autostart(const CmdLineArgs& args)
{
/*
@ -958,6 +912,12 @@ static bool Autostart(const CmdLineArgs& args)
g_Game = new CGame();
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
CScriptValRooted attrs;
scriptInterface.Eval("({})", attrs);
scriptInterface.SetProperty(attrs.get(), "mapType", std::string("scenario"), false);
scriptInterface.SetProperty(attrs.get(), "map", std::string(autostartMap), false);
if (args.Has("autostart-host"))
{
InitPs(true, L"page_loading.xml");
@ -966,7 +926,10 @@ static bool Autostart(const CmdLineArgs& args)
if (args.Has("autostart-players"))
maxPlayers = args.Get("autostart-players").ToUInt();
g_NetServer = new AutostartNetServer(autostartMap, maxPlayers);
g_NetServer = new CNetServer(maxPlayers);
g_NetServer->UpdateGameAttributes(attrs.get(), scriptInterface);
bool ok = g_NetServer->SetupConnection();
debug_assert(ok);
@ -980,17 +943,16 @@ static bool Autostart(const CmdLineArgs& args)
g_NetClient = new CNetClient(g_Game);
// TODO: player name, etc
bool ok = g_NetClient->SetupConnection(args.Get("autostart-ip"));
CStr ip = "127.0.0.1";
if (args.Has("autostart-ip"))
ip = args.Get("autostart-ip");
bool ok = g_NetClient->SetupConnection(ip);
debug_assert(ok);
}
else
{
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
CScriptValRooted attrs;
scriptInterface.Eval("({})", attrs);
scriptInterface.SetProperty(attrs.get(), "mapType", std::string("scenario"), false);
scriptInterface.SetProperty(attrs.get(), "map", std::string(autostartMap), false);
g_Game->SetPlayerID(1);
g_Game->StartGame(attrs);
LDR_NonprogressiveLoad();

View File

@ -25,6 +25,7 @@
#include <vector>
#include <map>
#include "Singleton.h"
#include "ps/ThreadUtil.h"
#define PROFILE_AMORTIZE
#define PROFILE_AMORTIZE_FRAMES 50
@ -158,7 +159,12 @@ public:
CProfileSample( const char* name )
{
if (CProfileManager::IsInitialised())
{
// The profiler is only safe to use on the main thread
debug_assert(ThreadUtil::IsMainThread());
g_Profiler.Start( name );
}
}
~CProfileSample()
{
@ -173,7 +179,12 @@ public:
CProfileSampleScript( const char* name )
{
if (CProfileManager::IsInitialised())
{
// The profiler is only safe to use on the main thread
debug_assert(ThreadUtil::IsMainThread());
g_Profiler.StartScript( name );
}
}
~CProfileSampleScript()
{

View File

@ -360,9 +360,11 @@ namespace
{
std::vector<CStr> data; // 2d array of (rows+head)*columns elements
const std::vector<ProfileColumn>& columns = table->GetColumns();
// Add column headers to 'data'
for (std::vector<ProfileColumn>::const_iterator col_it = table->GetColumns().begin();
col_it != table->GetColumns().end(); ++col_it)
for (std::vector<ProfileColumn>::const_iterator col_it = columns.begin();
col_it != columns.end(); ++col_it)
data.push_back(col_it->title);
// Recursively add all profile data to 'data'
@ -371,7 +373,7 @@ namespace
// Calculate the width of each column ( = the maximum width of
// any value in that column)
std::vector<size_t> columnWidths;
size_t cols = table->GetColumns().size();
size_t cols = columns.size();
for (size_t c = 0; c < cols; ++c)
{
size_t max = 0;
@ -401,6 +403,8 @@ namespace
void WriteRows(int indent, AbstractProfileTable* table, std::vector<CStr>& data)
{
const std::vector<ProfileColumn>& columns = table->GetColumns();
for (size_t r = 0; r < table->GetNumberRows(); ++r)
{
// Do pretty tree-structure indenting
@ -410,7 +414,7 @@ namespace
else
indentation += "|-";
for (size_t c = 0; c < table->GetColumns().size(); ++c)
for (size_t c = 0; c < columns.size(); ++c)
if (c == 0)
data.push_back(indentation + table->GetCellText(r, c));
else

View File

@ -138,9 +138,7 @@ void CReplayPlayer::Replay()
{
std::string line;
std::getline(*m_Stream, line);
std::wstring linew = wstring_from_utf8(line);
utf16string line16(linew.begin(), linew.end());
CScriptValRooted attribs = game.GetSimulation2()->GetScriptInterface().ParseJSON(line16);
CScriptValRooted attribs = game.GetSimulation2()->GetScriptInterface().ParseJSON(line);
game.StartGame(attribs);
LDR_NonprogressiveLoad();
@ -159,9 +157,7 @@ void CReplayPlayer::Replay()
std::string line;
std::getline(*m_Stream, line);
std::wstring linew = wstring_from_utf8(line);
utf16string line16(linew.begin(), linew.end());
CScriptValRooted data = game.GetSimulation2()->GetScriptInterface().ParseJSON(line16);
CScriptValRooted data = game.GetSimulation2()->GetScriptInterface().ParseJSON(line);
SimulationCommand cmd = { player, data };
commands.push_back(cmd);

32
source/ps/ThreadUtil.cpp Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "ThreadUtil.h"
static pthread_t g_MainThread;
bool ThreadUtil::IsMainThread()
{
return pthread_equal(pthread_self(), g_MainThread);
}
void ThreadUtil::SetMainThread()
{
g_MainThread = pthread_self();
}

View File

@ -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
@ -15,71 +15,8 @@
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
/*
ThreadUtil.h - Thread Utility Functions
--Overview--
Contains three classes: CMutex, CRWLock and CLocker.
CMutex is a Mutual Exclusion lock that can be locked and unlocked. The Lock
function waits for the current lock holder (if any) to release the lock and
then acquires it.
CRWLock is a lock where any number of readers *or* one writer can hold the lock.
CLocker is a generic wrapper class that can wrap any data class with any locker
class.
--Example--
CMutex usage:
CMutex protectData;
void function()
{
protectData.Lock();
.. do stuff with data ..
protectData.Unlock();
}
CLocker usage:
class CDataClass {};
CLocker<CDataClass> instance;
void function()
{
instance.Lock();
.. do stuff with instance ..
instance.Unlock();
}
CLocker usage 2:
class CDataClass {};
class CCustomLockerClass { void MyOwnLock(); void MyOwnUnlock(); };
CLocker<CDataClass, CCustomLockerClass> instance;
void function()
{
instance.MyOwnLock();
.. do stuff with instance ..
instance.MyOwnUnlock();
}
--More Info--
*/
#ifndef _ThreadUtil_h
#define _ThreadUtil_h
//--------------------------------------------------------
// Includes / Compiler directives
//--------------------------------------------------------
#ifndef INCLUDED_THREADUTIL
#define INCLUDED_THREADUTIL
#include "lib/posix/posix_pthread.h"
@ -102,98 +39,79 @@ CLocker usage 2:
#endif
//-------------------------------------------------
// Types
//-------------------------------------------------
//-------------------------------------------------
// Declarations
//-------------------------------------------------
/**
* A Mutual Exclusion lock.
* A non-recursive mutual exclusion lock.
*/
class CMutex
{
NONCOPYABLE(CMutex);
friend class CScopeLock;
public:
inline CMutex()
CMutex()
{
pthread_mutex_init(&m_Mutex, NULL);
int ret = pthread_mutex_init(&m_Mutex, NULL);
debug_assert(ret == 0);
}
inline ~CMutex()
~CMutex()
{
if (pthread_mutex_destroy(&m_Mutex) != 0)
{
Unlock();
pthread_mutex_destroy(&m_Mutex);
}
}
/**
* Atomically wait for the mutex to become unlocked, then lock it.
*/
inline void Lock()
{
LOCK_MUTEX(&m_Mutex);
}
/**
* Unlock the mutex.
*/
inline void Unlock()
{
UNLOCK_MUTEX(&m_Mutex);
}
pthread_mutex_t m_Mutex;
};
// CScopeLock
// ---------------------------------------------------------------------| Class
// Locks a CMutex over the objects lifetime
class CScopeLock
{
NONCOPYABLE(CScopeLock);
public:
inline CScopeLock(pthread_mutex_t &mutex): m_Mutex(mutex)
{
LOCK_MUTEX(&m_Mutex);
}
inline CScopeLock(CMutex &mutex): m_Mutex(mutex.m_Mutex)
{
LOCK_MUTEX(&m_Mutex);
}
inline ~CScopeLock()
{
UNLOCK_MUTEX(&m_Mutex);
int ret = pthread_mutex_destroy(&m_Mutex);
debug_assert(ret == 0);
}
private:
pthread_mutex_t &m_Mutex;
pthread_mutex_t m_Mutex;
};
// CLocker
// ---------------------------------------------------------------------| Class
// This will not give access to the wrapped class constructors directly,
// to use them you have to do CLocker(CWrappedClass(args))
template <typename _T>
struct CLocker: public CMutex, public _T
/**
* Locks a CMutex over this object's lifetime.
* The mutexes are non-recursive - a single thread locking a mutex more than once
* results in undefined behaviour.
*/
class CScopeLock
{
public:
inline CLocker()
{}
NONCOPYABLE(CScopeLock);
/*
GCC doesn't take these... I don't understand what the problem is! // Simon
inline CLocker(const _T &arg): _T(arg)
{}
inline CLocker(_T &arg): _T(arg)
{}*/
public:
CScopeLock(pthread_mutex_t* mutex) :
m_Mutex(mutex)
{
LOCK_MUTEX(m_Mutex);
}
CScopeLock(CMutex& mutex) :
m_Mutex(&mutex.m_Mutex)
{
LOCK_MUTEX(m_Mutex);
}
~CScopeLock()
{
UNLOCK_MUTEX(m_Mutex);
}
private:
pthread_mutex_t* m_Mutex;
};
#endif
namespace ThreadUtil
{
/**
* Returns whether the current thread is the 'main' thread
* (i.e. matches an earlier call to SetMainThread).
*/
bool IsMainThread();
/**
* Set the current thread as the 'main' thread.
* (This is called during engine initialisation.)
*/
void SetMainThread();
}
#endif // INCLUDED_THREADUTIL

View File

@ -51,11 +51,6 @@ public:
logger->Log(CLogger::Normal, L"", L"%hs", msg2.c_str());
logger->Log(CLogger::Normal, L"", L"%hs", msg3.c_str());
logger->LogOnce(CLogger::Normal, L"", L"%hs", msg0.c_str());
logger->LogOnce(CLogger::Normal, L"", L"%hs", msg1.c_str());
logger->LogOnce(CLogger::Normal, L"", L"%hs", msg2.c_str());
logger->LogOnce(CLogger::Normal, L"", L"%hs", msg3.c_str());
logger->LogMessage(L"%hs", msg0.c_str());
logger->LogMessage(L"%hs", msg1.c_str());
logger->LogMessage(L"%hs", msg2.c_str());
@ -73,7 +68,7 @@ public:
ParseOutput();
TS_ASSERT_EQUALS((int)lines.size(), 4*5-1);
TS_ASSERT_EQUALS((int)lines.size(), 4*4);
TS_ASSERT_EQUALS(lines[0], msg0);
TS_ASSERT_EQUALS(lines[1], msg1);
TS_ASSERT_EQUALS(lines[2], clipped);
@ -82,21 +77,17 @@ public:
TS_ASSERT_EQUALS(lines[4], msg0);
TS_ASSERT_EQUALS(lines[5], msg1);
TS_ASSERT_EQUALS(lines[6], clipped);
TS_ASSERT_EQUALS(lines[7], clipped);
TS_ASSERT_EQUALS(lines[7], msg0);
TS_ASSERT_EQUALS(lines[8], msg1);
TS_ASSERT_EQUALS(lines[9], clipped);
TS_ASSERT_EQUALS(lines[10], clipped);
TS_ASSERT_EQUALS(lines[8], "WARNING: "+msg0);
TS_ASSERT_EQUALS(lines[9], "WARNING: "+msg1);
TS_ASSERT_EQUALS(lines[10], "WARNING: "+clipped);
TS_ASSERT_EQUALS(lines[11], "WARNING: "+clipped);
TS_ASSERT_EQUALS(lines[11], "WARNING: "+msg0);
TS_ASSERT_EQUALS(lines[12], "WARNING: "+msg1);
TS_ASSERT_EQUALS(lines[13], "WARNING: "+clipped);
TS_ASSERT_EQUALS(lines[14], "WARNING: "+clipped);
TS_ASSERT_EQUALS(lines[15], "ERROR: "+msg0);
TS_ASSERT_EQUALS(lines[16], "ERROR: "+msg1);
TS_ASSERT_EQUALS(lines[17], "ERROR: "+clipped);
TS_ASSERT_EQUALS(lines[18], "ERROR: "+clipped);
TS_ASSERT_EQUALS(lines[12], "ERROR: "+msg0);
TS_ASSERT_EQUALS(lines[13], "ERROR: "+msg1);
TS_ASSERT_EQUALS(lines[14], "ERROR: "+clipped);
TS_ASSERT_EQUALS(lines[15], "ERROR: "+clipped);
}
void test_unicode()

View File

@ -41,8 +41,6 @@
#include <boost/weak_ptr.hpp>
#define LOG_CATEGORY L"graphics"
///////////////////////////////////////////////////////////////////////////////////////////////
// ModelRenderer implementation
@ -82,7 +80,7 @@ void ModelRenderer::BuildPositionAndNormals(
// some broken situations
if (numVertices && vertices[0].m_Blend.m_Bone[0] == 0xff)
{
LOG_ONCE(CLogger::Error, LOG_CATEGORY, L"Model %ls is boned with unboned animation", mdef->GetName().string().c_str());
LOGERROR(L"Model %ls is boned with unboned animation", mdef->GetName().string().c_str());
return;
}

View File

@ -252,10 +252,14 @@ ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, JSContex
debug_assert(m_rt); // TODO: error handling
#if ENABLE_SCRIPT_PROFILING
if (CProfileManager::IsInitialised())
// Profiler isn't thread-safe, so only enable this on the main thread
if (ThreadUtil::IsMainThread())
{
JS_SetExecuteHook(m_rt, jshook_script, this);
JS_SetCallHook(m_rt, jshook_function, this);
if (CProfileManager::IsInitialised())
{
JS_SetExecuteHook(m_rt, jshook_script, this);
JS_SetCallHook(m_rt, jshook_function, this);
}
}
#endif
@ -316,14 +320,21 @@ void ScriptInterface_impl::Register(const char* name, JSFastNative fptr, uintN n
ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName) :
m(new ScriptInterface_impl(nativeScopeName, NULL))
{
if (g_ScriptStatsTable)
g_ScriptStatsTable->Add(this, debugName);
// Profiler stats table isn't thread-safe, so only enable this on the main thread
if (ThreadUtil::IsMainThread())
{
if (g_ScriptStatsTable)
g_ScriptStatsTable->Add(this, debugName);
}
}
ScriptInterface::~ScriptInterface()
{
if (g_ScriptStatsTable)
g_ScriptStatsTable->Remove(this);
if (ThreadUtil::IsMainThread())
{
if (g_ScriptStatsTable)
g_ScriptStatsTable->Remove(this);
}
}
void ScriptInterface::ShutDown()
@ -691,9 +702,16 @@ CScriptValRooted ScriptInterface::ParseJSON(const utf16string& string)
return CScriptValRooted(m->m_cx, vp);
}
CScriptValRooted ScriptInterface::ParseJSON(const std::string& string_utf8)
{
// TODO: we could do more efficient string conversion
std::wstring attrsW = wstring_from_utf8(string_utf8);
return ParseJSON(utf16string(attrsW.begin(), attrsW.end()));
}
struct Stringifier
{
static JSBool callback(const jschar *buf, uint32 len, void *data)
static JSBool callback(const jschar* buf, uint32 len, void* data)
{
utf16string str(buf, buf+len);
std::wstring strw(str.begin(), str.end());

View File

@ -50,6 +50,15 @@ namespace boost { class rand48; }
#endif
struct ScriptInterface_impl;
/**
* Abstraction around a SpiderMonkey JSContext.
*
* Thread-safety:
* - May be used in non-main threads.
* - Each ScriptInterface must be created, used, and destroyed, all in a single thread
* (it must never be shared between threads).
*/
class ScriptInterface
{
public:
@ -170,6 +179,11 @@ public:
*/
CScriptValRooted ParseJSON(const utf16string& string);
/**
* Parse a UTF-8-encoded JSON string. Returns the undefined value on error.
*/
CScriptValRooted ParseJSON(const std::string& string_utf8);
/**
* Stringify to a JSON string, UTF-8 encoded. Returns an empty string on error.
*/

View File

@ -138,8 +138,7 @@ public:
std::string stringified = script.StringifyJSON(val.get());
TS_ASSERT_STR_EQUALS(stringified, "{\n \"x\":1,\n \"z\":[2,\n \"3\xE2\x98\xBA\xEF\xBF\xBD\"\n ],\n \"y\":true\n}");
std::wstring stringifiedw = wstring_from_utf8(stringified.c_str());
val = script.ParseJSON(utf16string(stringifiedw.begin(), stringifiedw.end()));
val = script.ParseJSON(stringified);
TS_ASSERT_WSTR_EQUALS(script.ToString(val.get()), L"({x:1, z:[2, \"3\\u263A\\uFFFD\"], y:true})");
}
};

View File

@ -906,7 +906,7 @@ CScriptVal CComponentManager::Script_ReadJSONFile(void* cbdata, std::string file
return CScriptVal();
}
utf16string content(file.GetBuffer(), file.GetBuffer() + file.GetBufferSize()); // TODO: encodings etc
std::string content(file.GetBuffer(), file.GetBuffer() + file.GetBufferSize()); // assume it's UTF-8
return (componentManager->m_ScriptInterface.ParseJSON(content)).get();

View File

@ -85,9 +85,10 @@ void MessagePasserImpl::Add(IMessage* msg)
if (m_Trace)
debug_printf(L"%8.3f add message: %hs\n", timer_Time(), msg->GetName());
m_Mutex.Lock();
m_Queue.push(msg);
m_Mutex.Unlock();
{
CScopeLock lock(m_Mutex);
m_Queue.push(msg);
}
}
IMessage* MessagePasserImpl::Retrieve()
@ -96,16 +97,16 @@ IMessage* MessagePasserImpl::Retrieve()
// since there's only one thread adding items and one thread consuming;
// but it's not worthwhile yet.)
m_Mutex.Lock();
IMessage* msg = NULL;
if (! m_Queue.empty())
{
msg = m_Queue.front();
m_Queue.pop();
}
m_Mutex.Unlock();
{
CScopeLock lock(m_Mutex);
if (! m_Queue.empty())
{
msg = m_Queue.front();
m_Queue.pop();
}
}
if (m_Trace && msg)
debug_printf(L"%8.3f retrieved message: %hs\n", timer_Time(), msg->GetName());
@ -124,9 +125,10 @@ void MessagePasserImpl::Query(QueryMessage* qry, void(* UNUSED(timeoutCallback)
// Set the semaphore, so we can block until the query has been handled
qry->m_Semaphore = static_cast<void*>(m_Semaphore);
m_Mutex.Lock();
m_Queue.push(qry);
m_Mutex.Unlock();
{
CScopeLock lock(m_Mutex);
m_Queue.push(qry);
}
// Wait until the query handler has handled the query and called sem_post:
@ -183,10 +185,8 @@ void MessagePasserImpl::Query(QueryMessage* qry, void(* UNUSED(timeoutCallback)
bool MessagePasserImpl::IsEmpty()
{
m_Mutex.Lock();
bool empty = m_Queue.empty();
m_Mutex.Unlock();
return empty;
CScopeLock lock(m_Mutex);
return m_Queue.empty();
}
void MessagePasserImpl::SetTrace(bool t)