Index: ps/trunk/source/graphics/MapGenerator.cpp
===================================================================
--- ps/trunk/source/graphics/MapGenerator.cpp (revision 24170)
+++ ps/trunk/source/graphics/MapGenerator.cpp (revision 24171)
@@ -1,440 +1,440 @@
/* Copyright (C) 2020 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 "MapGenerator.h"
#include "graphics/MapIO.h"
#include "graphics/Patch.h"
#include "graphics/Terrain.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/status.h"
#include "lib/timer.h"
#include "lib/file/vfs/vfs_path.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/FileIo.h"
#include "ps/Profile.h"
#include "ps/scripting/JSInterface_VFS.h"
#include "scriptinterface/ScriptRuntime.h"
#include "scriptinterface/ScriptConversions.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/helpers/MapEdgeTiles.h"
#include
#include
-// TODO: what's a good default? perhaps based on map size
-#define RMS_RUNTIME_SIZE 96 * 1024 * 1024
+// TODO: Maybe this should be optimized depending on the map size.
+constexpr int RMS_RUNTIME_SIZE = 96 * 1024 * 1024;
extern bool IsQuitRequested();
static bool
MapGeneratorInterruptCallback(JSContext* UNUSED(cx))
{
// This may not use SDL_IsQuitRequested(), because it runs in a thread separate to SDL, see SDL_PumpEvents
if (IsQuitRequested())
{
LOGWARNING("Quit requested!");
return false;
}
return true;
}
CMapGeneratorWorker::CMapGeneratorWorker(ScriptInterface* scriptInterface) :
m_ScriptInterface(scriptInterface)
{
// If something happens before we initialize, that's a failure
m_Progress = -1;
}
CMapGeneratorWorker::~CMapGeneratorWorker()
{
// Wait for thread to end
if (m_WorkerThread.joinable())
m_WorkerThread.join();
}
void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings)
{
std::lock_guard lock(m_WorkerMutex);
// Set progress to positive value
m_Progress = 1;
m_ScriptPath = scriptFile;
m_Settings = settings;
// Launch the worker thread
m_WorkerThread = std::thread(RunThread, this);
}
void* CMapGeneratorWorker::RunThread(CMapGeneratorWorker* self)
{
debug_SetThreadName("MapGenerator");
g_Profiler2.RegisterCurrentThread("MapGenerator");
- shared_ptr mapgenRuntime = ScriptInterface::CreateRuntime(g_ScriptRuntime, RMS_RUNTIME_SIZE);
+ shared_ptr mapgenRuntime = ScriptRuntime::CreateRuntime(RMS_RUNTIME_SIZE);
// Enable the script to be aborted
JS_SetInterruptCallback(mapgenRuntime->m_rt, MapGeneratorInterruptCallback);
self->m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenRuntime);
// Run map generation scripts
if (!self->Run() || self->m_Progress > 0)
{
// Don't leave progress in an unknown state, if generator failed, set it to -1
std::lock_guard lock(self->m_WorkerMutex);
self->m_Progress = -1;
}
SAFE_DELETE(self->m_ScriptInterface);
// At this point the random map scripts are done running, so the thread has no further purpose
// and can die. The data will be stored in m_MapData already if successful, or m_Progress
// will contain an error value on failure.
return NULL;
}
bool CMapGeneratorWorker::Run()
{
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
// Parse settings
JS::RootedValue settingsVal(cx);
if (!m_ScriptInterface->ParseJSON(m_Settings, &settingsVal) && settingsVal.isUndefined())
{
LOGERROR("CMapGeneratorWorker::Run: Failed to parse settings");
return false;
}
// Prevent unintentional modifications to the settings object by random map scripts
if (!m_ScriptInterface->FreezeObject(settingsVal, true))
{
LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings");
return false;
}
// Init RNG seed
u32 seed = 0;
if (!m_ScriptInterface->HasProperty(settingsVal, "Seed") ||
!m_ScriptInterface->GetProperty(settingsVal, "Seed", seed))
LOGWARNING("CMapGeneratorWorker::Run: No seed value specified - using 0");
InitScriptInterface(seed);
RegisterScriptFunctions_MapGenerator();
// Copy settings to global variable
JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject());
if (!m_ScriptInterface->SetProperty(global, "g_MapSettings", settingsVal, true, true))
{
LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings");
return false;
}
// Load RMS
LOGMESSAGE("Loading RMS '%s'", m_ScriptPath.string8());
if (!m_ScriptInterface->LoadGlobalScriptFile(m_ScriptPath))
{
LOGERROR("CMapGeneratorWorker::Run: Failed to load RMS '%s'", m_ScriptPath.string8());
return false;
}
return true;
}
void CMapGeneratorWorker::InitScriptInterface(const u32 seed)
{
m_ScriptInterface->SetCallbackData(static_cast(this));
m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG);
m_MapGenRNG.seed(seed);
// VFS
JSI_VFS::RegisterScriptFunctions_Maps(*m_ScriptInterface);
// Globalscripts may use VFS script functions
m_ScriptInterface->LoadGlobalScripts();
// File loading
m_ScriptInterface->RegisterFunction("LoadLibrary");
m_ScriptInterface->RegisterFunction("LoadHeightmapImage");
m_ScriptInterface->RegisterFunction("LoadMapTerrain");
// Engine constants
// Length of one tile of the terrain grid in metres.
// Useful to transform footprint sizes to the tilegrid coordinate system.
m_ScriptInterface->SetGlobal("TERRAIN_TILE_SIZE", static_cast(TERRAIN_TILE_SIZE));
// Number of impassable tiles at the map border
m_ScriptInterface->SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES));
}
void CMapGeneratorWorker::RegisterScriptFunctions_MapGenerator()
{
// Template functions
m_ScriptInterface->RegisterFunction("GetTemplate");
m_ScriptInterface->RegisterFunction("TemplateExists");
m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindTemplates>("FindTemplates");
m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindActorTemplates>("FindActorTemplates");
// Progression and profiling
m_ScriptInterface->RegisterFunction("SetProgress");
m_ScriptInterface->RegisterFunction("GetMicroseconds");
m_ScriptInterface->RegisterFunction("ExportMap");
}
int CMapGeneratorWorker::GetProgress()
{
std::lock_guard lock(m_WorkerMutex);
return m_Progress;
}
double CMapGeneratorWorker::GetMicroseconds(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return JS_Now();
}
shared_ptr CMapGeneratorWorker::GetResults()
{
std::lock_guard lock(m_WorkerMutex);
return m_MapData;
}
bool CMapGeneratorWorker::LoadLibrary(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& name)
{
CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData);
return self->LoadScripts(name);
}
void CMapGeneratorWorker::ExportMap(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data)
{
CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData);
// Copy results
std::lock_guard lock(self->m_WorkerMutex);
self->m_MapData = self->m_ScriptInterface->WriteStructuredClone(data);
self->m_Progress = 0;
}
void CMapGeneratorWorker::SetProgress(ScriptInterface::CxPrivate* pCxPrivate, int progress)
{
CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData);
// Copy data
std::lock_guard lock(self->m_WorkerMutex);
if (progress >= self->m_Progress)
self->m_Progress = progress;
else
LOGWARNING("The random map script tried to reduce the loading progress from %d to %d", self->m_Progress, progress);
}
CParamNode CMapGeneratorWorker::GetTemplate(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName)
{
CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData);
const CParamNode& templateRoot = self->m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity");
if (!templateRoot.IsOk())
LOGERROR("Invalid template found for '%s'", templateName.c_str());
return templateRoot;
}
bool CMapGeneratorWorker::TemplateExists(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName)
{
CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData);
return self->m_TemplateLoader.TemplateExists(templateName);
}
std::vector CMapGeneratorWorker::FindTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories)
{
CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData);
return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES);
}
std::vector CMapGeneratorWorker::FindActorTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories)
{
CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData);
return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES);
}
bool CMapGeneratorWorker::LoadScripts(const VfsPath& libraryName)
{
// Ignore libraries that are already loaded
if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end())
return true;
// Mark this as loaded, to prevent it recursively loading itself
m_LoadedLibraries.insert(libraryName);
VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath();
VfsPaths pathnames;
// Load all scripts in mapgen directory
Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames);
if (ret == INFO::OK)
{
for (const VfsPath& p : pathnames)
{
LOGMESSAGE("Loading map generator script '%s'", p.string8());
if (!m_ScriptInterface->LoadGlobalScriptFile(p))
{
LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8());
return false;
}
}
}
else
{
// Some error reading directory
wchar_t error[200];
LOGERROR("CMapGeneratorWorker::LoadScripts: Error reading scripts in directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error))));
return false;
}
return true;
}
JS::Value CMapGeneratorWorker::LoadHeightmap(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& filename)
{
std::vector heightmap;
if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK)
{
LOGERROR("Could not load heightmap file '%s'", filename.string8());
return JS::UndefinedValue();
}
CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData);
JSContext* cx = self->m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue returnValue(cx);
ToJSVal_vector(cx, &returnValue, heightmap);
return returnValue;
}
// See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering
JS::Value CMapGeneratorWorker::LoadMapTerrain(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& filename)
{
CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData);
JSContext* cx = self->m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
if (!VfsFileExists(filename))
{
self->m_ScriptInterface->ReportError(
("Terrain file \"" + filename.string8() + "\" does not exist!").c_str());
return JS::UndefinedValue();
}
CFileUnpacker unpacker;
unpacker.Read(filename, "PSMP");
if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION)
{
self->m_ScriptInterface->ReportError(
("Could not load terrain file \"" + filename.string8() + "\" too old version!").c_str());
return JS::UndefinedValue();
}
// unpack size
ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize();
size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1;
// unpack heightmap
std::vector heightmap;
heightmap.resize(SQR(verticesPerSide));
unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16));
// unpack texture names
size_t textureCount = unpacker.UnpackSize();
std::vector textureNames;
textureNames.reserve(textureCount);
for (size_t i = 0; i < textureCount; ++i)
{
CStr texturename;
unpacker.UnpackString(texturename);
textureNames.push_back(texturename);
}
// unpack texture IDs per tile
ssize_t tilesPerSide = patchesPerSide * PATCH_SIZE;
std::vector tiles;
tiles.resize(size_t(SQR(tilesPerSide)));
unpacker.UnpackRaw(&tiles[0], sizeof(CMapIO::STileDesc) * tiles.size());
// reorder by patches and store and save texture IDs per tile
std::vector textureIDs;
for (ssize_t x = 0; x < tilesPerSide; ++x)
{
size_t patchX = x / PATCH_SIZE;
size_t offX = x % PATCH_SIZE;
for (ssize_t y = 0; y < tilesPerSide; ++y)
{
size_t patchY = y / PATCH_SIZE;
size_t offY = y % PATCH_SIZE;
// m_Priority and m_Tex2Index unused
textureIDs.push_back(tiles[(patchY * patchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)].m_Tex1Index);
}
}
JS::RootedValue returnValue(cx);
ScriptInterface::CreateObject(
cx,
&returnValue,
"height", heightmap,
"textureNames", textureNames,
"textureIDs", textureIDs);
return returnValue;
}
//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////
CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker(nullptr))
{
}
CMapGenerator::~CMapGenerator()
{
delete m_Worker;
}
void CMapGenerator::GenerateMap(const VfsPath& scriptFile, const std::string& settings)
{
m_Worker->Initialize(scriptFile, settings);
}
int CMapGenerator::GetProgress()
{
return m_Worker->GetProgress();
}
shared_ptr CMapGenerator::GetResults()
{
return m_Worker->GetResults();
}
Index: ps/trunk/source/network/NetServer.cpp
===================================================================
--- ps/trunk/source/network/NetServer.cpp (revision 24170)
+++ ps/trunk/source/network/NetServer.cpp (revision 24171)
@@ -1,1638 +1,1639 @@
/* Copyright (C) 2020 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 "NetServer.h"
#include "NetClient.h"
#include "NetMessage.h"
#include "NetSession.h"
#include "NetServerTurnManager.h"
#include "NetStats.h"
#include "lib/external_libraries/enet.h"
#include "lib/types.h"
#include "network/StunClient.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/GUID.h"
#include "ps/Profile.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptRuntime.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/TurnManager.h"
#if CONFIG2_MINIUPNPC
#include
#include
#include
#include
#endif
#include
/**
* Number of peers to allocate for the enet host.
* Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
*
* At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
*/
#define MAX_CLIENTS 41
#define DEFAULT_SERVER_NAME L"Unnamed Server"
static const int CHANNEL_COUNT = 1;
/**
* 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)
{
if (session == NULL)
return "[unknown host]";
if (session->GetGUID().empty())
return "[unauthed host]";
return "[" + session->GetGUID().substr(0, 8) + "...]";
}
/**
* Async task for receiving the initial game state to be forwarded to another
* client that is rejoining an in-progress network game.
*/
class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask
{
NONCOPYABLE(CNetFileReceiveTask_ServerRejoin);
public:
CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID)
: m_Server(server), m_RejoinerHostID(hostID)
{
}
virtual void OnComplete()
{
// We've received the game state from an existing player - now
// we need to send it onwards to the newly rejoining player
// Find the session corresponding to the rejoining host (if any)
CNetServerSession* session = NULL;
for (CNetServerSession* serverSession : m_Server.m_Sessions)
{
if (serverSession->GetHostID() == m_RejoinerHostID)
{
session = serverSession;
break;
}
}
if (!session)
{
LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
return;
}
// Store the received state file, and tell the client to start downloading it from us
// TODO: this will get kind of confused if there's multiple clients downloading in parallel;
// they'll race and get whichever happens to be the latest received by the server,
// which should still work but isn't great
m_Server.m_JoinSyncFile = m_Buffer;
CJoinSyncStartMessage message;
session->SendMessage(&message);
}
private:
CNetServerWorker& m_Server;
u32 m_RejoinerHostID;
};
/*
* XXX: We use some non-threadsafe functions from the worker thread.
* See http://trac.wildfiregames.com/ticket/654
*/
CNetServerWorker::CNetServerWorker(bool useLobbyAuth, int autostartPlayers) :
m_AutostartPlayers(autostartPlayers),
m_LobbyAuth(useLobbyAuth),
m_Shutdown(false),
m_ScriptInterface(NULL),
m_NextHostID(1), m_Host(NULL), m_HostGUID(), m_Stats(NULL),
m_LastConnectionCheck(0)
{
m_State = SERVER_STATE_UNCONNECTED;
m_ServerTurnManager = NULL;
m_ServerName = DEFAULT_SERVER_NAME;
}
CNetServerWorker::~CNetServerWorker()
{
if (m_State != SERVER_STATE_UNCONNECTED)
{
// Tell the thread to shut down
{
std::lock_guard lock(m_WorkerMutex);
m_Shutdown = true;
}
// Wait for it to shut down cleanly
m_WorkerThread.join();
}
#if CONFIG2_MINIUPNPC
if (m_UPnPThread.joinable())
m_UPnPThread.detach();
#endif
// Clean up resources
delete m_Stats;
for (CNetServerSession* session : m_Sessions)
{
session->DisconnectNow(NDR_SERVER_SHUTDOWN);
delete session;
}
if (m_Host)
enet_host_destroy(m_Host);
delete m_ServerTurnManager;
}
bool CNetServerWorker::SetupConnection(const u16 port)
{
ENSURE(m_State == SERVER_STATE_UNCONNECTED);
ENSURE(!m_Host);
// Bind to default host
ENetAddress addr;
addr.host = ENET_HOST_ANY;
addr.port = port;
// Create ENet server
m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0);
if (!m_Host)
{
LOGERROR("Net server: enet_host_create failed");
return false;
}
m_Stats = new CNetStatsTable();
if (CProfileViewer::IsInitialised())
g_ProfileViewer.AddRootTable(m_Stats);
m_State = SERVER_STATE_PREGAME;
// Launch the worker thread
m_WorkerThread = std::thread(RunThread, this);
#if CONFIG2_MINIUPNPC
// Launch the UPnP thread
m_UPnPThread = std::thread(SetupUPnP);
#endif
return true;
}
#if CONFIG2_MINIUPNPC
void CNetServerWorker::SetupUPnP()
{
debug_SetThreadName("UPnP");
// Values we want to set.
char psPort[6];
sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT);
const char* leaseDuration = "0"; // Indefinite/permanent lease duration.
const char* description = "0AD Multiplayer";
const char* protocall = "UDP";
char internalIPAddress[64];
char externalIPAddress[40];
// Variables to hold the values that actually get set.
char intClient[40];
char intPort[6];
char duration[16];
// Intermediate variables.
bool allocatedUrls = false;
struct UPNPUrls urls;
struct IGDdatas data;
struct UPNPDev* devlist = NULL;
// Make sure everything is properly freed.
std::function freeUPnP = [&allocatedUrls, &urls, &devlist]()
{
if (allocatedUrls)
FreeUPNPUrls(&urls);
freeUPNPDevlist(devlist);
// IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl
};
// Cached root descriptor URL.
std::string rootDescURL;
CFG_GET_VAL("network.upnprootdescurl", rootDescURL);
if (!rootDescURL.empty())
LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str());
int ret = 0;
// Try a cached URL first
if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress)))
{
LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL);
ret = 1;
}
// No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds.
#if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14
else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL)
#else
else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL)
#endif
{
ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress));
allocatedUrls = ret != 0; // urls is allocated on non-zero return values
}
else
{
LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
freeUPnP();
return;
}
switch (ret)
{
case 0:
LOGMESSAGE("Net server: No IGD found");
break;
case 1:
LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL);
break;
case 2:
LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL);
break;
case 3:
LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL);
break;
default:
debug_warn(L"Unrecognized return value from UPNP_GetValidIGD");
}
// Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance.
ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret));
freeUPnP();
return;
}
LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress);
// Try to setup port forwarding.
ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort,
internalIPAddress, description, protocall, 0, leaseDuration);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
psPort, psPort, internalIPAddress, ret, strupnperror(ret));
freeUPnP();
return;
}
// Check that the port was actually forwarded.
ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL,
data.first.servicetype,
psPort, protocall,
#if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10
NULL/*remoteHost*/,
#endif
intClient, intPort, NULL/*desc*/,
NULL/*enabled*/, duration);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret));
freeUPnP();
return;
}
LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
externalIPAddress, psPort, protocall, intClient, intPort, duration);
// Cache root descriptor URL to try to avoid discovery next time.
g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL);
g_ConfigDB.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL);
LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL);
freeUPnP();
}
#endif // CONFIG2_MINIUPNPC
bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
{
ENSURE(m_Host);
CNetServerSession* session = static_cast(peer->data);
return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
}
bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector& targetStates)
{
ENSURE(m_Host);
bool ok = true;
// TODO: this does lots of repeated message serialisation if we have lots
// of remote peers; could do it more efficiently if that's a real problem
for (CNetServerSession* session : m_Sessions)
if (std::find(targetStates.begin(), targetStates.end(), session->GetCurrState()) != targetStates.end() &&
!session->SendMessage(message))
ok = false;
return ok;
}
void CNetServerWorker::RunThread(CNetServerWorker* data)
{
debug_SetThreadName("NetServer");
data->Run();
}
void CNetServerWorker::Run()
{
// The script runtime uses the profiler and therefore the thread must be registered before the runtime is created
g_Profiler2.RegisterCurrentThread("Net server");
// 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", ScriptInterface::CreateRuntime(g_ScriptRuntime));
+ shared_ptr netServerRuntime = ScriptRuntime::CreateRuntime();
+ m_ScriptInterface = new ScriptInterface("Engine", "Net server", netServerRuntime);
m_GameAttributes.init(m_ScriptInterface->GetJSRuntime(), JS::UndefinedValue());
while (true)
{
if (!RunStep())
break;
// Implement autostart mode
if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers)
StartGame();
// Update profiler stats
m_Stats->LatchHostState(m_Host);
}
// Clear roots before deleting their context
m_SavedCommands.clear();
SAFE_DELETE(m_ScriptInterface);
}
bool CNetServerWorker::RunStep()
{
// 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.)
m_ScriptInterface->GetRuntime()->MaybeIncrementalGC(0.5f);
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
std::vector newStartGame;
std::vector newGameAttributes;
std::vector> newLobbyAuths;
std::vector newTurnLength;
{
std::lock_guard lock(m_WorkerMutex);
if (m_Shutdown)
return false;
newStartGame.swap(m_StartGameQueue);
newGameAttributes.swap(m_GameAttributesQueue);
newLobbyAuths.swap(m_LobbyAuthQueue);
newTurnLength.swap(m_TurnLengthQueue);
}
if (!newGameAttributes.empty())
{
JS::RootedValue gameAttributesVal(cx);
GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal);
UpdateGameAttributes(&gameAttributesVal);
}
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();
while (!newLobbyAuths.empty())
{
const std::pair& auth = newLobbyAuths.back();
ProcessLobbyAuth(auth.first, auth.second);
newLobbyAuths.pop_back();
}
// Perform file transfers
for (CNetServerSession* session : m_Sessions)
session->GetFileTransferer().Poll();
CheckClientConnections();
// Process network events:
ENetEvent event;
int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
if (status < 0)
{
LOGERROR("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("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
// Set up a session object for this peer
CNetServerSession* session = new CNetServerSession(*this, event.peer);
m_Sessions.push_back(session);
SetupSession(session);
ENSURE(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(event.peer->data);
if (session)
{
LOGMESSAGE("Net server: Disconnected %s", 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;
}
if (m_State == SERVER_STATE_LOADING)
CheckGameLoadStatus(NULL);
break;
}
case ENET_EVENT_TYPE_RECEIVE:
{
// If there is an active session with this peer, then process the message
CNetServerSession* session = static_cast(event.peer->data);
if (session)
{
// Create message from raw data
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
if (msg)
{
LOGMESSAGE("Net server: Received message %s of size %lu from %s", 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;
}
case ENET_EVENT_TYPE_NONE:
break;
}
return true;
}
void CNetServerWorker::CheckClientConnections()
{
// Send messages at most once per second
std::time_t now = std::time(nullptr);
if (now <= m_LastConnectionCheck)
return;
m_LastConnectionCheck = now;
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
u32 lastReceived = m_Sessions[i]->GetLastReceivedTime();
u32 meanRTT = m_Sessions[i]->GetMeanRTT();
CNetMessage* message = nullptr;
// Report if we didn't hear from the client since few seconds
if (lastReceived > NETWORK_WARNING_TIMEOUT)
{
CClientTimeoutMessage* msg = new CClientTimeoutMessage();
msg->m_GUID = m_Sessions[i]->GetGUID();
msg->m_LastReceivedTime = lastReceived;
message = msg;
}
// Report if the client has bad ping
else if (meanRTT > DEFAULT_TURN_LENGTH_MP)
{
CClientPerformanceMessage* msg = new CClientPerformanceMessage();
CClientPerformanceMessage::S_m_Clients client;
client.m_GUID = m_Sessions[i]->GetGUID();
client.m_MeanRTT = meanRTT;
msg->m_Clients.push_back(client);
message = msg;
}
// Send to all clients except the affected one
// (since that will show the locally triggered warning instead).
// Also send it to clients that finished the loading screen while
// the game is still waiting for other clients to finish the loading screen.
if (message)
for (size_t j = 0; j < m_Sessions.size(); ++j)
{
if (i != j && (
(m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) ||
m_Sessions[j]->GetCurrState() == NSS_INGAME))
{
m_Sessions[j]->SendMessage(message);
}
}
SAFE_DELETE(message);
}
}
void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
{
// Handle non-FSM messages first
Status status = session->GetFileTransferer().HandleMessageReceive(*message);
if (status != INFO::SKIPPED)
return;
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{
CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
// Rejoining client got our JoinSyncStart after we received the state from
// another client, and has now requested that we forward it to them
ENSURE(!m_JoinSyncFile.empty());
session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile);
return;
}
// Update FSM
if (!session->Update(message->GetType(), (void*)message))
LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
}
void CNetServerWorker::SetupSession(CNetServerSession* session)
{
void* context = session;
// Set up transitions for session
session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, (void*)&OnClientHandshake, context);
session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, (void*)&OnChat, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, (void*)&OnReady, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, (void*)&OnClearAllReady, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, (void*)&OnGameSetup, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, (void*)&OnAssignPlayer, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnStartGame, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, (void*)&OnRejoined, context);
session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, (void*)&OnClientPaused, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnSimulationCommand, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnSyncCheck, context);
session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnEndCommandBatch, context);
// Set first state
session->SetFirstState(NSS_HANDSHAKE);
}
bool CNetServerWorker::HandleConnect(CNetServerSession* session)
{
if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end())
{
session->Disconnect(NDR_BANNED);
return false;
}
CSrvHandshakeMessage handshake;
handshake.m_Magic = PS_PROTOCOL_MAGIC;
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
return session->SendMessage(&handshake);
}
void CNetServerWorker::OnUserJoin(CNetServerSession* session)
{
AddPlayer(session->GetGUID(), session->GetUserName());
if (m_HostGUID.empty() && session->IsLocalClient())
m_HostGUID = session->GetGUID();
CGameSetupMessage gameSetupMessage(GetScriptInterface());
gameSetupMessage.m_Data = m_GameAttributes;
session->SendMessage(&gameSetupMessage);
CPlayerAssignmentMessage assignMessage;
ConstructPlayerAssignmentMessage(assignMessage);
session->SendMessage(&assignMessage);
}
void CNetServerWorker::OnUserLeave(CNetServerSession* session)
{
std::vector::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID());
if (pausing != m_PausingPlayers.end())
m_PausingPlayers.erase(pausing);
RemovePlayer(session->GetGUID());
if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers
// TODO: ought to switch the player controlled by that client
// back to AI control, or something?
}
void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
{
// Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
std::set usedIDs;
for (const std::pair& p : m_PlayerAssignments)
if (p.second.m_Enabled && p.second.m_PlayerID != -1)
usedIDs.insert(p.second.m_PlayerID);
// If the player is rejoining after disconnecting, try to give them
// back their old player ID
i32 playerID = -1;
// Try to match GUID first
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
goto found;
}
}
// Try to match username next
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
goto found;
}
}
// Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed.
found:
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = name;
assignment.m_PlayerID = playerID;
assignment.m_Status = 0;
m_PlayerAssignments[guid] = assignment;
// Send the new assignments to all currently active players
// (which does not include the one that's just joining)
SendPlayerAssignments();
}
void CNetServerWorker::RemovePlayer(const CStr& guid)
{
m_PlayerAssignments[guid].m_Enabled = false;
SendPlayerAssignments();
}
void CNetServerWorker::ClearAllPlayerReady()
{
for (std::pair& p : m_PlayerAssignments)
if (p.second.m_Status != 2)
p.second.m_Status = 0;
SendPlayerAssignments();
}
void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban)
{
// Find the user with that name
std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
[&](CNetServerSession* session) { return session->GetUserName() == playerName; });
// and return if no one or the host has that name
if (it == m_Sessions.end() || (*it)->GetGUID() == m_HostGUID)
return;
if (ban)
{
// Remember name
if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end())
m_BannedPlayers.push_back(m_LobbyAuth ? CStrW(playerName.substr(0, playerName.find(L" ("))) : playerName);
// Remember IP address
u32 ipAddress = (*it)->GetIPAddress();
if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end())
m_BannedIPs.push_back(ipAddress);
}
// Disconnect that user
(*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED);
// Send message notifying other clients
CKickedMessage kickedMessage;
kickedMessage.m_Name = playerName;
kickedMessage.m_Ban = ban;
Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
}
void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
{
// Remove anyone who's already assigned to this player
for (std::pair& p : m_PlayerAssignments)
{
if (p.second.m_PlayerID == playerID)
p.second.m_PlayerID = -1;
}
// Update this host's assignment if it exists
if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end())
m_PlayerAssignments[guid].m_PlayerID = playerID;
SendPlayerAssignments();
}
void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
{
for (const std::pair& p : m_PlayerAssignments)
{
if (!p.second.m_Enabled)
continue;
CPlayerAssignmentMessage::S_m_Hosts h;
h.m_GUID = p.first;
h.m_Name = p.second.m_Name;
h.m_PlayerID = p.second.m_PlayerID;
h.m_Status = p.second.m_Status;
message.m_Hosts.push_back(h);
}
}
void CNetServerWorker::SendPlayerAssignments()
{
CPlayerAssignmentMessage message;
ConstructPlayerAssignmentMessage(message);
Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
}
const ScriptInterface& CNetServerWorker::GetScriptInterface()
{
return *m_ScriptInterface;
}
void CNetServerWorker::SetTurnLength(u32 msecs)
{
if (m_ServerTurnManager)
m_ServerTurnManager->SetTurnLength(msecs);
}
void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token)
{
LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token);
// Find the user with that guid
std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
[&](CNetServerSession* session)
{ return session->GetGUID() == token; });
if (it == m_Sessions.end())
return;
(*it)->SetUserName(name.FromUTF8());
// Send an empty message to request the authentication message from the client
// after its identity has been confirmed via the lobby
CAuthenticateMessage emptyMessage;
(*it)->SendMessage(&emptyMessage);
}
bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef();
if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION)
{
session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION);
return false;
}
CStr guid = ps_generate_guid();
int count = 0;
// Ensure unique GUID
while(std::find_if(
server.m_Sessions.begin(), server.m_Sessions.end(),
[&guid] (const CNetServerSession* session)
{ return session->GetGUID() == guid; }) != server.m_Sessions.end())
{
if (++count > 100)
{
session->Disconnect(NDR_GUID_FAILED);
return true;
}
guid = ps_generate_guid();
}
session->SetGUID(guid);
CSrvHandshakeResponseMessage handshakeResponse;
handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
handshakeResponse.m_GUID = guid;
handshakeResponse.m_Flags = 0;
if (server.m_LobbyAuth)
{
handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH;
session->SetNextState(NSS_LOBBY_AUTHENTICATE);
}
session->SendMessage(&handshakeResponse);
return true;
}
bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Prohibit joins while the game is loading
if (server.m_State == SERVER_STATE_LOADING)
{
LOGMESSAGE("Refused connection while the game is loading");
session->Disconnect(NDR_SERVER_LOADING);
return true;
}
CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
CStrW username = SanitisePlayerName(message->m_Name);
CStrW usernameWithoutRating(username.substr(0, username.find(L" (")));
// Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
// "[...] comparisons will be made in case-normalized canonical form."
if (server.m_LobbyAuth && usernameWithoutRating.LowerCase() != session->GetUserName().LowerCase())
{
LOGERROR("Net server: lobby auth: %s tried joining as %s",
session->GetUserName().ToUTF8(),
usernameWithoutRating.ToUTF8());
session->Disconnect(NDR_LOBBY_AUTH_FAILED);
return true;
}
// Either deduplicate or prohibit join if name is in use
bool duplicatePlayernames = false;
CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames);
// If lobby authentication is enabled, the clients playername has already been registered.
// There also can't be any duplicated names.
if (!server.m_LobbyAuth && duplicatePlayernames)
username = server.DeduplicatePlayerName(username);
else
{
std::vector::iterator it = std::find_if(
server.m_Sessions.begin(), server.m_Sessions.end(),
[&username] (const CNetServerSession* session)
{ return session->GetUserName() == username; });
if (it != server.m_Sessions.end() && (*it) != session)
{
session->Disconnect(NDR_PLAYERNAME_IN_USE);
return true;
}
}
// Disconnect banned usernames
if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), server.m_LobbyAuth ? usernameWithoutRating : username) != server.m_BannedPlayers.end())
{
session->Disconnect(NDR_BANNED);
return true;
}
int maxObservers = 0;
CFG_GET_VAL("network.observerlimit", maxObservers);
bool isRejoining = false;
bool serverFull = false;
if (server.m_State == SERVER_STATE_PREGAME)
{
// Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
serverFull = server.m_Sessions.size() >= MAX_CLIENTS;
}
else
{
bool isObserver = true;
int disconnectedPlayers = 0;
int connectedPlayers = 0;
// (TODO: if GUIDs were stable, we should use them instead)
for (const std::pair& p : server.m_PlayerAssignments)
{
const PlayerAssignment& assignment = p.second;
if (!assignment.m_Enabled && assignment.m_Name == username)
{
isObserver = assignment.m_PlayerID == -1;
isRejoining = true;
}
if (assignment.m_PlayerID == -1)
continue;
if (assignment.m_Enabled)
++connectedPlayers;
else
++disconnectedPlayers;
}
// Optionally allow everyone or only buddies to join after the game has started
if (!isRejoining)
{
CStr observerLateJoin;
CFG_GET_VAL("network.lateobservers", observerLateJoin);
if (observerLateJoin == "everyone")
{
isRejoining = true;
}
else if (observerLateJoin == "buddies")
{
CStr buddies;
CFG_GET_VAL("lobby.buddies", buddies);
std::wstringstream buddiesStream(wstring_from_utf8(buddies));
CStrW buddy;
while (std::getline(buddiesStream, buddy, L','))
{
if (buddy == usernameWithoutRating)
{
isRejoining = true;
break;
}
}
}
}
if (!isRejoining)
{
LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username));
session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
return true;
}
// Ensure all players will be able to rejoin
serverFull = isObserver && (
(int) server.m_Sessions.size() - connectedPlayers > maxObservers ||
(int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS);
}
if (serverFull)
{
session->Disconnect(NDR_SERVER_FULL);
return true;
}
// TODO: check server password etc?
u32 newHostID = server.m_NextHostID++;
session->SetUserName(username);
session->SetHostID(newHostID);
session->SetLocalClient(message->m_IsLocalClient);
CAuthenticateResultMessage authenticateResult;
authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
authenticateResult.m_HostID = newHostID;
authenticateResult.m_Message = L"Logged in";
session->SendMessage(&authenticateResult);
server.OnUserJoin(session);
if (isRejoining)
{
// Request a copy of the current game state from an existing player,
// so we can send it on to the new player
// Assume session 0 is most likely the local player, so they're
// the most efficient client to request a copy from
CNetServerSession* sourceSession = server.m_Sessions.at(0);
session->SetLongTimeout(true);
sourceSession->GetFileTransferer().StartTask(
shared_ptr(new CNetFileReceiveTask_ServerRejoin(server, newHostID))
);
session->SetNextState(NSS_JOIN_SYNCING);
}
return true;
}
bool CNetServerWorker::OnSimulationCommand(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SIMULATION_COMMAND);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CSimulationMessage* message = (CSimulationMessage*)event->GetParamRef();
// Ignore messages sent by one player on behalf of another player
// unless cheating is enabled
bool cheatsEnabled = false;
const ScriptInterface& scriptInterface = server.GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue settings(cx);
scriptInterface.GetProperty(server.m_GameAttributes, "settings", &settings);
if (scriptInterface.HasProperty(settings, "CheatsEnabled"))
scriptInterface.GetProperty(settings, "CheatsEnabled", cheatsEnabled);
PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID());
// When cheating is disabled, fail if the player the message claims to
// represent does not exist or does not match the sender's player name
if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player))
return true;
// Send it back to all clients that have finished
// the loading screen (and the synchronization when rejoining)
server.Broadcast(message, { NSS_INGAME });
// Save all the received commands
if (server.m_SavedCommands.size() < message->m_Turn + 1)
server.m_SavedCommands.resize(message->m_Turn + 1);
server.m_SavedCommands[message->m_Turn].push_back(*message);
// TODO: we shouldn't send the message back to the client that first sent it
return true;
}
bool CNetServerWorker::OnSyncCheck(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CSyncCheckMessage* message = (CSyncCheckMessage*)event->GetParamRef();
server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash);
return true;
}
bool CNetServerWorker::OnEndCommandBatch(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CEndCommandBatchMessage* message = (CEndCommandBatchMessage*)event->GetParamRef();
// The turn-length field is ignored
server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, message->m_Turn);
return true;
}
bool CNetServerWorker::OnChat(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CHAT);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CChatMessage* message = (CChatMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_PREGAME, NSS_INGAME });
return true;
}
bool CNetServerWorker::OnReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_READY);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Occurs if a client presses not-ready
// in the very last moment before the hosts starts the game
if (server.m_State == SERVER_STATE_LOADING)
return true;
CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_PREGAME });
server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status;
return true;
}
bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_HostGUID)
server.ClearAllPlayerReady();
return true;
}
bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
// This happened when doubleclicking on the startgame button.
if (server.m_State != SERVER_STATE_PREGAME)
return true;
if (session->GetGUID() == server.m_HostGUID)
{
CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
server.UpdateGameAttributes(&(message->m_Data));
}
return true;
}
bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_HostGUID)
{
CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef();
server.AssignPlayer(message->m_PlayerID, message->m_GUID);
}
return true;
}
bool CNetServerWorker::OnStartGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_START);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_HostGUID)
server.StartGame();
return true;
}
bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* loadedSession = (CNetServerSession*)context;
CNetServerWorker& server = loadedSession->GetServer();
loadedSession->SetLongTimeout(false);
// We're in the loading state, so wait until every client has loaded
// before starting the game
ENSURE(server.m_State == SERVER_STATE_LOADING);
if (server.CheckGameLoadStatus(loadedSession))
return true;
CClientsLoadingMessage message;
// We always send all GUIDs of clients in the loading state
// so that we don't have to bother about switching GUI pages
for (CNetServerSession* session : server.m_Sessions)
if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID())
{
CClientsLoadingMessage::S_m_Clients client;
client.m_GUID = session->GetGUID();
message.m_Clients.push_back(client);
}
// Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
loadedSession->SendMessage(&message);
server.Broadcast(&message, { NSS_INGAME });
return true;
}
bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event)
{
// A client rejoining an in-progress game has now finished loading the
// map and deserialized the initial state.
// The simulation may have progressed since then, so send any subsequent
// commands to them and set them as an active player so they can participate
// in all future turns.
//
// (TODO: if it takes a long time for them to receive and execute all these
// commands, the other players will get frozen for that time and may be unhappy;
// we could try repeating this process a few times until the client converges
// on the up-to-date state, before setting them as active.)
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
u32 turn = message->m_CurrentTurn;
u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
// Send them all commands received since their saved state,
// and turn-ended messages for any turns that have already been processed
for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
{
if (i < server.m_SavedCommands.size())
for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
session->SendMessage(&server.m_SavedCommands[i][j]);
if (i <= readyTurn)
{
CEndCommandBatchMessage endMessage;
endMessage.m_Turn = i;
endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i);
session->SendMessage(&endMessage);
}
}
// Tell the turn manager to expect commands from this new client
server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn);
// Tell the client that everything has finished loading and it should start now
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = readyTurn;
session->SendMessage(&loaded);
return true;
}
bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event)
{
// A client has finished rejoining and the loading screen disappeared.
ENSURE(event->GetType() == (uint)NMT_REJOINED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Inform everyone of the client having rejoined
CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_INGAME });
// Send all pausing players to the rejoined client.
for (const CStr& guid : server.m_PausingPlayers)
{
CClientPausedMessage pausedMessage;
pausedMessage.m_GUID = guid;
pausedMessage.m_Pause = true;
session->SendMessage(&pausedMessage);
}
session->SetLongTimeout(false);
return true;
}
bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_KICKED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_HostGUID)
{
CKickedMessage* message = (CKickedMessage*)event->GetParamRef();
server.KickPlayer(message->m_Name, message->m_Ban);
}
return true;
}
bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
server.OnUserLeave(session);
return true;
}
bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
// Update the list of pausing players.
std::vector::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID());
if (message->m_Pause)
{
if (player != server.m_PausingPlayers.end())
return true;
server.m_PausingPlayers.push_back(session->GetGUID());
}
else
{
if (player == server.m_PausingPlayers.end())
return true;
server.m_PausingPlayers.erase(player);
}
// Send messages to clients that are in game, and are not the client who paused.
for (CNetServerSession* session : server.m_Sessions)
{
if (session->GetCurrState() == NSS_INGAME && message->m_GUID != session->GetGUID())
session->SendMessage(message);
}
return true;
}
bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
{
for (const CNetServerSession* session : m_Sessions)
if (session != changedSession && session->GetCurrState() != NSS_INGAME)
return false;
// Inform clients that everyone has loaded the map and that the game can start
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = 0;
// Notice the changedSession is still in the NSS_PREGAME state
Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME });
m_State = SERVER_STATE_INGAME;
return true;
}
void CNetServerWorker::StartGame()
{
for (std::pair& player : m_PlayerAssignments)
if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0)
{
LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str());
return;
}
m_ServerTurnManager = new CNetServerTurnManager(*this);
for (CNetServerSession* session : m_Sessions)
{
m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers
session->SetLongTimeout(true);
}
m_State = SERVER_STATE_LOADING;
// Send the final setup state to all clients
UpdateGameAttributes(&m_GameAttributes);
// Remove players and observers that are not present when the game starts
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();)
if (it->second.m_Enabled)
++it;
else
it = m_PlayerAssignments.erase(it);
SendPlayerAssignments();
CGameStartMessage gameStart;
Broadcast(&gameStart, { NSS_PREGAME });
}
void CNetServerWorker::UpdateGameAttributes(JS::MutableHandleValue attrs)
{
m_GameAttributes = attrs;
if (!m_Host)
return;
CGameSetupMessage gameSetupMessage(GetScriptInterface());
gameSetupMessage.m_Data = m_GameAttributes;
Broadcast(&gameSetupMessage, { NSS_PREGAME });
}
CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
{
const size_t MAX_LENGTH = 32;
CStrW name = original;
name.Replace(L"[", L"{"); // remove GUI tags
name.Replace(L"]", L"}"); // remove for symmetry
// Restrict the length
if (name.length() > MAX_LENGTH)
name = name.Left(MAX_LENGTH);
// Don't allow surrounding whitespace
name.Trim(PS_TRIM_BOTH);
// Don't allow empty name
if (name.empty())
name = L"Anonymous";
return name;
}
CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
{
CStrW name = original;
// Try names "Foo", "Foo (2)", "Foo (3)", etc
size_t id = 2;
while (true)
{
bool unique = true;
for (const CNetServerSession* session : m_Sessions)
{
if (session->GetUserName() == name)
{
unique = false;
break;
}
}
if (unique)
return name;
name = original + L" (" + CStrW::FromUInt(id++) + L")";
}
}
void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
{
if (m_Host)
StunClient::SendHolePunchingMessages(*m_Host, ipStr, port);
}
CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) :
m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers)),
m_LobbyAuth(useLobbyAuth)
{
}
CNetServer::~CNetServer()
{
delete m_Worker;
}
bool CNetServer::UseLobbyAuth() const
{
return m_LobbyAuth;
}
bool CNetServer::SetupConnection(const u16 port)
{
return m_Worker->SetupConnection(port);
}
void CNetServer::StartGame()
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_StartGameQueue.push_back(true);
}
void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs, const 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, false);
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_GameAttributesQueue.push_back(attrsJSON);
}
void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token));
}
void CNetServer::SetTurnLength(u32 msecs)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_TurnLengthQueue.push_back(msecs);
}
void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port)
{
m_Worker->SendHolePunchingMessage(ip, port);
}
Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp
===================================================================
--- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 24170)
+++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 24171)
@@ -1,1642 +1,1642 @@
/* Copyright (C) 2020 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 "lib/app_hooks.h"
#include "lib/config2.h"
#include "lib/input.h"
#include "lib/ogl.h"
#include "lib/timer.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/file/common/file_stats.h"
#include "lib/res/h_mgr.h"
#include "lib/res/graphics/cursor.h"
#include "graphics/CinemaManager.h"
#include "graphics/FontMetrics.h"
#include "graphics/GameView.h"
#include "graphics/LightEnv.h"
#include "graphics/MapReader.h"
#include "graphics/MaterialManager.h"
#include "graphics/TerrainTextureManager.h"
#include "gui/CGUI.h"
#include "gui/GUIManager.h"
#include "i18n/L10n.h"
#include "maths/MathUtil.h"
#include "network/NetServer.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "network/NetMessages.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/GameSetup/GameSetup.h"
#include "ps/GameSetup/Paths.h"
#include "ps/GameSetup/Config.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "ps/GameSetup/HWDetect.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Joystick.h"
#include "ps/Loader.h"
#include "ps/Mod.h"
#include "ps/ModIo.h"
#include "ps/Profile.h"
#include "ps/ProfileViewer.h"
#include "ps/Profiler2.h"
#include "ps/Pyrogenesis.h" // psSetLogDir
#include "ps/scripting/JSInterface_Console.h"
#include "ps/TouchInput.h"
#include "ps/UserReport.h"
#include "ps/Util.h"
#include "ps/VideoMode.h"
#include "ps/VisualReplay.h"
#include "ps/World.h"
#include "renderer/Renderer.h"
#include "renderer/VertexBufferManager.h"
#include "renderer/ModelRenderer.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptStats.h"
#include "scriptinterface/ScriptConversions.h"
#include "scriptinterface/ScriptRuntime.h"
#include "simulation2/Simulation2.h"
#include "lobby/IXmppClient.h"
#include "soundmanager/scripting/JSInterface_Sound.h"
#include "soundmanager/ISoundManager.h"
#include "tools/atlas/GameInterface/GameLoop.h"
#include "tools/atlas/GameInterface/View.h"
#if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets
#define MUST_INIT_X11 1
#include
#else
#define MUST_INIT_X11 0
#endif
extern void RestartEngine();
#include
#include
#include
ERROR_GROUP(System);
ERROR_TYPE(System, SDLInitFailed);
ERROR_TYPE(System, VmodeFailed);
ERROR_TYPE(System, RequiredExtensionsMissing);
bool g_DoRenderGui = true;
bool g_DoRenderLogger = true;
bool g_DoRenderCursor = true;
-shared_ptr g_ScriptRuntime;
+thread_local shared_ptr g_ScriptRuntime;
static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code
static const CStr g_EventNameGameLoadProgress = "GameLoadProgress";
bool g_InDevelopmentCopy;
bool g_CheckedIfInDevelopmentCopy = false;
static void SetTextureQuality(int quality)
{
int q_flags;
GLint filter;
retry:
// keep this in sync with SANE_TEX_QUALITY_DEFAULT
switch(quality)
{
// worst quality
case 0:
q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP;
filter = GL_NEAREST;
break;
// [perf] add bilinear filtering
case 1:
q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP;
filter = GL_LINEAR;
break;
// [vmem] no longer reduce resolution
case 2:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_LINEAR;
break;
// [vmem] add mipmaps
case 3:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_NEAREST_MIPMAP_LINEAR;
break;
// [perf] better filtering
case 4:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// [vmem] no longer reduce bpp
case SANE_TEX_QUALITY_DEFAULT:
q_flags = OGL_TEX_FULL_QUALITY;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// [perf] add anisotropy
case 6:
// TODO: add anisotropic filtering
q_flags = OGL_TEX_FULL_QUALITY;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// invalid
default:
debug_warn(L"SetTextureQuality: invalid quality");
quality = SANE_TEX_QUALITY_DEFAULT;
// careful: recursion doesn't work and we don't want to duplicate
// the "sane" default values.
goto retry;
}
ogl_tex_set_defaults(q_flags, filter);
}
//----------------------------------------------------------------------------
// GUI integration
//----------------------------------------------------------------------------
// display progress / description in loading screen
void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task)
{
const ScriptInterface& scriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface());
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::AutoValueVector paramData(cx);
paramData.append(JS::NumberValue(percent));
JS::RootedValue valPendingTask(cx);
scriptInterface.ToJSVal(cx, &valPendingTask, pending_task);
paramData.append(valPendingTask);
g_GUI->SendEventToAll(g_EventNameGameLoadProgress, paramData);
}
bool ShouldRender()
{
return !g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen());
}
void Render()
{
// Do not render if not focused while in fullscreen or minimised,
// as that triggers a difficult-to-reproduce crash on some graphic cards.
if (!ShouldRender())
return;
PROFILE3("render");
ogl_WarnIfError();
g_Profiler2.RecordGPUFrameStart();
ogl_WarnIfError();
// prepare before starting the renderer frame
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->BeginFrame();
if (g_Game)
g_Renderer.SetSimulation(g_Game->GetSimulation2());
// start new frame
g_Renderer.BeginFrame();
ogl_WarnIfError();
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->Render();
ogl_WarnIfError();
g_Renderer.RenderTextOverlays();
// If we're in Atlas game view, render special tools
if (g_AtlasGameLoop && g_AtlasGameLoop->view)
{
g_AtlasGameLoop->view->DrawCinemaPathTool();
ogl_WarnIfError();
}
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->GetCinema()->Render();
ogl_WarnIfError();
if (g_DoRenderGui)
g_GUI->Draw();
ogl_WarnIfError();
// If we're in Atlas game view, render special overlays (e.g. editor bandbox)
if (g_AtlasGameLoop && g_AtlasGameLoop->view)
{
g_AtlasGameLoop->view->DrawOverlays();
ogl_WarnIfError();
}
// Text:
glDisable(GL_DEPTH_TEST);
g_Console->Render();
ogl_WarnIfError();
if (g_DoRenderLogger)
g_Logger->Render();
ogl_WarnIfError();
// Profile information
g_ProfileViewer.RenderProfile();
ogl_WarnIfError();
// Draw the cursor (or set the Windows cursor, on Windows)
if (g_DoRenderCursor)
{
PROFILE3_GPU("cursor");
CStrW cursorName = g_CursorName;
if (cursorName.empty())
{
cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y, g_GuiScale, false);
}
else
{
bool forceGL = false;
CFG_GET_VAL("nohwcursor", forceGL);
#if CONFIG2_GLES
#warning TODO: implement cursors for GLES
#else
// set up transform for GL cursor
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
CMatrix3D transform;
transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f);
glLoadMatrixf(&transform._11);
#endif
#if OS_ANDROID
#warning TODO: cursors for Android
#else
if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, g_GuiScale, forceGL) < 0)
LOGWARNING("Failed to draw cursor '%s'", utf8_from_wstring(cursorName));
#endif
#if CONFIG2_GLES
#warning TODO: implement cursors for GLES
#else
// restore transform
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
#endif
}
}
glEnable(GL_DEPTH_TEST);
g_Renderer.EndFrame();
PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls);
PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris);
PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris);
PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris);
PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris);
PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats);
PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles);
ogl_WarnIfError();
g_Profiler2.RecordGPUFrameEnd();
ogl_WarnIfError();
}
ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags))
{
// If we're fullscreen, then sometimes (at least on some particular drivers on Linux)
// displaying the error dialog hangs the desktop since the dialog box is behind the
// fullscreen window. So we just force the game to windowed mode before displaying the dialog.
// (But only if we're in the main thread, and not if we're being reentrant.)
if (ThreadUtil::IsMainThread())
{
static bool reentering = false;
if (!reentering)
{
reentering = true;
g_VideoMode.SetFullscreen(false);
reentering = false;
}
}
// We don't actually implement the error display here, so return appropriately
return ERI_NOT_IMPLEMENTED;
}
const std::vector& GetMods(const CmdLineArgs& args, int flags)
{
const bool init_mods = (flags & INIT_MODS) == INIT_MODS;
const bool add_user = !InDevelopmentCopy() && !args.Has("noUserMod");
const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC;
if (!init_mods)
{
// Add the user mod if it should be present
if (add_user && (g_modsLoaded.empty() || g_modsLoaded.back() != "user"))
g_modsLoaded.push_back("user");
return g_modsLoaded;
}
g_modsLoaded = args.GetMultiple("mod");
if (add_public)
g_modsLoaded.insert(g_modsLoaded.begin(), "public");
g_modsLoaded.insert(g_modsLoaded.begin(), "mod");
// Add the user mod if not explicitly disabled or we have a dev copy so
// that saved files end up in version control and not in the user mod.
if (add_user)
g_modsLoaded.push_back("user");
return g_modsLoaded;
}
void MountMods(const Paths& paths, const std::vector& mods)
{
OsPath modPath = paths.RData()/"mods";
OsPath modUserPath = paths.UserData()/"mods";
for (size_t i = 0; i < mods.size(); ++i)
{
size_t priority = (i+1)*2; // mods are higher priority than regular mountings, which default to priority 0
size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE|VFS_MOUNT_REPLACEABLE;
size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST;
OsPath modName(mods[i]);
if (InDevelopmentCopy())
{
// We are running a dev copy, so only mount mods in the user mod path
// if the mod does not exist in the data path.
if (DirectoryExists(modPath / modName/""))
g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority);
else
g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority);
}
else
{
g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority);
// Ensure that user modified files are loaded, if they are present
g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority+1);
}
}
}
static void InitVfs(const CmdLineArgs& args, int flags)
{
TIMER(L"InitVfs");
const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0;
const Paths paths(args);
OsPath logs(paths.Logs());
CreateDirectories(logs, 0700);
psSetLogDir(logs);
// desired location for crashlog is now known. update AppHooks ASAP
// (particularly before the following error-prone operations):
AppHooks hooks = {0};
hooks.bundle_logs = psBundleLogs;
hooks.get_log_dir = psLogDir;
if (setup_error)
hooks.display_error = psDisplayError;
app_hooks_update(&hooks);
g_VFS = CreateVfs();
const OsPath readonlyConfig = paths.RData()/"config"/"";
g_VFS->Mount(L"config/", readonlyConfig);
// Engine localization files.
g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/"");
MountMods(paths, GetMods(args, flags));
// We mount these dirs last as otherwise writing could result in files being placed in a mod's dir.
g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"");
g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH);
// Mounting with highest priority, so that a mod supplied user.cfg is harmless
g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1);
if(readonlyConfig != paths.Config())
g_VFS->Mount(L"config/", paths.Config(), 0, (size_t)-1);
g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads)
// note: don't bother with g_VFS->TextRepresentation - directories
// haven't yet been populated and are empty.
}
static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData)
{
{
// console
TIMER(L"ps_console");
g_Console->UpdateScreenSize(g_xres, g_yres);
// Calculate and store the line spacing
CFontMetrics font(CStrIntern(CONSOLE_FONT));
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 = 7;
double blinkRate = 0.5;
CFG_GET_VAL("gui.cursorblinkrate", blinkRate);
g_Console->SetCursorBlinkRate(blinkRate);
}
// hotkeys
{
TIMER(L"ps_lang_hotkeys");
LoadHotkeys();
}
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", srcScriptInterface, initData);
return;
}
// GUI uses VFS, so this must come after VFS init.
g_GUI->SwitchPage(gui_page, srcScriptInterface, initData);
}
void InitPsAutostart(bool networked, JS::HandleValue attrs)
{
// The GUI has not been initialized yet, so use the simulation scriptinterface for this variable
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue playerAssignments(cx);
ScriptInterface::CreateObject(cx, &playerAssignments);
if (!networked)
{
JS::RootedValue localPlayer(cx);
ScriptInterface::CreateObject(cx, &localPlayer, "player", g_Game->GetPlayerID());
scriptInterface.SetProperty(playerAssignments, "local", localPlayer);
}
JS::RootedValue sessionInitData(cx);
ScriptInterface::CreateObject(
cx,
&sessionInitData,
"attribs", attrs,
"playerAssignments", playerAssignments);
InitPs(true, L"page_loading.xml", &scriptInterface, sessionInitData);
}
void InitInput()
{
g_Joystick.Initialise();
// register input handlers
// This stack is constructed so the first added, will be the last
// one called. This is important, because each of the handlers
// has the potential to block events to go further down
// in the chain. I.e. the last one in the list added, is the
// only handler that can block all messages before they are
// processed.
in_add_handler(game_view_handler);
in_add_handler(CProfileViewer::InputThunk);
in_add_handler(conInputHandler);
in_add_handler(HotkeyInputHandler);
// gui_handler needs to be registered after (i.e. called before!) the
// hotkey handler so that input boxes can be typed in without
// setting off hotkeys.
in_add_handler(gui_handler);
in_add_handler(touch_input_handler);
// must be registered after (called before) the GUI which relies on these globals
in_add_handler(GlobalsInputHandler);
// Should be called first, this updates our hotkey press state
// so that js calls to HotkeyIsPressed are synched with events.
in_add_handler(HotkeyStateChange);
}
static void ShutdownPs()
{
SAFE_DELETE(g_GUI);
UnloadHotkeys();
// disable the special Windows cursor, or free textures for OGL cursors
cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, 1.0, false);
}
static void InitRenderer()
{
TIMER(L"InitRenderer");
// create renderer
new CRenderer;
g_RenderingOptions.ReadConfig();
// create terrain related stuff
new CTerrainTextureManager;
g_Renderer.Open(g_xres, g_yres);
// Setup lighting environment. Since the Renderer accesses the
// lighting environment through a pointer, this has to be done before
// the first Frame.
g_Renderer.SetLightEnv(&g_LightEnv);
// I haven't seen the camera affecting GUI rendering and such, but the
// viewport has to be updated according to the video mode
SViewPort vp;
vp.m_X = 0;
vp.m_Y = 0;
vp.m_Width = g_xres;
vp.m_Height = g_yres;
g_Renderer.SetViewport(vp);
ColorActivateFastImpl();
ModelRenderer::Init();
}
static void InitSDL()
{
#if OS_LINUX
// In fullscreen mode when SDL is compiled with DGA support, the mouse
// sensitivity often appears to be unusably wrong (typically too low).
// (This seems to be reported almost exclusively on Ubuntu, but can be
// reproduced on Gentoo after explicitly enabling DGA.)
// Disabling the DGA mouse appears to fix that problem, and doesn't
// have any obvious negative effects.
setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0);
#endif
if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0)
{
LOGERROR("SDL library initialization failed: %s", SDL_GetError());
throw PSERROR_System_SDLInitFailed();
}
atexit(SDL_Quit);
// Text input is active by default, disable it until it is actually needed.
SDL_StopTextInput();
#if OS_MACOSX
// Some Mac mice only have one button, so they can't right-click
// but SDL2 can emulate that with Ctrl+Click
bool macMouse = false;
CFG_GET_VAL("macmouse", macMouse);
SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0");
#endif
}
static void ShutdownSDL()
{
SDL_Quit();
}
void EndGame()
{
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_NetServer);
SAFE_DELETE(g_Game);
if (CRenderer::IsInitialised())
{
ISoundManager::CloseGame();
g_Renderer.ResetState();
}
}
void Shutdown(int flags)
{
const bool hasRenderer = CRenderer::IsInitialised();
if ((flags & SHUTDOWN_FROM_CONFIG))
goto from_config;
EndGame();
SAFE_DELETE(g_XmppClient);
SAFE_DELETE(g_ModIo);
ShutdownPs();
TIMER_BEGIN(L"shutdown TexMan");
delete &g_TexMan;
TIMER_END(L"shutdown TexMan");
if (hasRenderer)
{
TIMER_BEGIN(L"shutdown Renderer");
g_Renderer.~CRenderer();
g_VBMan.Shutdown();
TIMER_END(L"shutdown Renderer");
}
g_Profiler2.ShutdownGPU();
// Free cursors before shutting down SDL, as they may depend on SDL.
cursor_shutdown();
TIMER_BEGIN(L"shutdown SDL");
ShutdownSDL();
TIMER_END(L"shutdown SDL");
if (hasRenderer)
g_VideoMode.Shutdown();
TIMER_BEGIN(L"shutdown UserReporter");
g_UserReporter.Deinitialize();
TIMER_END(L"shutdown UserReporter");
// Cleanup curl now that g_ModIo and g_UserReporter have been shutdown.
curl_global_cleanup();
delete &g_L10n;
from_config:
TIMER_BEGIN(L"shutdown ConfigDB");
delete &g_ConfigDB;
TIMER_END(L"shutdown ConfigDB");
SAFE_DELETE(g_Console);
// This is needed to ensure that no callbacks from the JSAPI try to use
// the profiler when it's already destructed
g_ScriptRuntime.reset();
// resource
// first shut down all resource owners, and then the handle manager.
TIMER_BEGIN(L"resource modules");
ISoundManager::SetEnabled(false);
g_VFS.reset();
// this forcibly frees all open handles (thus preventing real leaks),
// and makes further access to h_mgr impossible.
h_mgr_shutdown();
file_stats_dump();
TIMER_END(L"resource modules");
TIMER_BEGIN(L"shutdown misc");
timer_DisplayClientTotals();
CNetHost::Deinitialize();
// should be last, since the above use them
SAFE_DELETE(g_Logger);
delete &g_Profiler;
delete &g_ProfileViewer;
SAFE_DELETE(g_ScriptStatsTable);
TIMER_END(L"shutdown misc");
}
#if OS_UNIX
static void FixLocales()
{
#if OS_MACOSX || OS_BSD
// OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle
// wide characters. Peculiarly the string "UTF-8" seems to be acceptable
// despite not being a real locale, and it's conveniently language-agnostic,
// so use that.
setlocale(LC_CTYPE, "UTF-8");
#endif
// On misconfigured systems with incorrect locale settings, we'll die
// with a C++ exception when some code (e.g. Boost) tries to use locales.
// To avoid death, we'll detect the problem here and warn the user and
// reset to the default C locale.
// For informing the user of the problem, use the list of env vars that
// glibc setlocale looks at. (LC_ALL is checked first, and LANG last.)
const char* const LocaleEnvVars[] = {
"LC_ALL",
"LC_COLLATE",
"LC_CTYPE",
"LC_MONETARY",
"LC_NUMERIC",
"LC_TIME",
"LC_MESSAGES",
"LANG"
};
try
{
// this constructor is similar to setlocale(LC_ALL, ""),
// but instead of returning NULL, it throws runtime_error
// when the first locale env variable found contains an invalid value
std::locale("");
}
catch (std::runtime_error&)
{
LOGWARNING("Invalid locale settings");
for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++)
{
if (char* envval = getenv(LocaleEnvVars[i]))
LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval);
else
LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]);
}
// We should set LC_ALL since it overrides LANG
if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1))
debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable.");
else
LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL"));
}
}
#else
static void FixLocales()
{
// Do nothing on Windows
}
#endif
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("TIMER");
timer_LatchStartTime();
// initialise profiler early so it can profile startup,
// but only after LatchStartTime
g_Profiler2.Initialise();
FixLocales();
// Because we do GL calls from a secondary thread, Xlib needs to
// be told to support multiple threads safely.
// This is needed for Atlas, but we have to call it before any other
// Xlib functions (e.g. the ones used when drawing the main menu
// before launching Atlas)
#if MUST_INIT_X11
int status = XInitThreads();
if (status == 0)
debug_printf("Error enabling thread-safety via XInitThreads\n");
#endif
// Initialise the low-quality rand function
srand(time(NULL)); // NOTE: this rand should *not* be used for simulation!
}
bool Autostart(const CmdLineArgs& args);
/**
* Returns true if the user has intended to start a visual replay from command line.
*/
bool AutostartVisualReplay(const std::string& replayFile);
bool Init(const CmdLineArgs& args, int flags)
{
h_mgr_init();
// Do this as soon as possible, because it chdirs
// and will mess up the error reporting if anything
// crashes before the working directory is set.
InitVfs(args, flags);
// This must come after VFS init, which sets the current directory
// (required for finding our output log files).
g_Logger = new CLogger;
new CProfileViewer;
new CProfileManager; // before any script code
g_ScriptStatsTable = new CScriptStatsTable;
g_ProfileViewer.AddRootTable(g_ScriptStatsTable);
// Set up the console early, so that debugging
// messages can be logged to it. (The console's size
// and fonts are set later in InitPs())
g_Console = new CConsole();
// g_ConfigDB, command line args, globals
CONFIG_Init(args);
// Using a global object for the runtime is a workaround until Simulation and AI use
// their own threads and also their own runtimes.
const int runtimeSize = 384 * 1024 * 1024;
const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024;
- g_ScriptRuntime = ScriptInterface::CreateRuntime(shared_ptr(), runtimeSize, heapGrowthBytesGCTrigger);
+ g_ScriptRuntime = ScriptRuntime::CreateRuntime(runtimeSize, heapGrowthBytesGCTrigger);
Mod::CacheEnabledModVersions(g_ScriptRuntime);
// Special command-line mode to dump the entity schemas instead of running the game.
// (This must be done after loading VFS etc, but should be done before wasting time
// on anything else.)
if (args.Has("dumpSchema"))
{
CSimulation2 sim(NULL, g_ScriptRuntime, NULL);
sim.LoadDefaultScripts();
std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc);
f << sim.GenerateSchema();
std::cout << "Generated entity.rng\n";
exit(0);
}
CNetHost::Initialize();
#if CONFIG2_AUDIO
if (!args.Has("autostart-nonvisual"))
ISoundManager::CreateSoundManager();
#endif
// Check if there are mods specified on the command line,
// or if we already set the mods (~INIT_MODS),
// else check if there are mods that should be loaded specified
// in the config and load those (by aborting init and restarting
// the engine).
if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS)
{
CStr modstring;
CFG_GET_VAL("mod.enabledmods", modstring);
if (!modstring.empty())
{
std::vector mods;
boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on);
std::swap(g_modsLoaded, mods);
// Abort init and restart
RestartEngine();
return false;
}
}
new L10n;
// Optionally start profiler HTTP output automatically
// (By default it's only enabled by a hotkey, for security/performance)
bool profilerHTTPEnable = false;
CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable);
if (profilerHTTPEnable)
g_Profiler2.EnableHTTP();
// Initialise everything except Win32 sockets (because our networking
// system already inits those)
curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32);
if (!g_Quickstart)
g_UserReporter.Initialize(); // after config
PROFILE2_EVENT("Init finished");
return true;
}
void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods)
{
const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0;
if(setup_vmode)
{
InitSDL();
if (!g_VideoMode.InitSDL())
throw PSERROR_System_VmodeFailed(); // abort startup
}
RunHardwareDetection();
const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file
SetTextureQuality(quality);
ogl_WarnIfError();
// Optionally start profiler GPU timings automatically
// (By default it's only enabled by a hotkey, for performance/compatibility)
bool profilerGPUEnable = false;
CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable);
if (profilerGPUEnable)
g_Profiler2.EnableGPU();
if(!g_Quickstart)
{
WriteSystemInfo();
// note: no longer vfs_display here. it's dog-slow due to unbuffered
// file output and very rarely needed.
}
if(g_DisableAudio)
ISoundManager::SetEnabled(false);
g_GUI = new CGUIManager();
// (must come after SetVideoMode, since it calls ogl_Init)
CStr8 renderPath = "default";
CFG_GET_VAL("renderpath", renderPath);
if ((ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB
&& ogl_HaveExtensions(0, "GL_ARB_vertex_shader", "GL_ARB_fragment_shader", NULL) != 0) // GLSL
|| RenderPathEnum::FromString(renderPath) == FIXED)
{
// It doesn't make sense to continue working here, because we're not
// able to display anything.
DEBUG_DISPLAY_FATAL_ERROR(
L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders."
L" The game does not support pre-shader graphics cards."
L" You are advised to try installing newer drivers and/or upgrade your graphics card."
L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734"
);
}
const char* missing = ogl_HaveExtensions(0,
"GL_ARB_multitexture",
"GL_EXT_draw_range_elements",
"GL_ARB_texture_env_combine",
"GL_ARB_texture_env_dot3",
NULL);
if(missing)
{
wchar_t buf[500];
swprintf_s(buf, ARRAY_SIZE(buf),
L"The %hs extension doesn't appear to be available on your computer."
L" The game may still work, though - you are welcome to try at your own risk."
L" If not or it doesn't look right, upgrade your graphics card.",
missing
);
DEBUG_DISPLAY_ERROR(buf);
// TODO: i18n
}
if (!ogl_HaveExtension("GL_ARB_texture_env_crossbar"))
{
DEBUG_DISPLAY_ERROR(
L"The GL_ARB_texture_env_crossbar extension doesn't appear to be available on your computer."
L" Shadows are not available and overall graphics quality might suffer."
L" You are advised to try installing newer drivers and/or upgrade your graphics card.");
g_ConfigDB.SetValueBool(CFG_HWDETECT, "shadows", false);
}
ogl_WarnIfError();
InitRenderer();
InitInput();
ogl_WarnIfError();
// TODO: Is this the best place for this?
if (VfsDirectoryExists(L"maps/"))
CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng");
try
{
if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args))
{
const bool setup_gui = ((flags & INIT_NO_GUI) == 0);
// We only want to display the splash screen at startup
shared_ptr scriptInterface = g_GUI->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue data(cx);
if (g_GUI)
{
ScriptInterface::CreateObject(cx, &data, "isStartup", true);
if (!installedMods.empty())
scriptInterface->SetProperty(data, "installedMods", installedMods);
}
InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data);
}
}
catch (PSERROR_Game_World_MapLoadFailed& e)
{
// Map Loading failed
// Start the engine so we have a GUI
InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue);
// Call script function to do the actual work
// (delete game data, switch GUI page, show error, etc.)
CancelLoad(CStr(e.what()).FromUTF8());
}
}
void InitNonVisual(const CmdLineArgs& args)
{
// Need some stuff for terrain movement costs:
// (TODO: this ought to be independent of any graphics code)
new CTerrainTextureManager;
g_TexMan.LoadTerrainTextures();
Autostart(args);
}
void RenderGui(bool RenderingState)
{
g_DoRenderGui = RenderingState;
}
void RenderLogger(bool RenderingState)
{
g_DoRenderLogger = RenderingState;
}
void RenderCursor(bool RenderingState)
{
g_DoRenderCursor = RenderingState;
}
/**
* Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON
* data from it.
* The scenario map format is used for scenario and skirmish map types (random
* games do not use a "map" (format) but a small JavaScript program which
* creates a map on the fly). It contains a section to initialize the game
* setup screen.
* @param mapPath Absolute path (from VFS root) to the map file to peek in.
* @return ScriptSettings in JSON format extracted from the map.
*/
CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath)
{
CXeromyces mapFile;
const char *pathToSettings[] =
{
"Scenario", "ScriptSettings", "" // Path to JSON data in map
};
Status loadResult = mapFile.Load(g_VFS, mapPath);
if (INFO::OK != loadResult)
{
LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8());
throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos.");
}
XMBElement mapElement = mapFile.GetRoot();
// Select the ScriptSettings node in the map file...
for (int i = 0; pathToSettings[i][0]; ++i)
{
int childId = mapFile.GetElementID(pathToSettings[i]);
XMBElementList nodes = mapElement.GetChildNodes();
auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) {
return child.GetNodeName() == childId;
});
if (it != nodes.end())
mapElement = *it;
}
// ... they contain a JSON document to initialize the game setup
// screen
return mapElement.GetText();
}
/*
* Command line options for autostart
* (keep synchronized with binaries/system/readme.txt):
*
* -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME;
* TYPEDIR is skirmishes, scenarios, or random
* -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random)
* -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra)
* -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI
* (0: sandbox, 5: very hard)
* -autostart-aiseed=AISEED sets the seed used for the AI random
* generator (default 0, use -1 for random)
* -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer)
* -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV
* (skirmish and random maps only)
* -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2).
* -autostart-ceasefire=NUM sets a ceasefire duration NUM
* (default 0 minutes)
* -autostart-nonvisual disable any graphics and sounds
* -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME
* located in simulation/data/settings/victory_conditions/
* (default conquest). When the first given SCRIPTNAME is
* "endless", no victory conditions will apply.
* -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition
* (default 10 minutes)
* -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition
* (default 10 minutes)
* -autostart-reliccount=NUM sets the number of relics for relic victory condition
* (default 2 relics)
* -autostart-disable-replay disable saving of replays
*
* Multiplayer:
* -autostart-playername=NAME sets local player NAME (default 'anonymous')
* -autostart-host sets multiplayer host mode
* -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer
* game (default 2)
* -autostart-client=IP sets multiplayer client to join host at
* given IP address
* Random maps only:
* -autostart-size=TILES sets random map size in TILES (default 192)
* -autostart-players=NUMBER sets NUMBER of players on random map
* (default 2)
*
* Examples:
* 1) "Bob" will host a 2 player game on the Arcadia map:
* -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob"
* "Alice" joins the match as player 2:
* -autostart="scenarios/Arcadia" -autostart-client=127.0.0.1 -autostart-playername="Alice"
* The players use the developer overlay to control players.
*
* 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot:
* -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra
*
* 3) Observe the PetraBot on a triggerscript map:
* -autostart="random/jebel_barkal" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=1:petra -autostart-ai=2:petra -autostart-player=-1
*/
bool Autostart(const CmdLineArgs& args)
{
CStr autoStartName = args.Get("autostart");
if (autoStartName.empty())
return false;
g_Game = new CGame(!args.Has("autostart-disable-replay"));
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue attrs(cx);
JS::RootedValue settings(cx);
JS::RootedValue playerData(cx);
ScriptInterface::CreateObject(cx, &attrs);
ScriptInterface::CreateObject(cx, &settings);
ScriptInterface::CreateArray(cx, &playerData);
// The directory in front of the actual map name indicates which type
// of map is being loaded. Drawback of this approach is the association
// of map types and folders is hard-coded, but benefits are:
// - No need to pass the map type via command line separately
// - Prevents mixing up of scenarios and skirmish maps to some degree
Path mapPath = Path(autoStartName);
std::wstring mapDirectory = mapPath.Parent().Filename().string();
std::string mapType;
if (mapDirectory == L"random")
{
// Random map definition will be loaded from JSON file, so we need to parse it
std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json";
JS::RootedValue scriptData(cx);
scriptInterface.ReadJSONFile(scriptPath, &scriptData);
if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings))
{
// JSON loaded ok - copy script name over to game attributes
std::wstring scriptFile;
scriptInterface.GetProperty(settings, "Script", scriptFile);
scriptInterface.SetProperty(attrs, "script", scriptFile); // RMS filename
}
else
{
// Problem with JSON file
LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath));
throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details.");
}
// Get optional map size argument (default 192)
uint mapSize = 192;
if (args.Has("autostart-size"))
{
CStr size = args.Get("autostart-size");
mapSize = size.ToUInt();
}
scriptInterface.SetProperty(settings, "Size", mapSize); // Random map size (in patches)
// Get optional number of players (default 2)
size_t numPlayers = 2;
if (args.Has("autostart-players"))
{
CStr num = args.Get("autostart-players");
numPlayers = num.ToUInt();
}
// Set up player data
for (size_t i = 0; i < numPlayers; ++i)
{
JS::RootedValue player(cx);
// We could load player_defaults.json here, but that would complicate the logic
// even more and autostart is only intended for developers anyway
ScriptInterface::CreateObject(cx, &player, "Civ", "athen");
scriptInterface.SetPropertyInt(playerData, i, player);
}
mapType = "random";
}
else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// Initialize general settings from the map data so some values
// (e.g. name of map) are always present, even when autostart is
// partially configured
CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml");
scriptInterface.ParseJSON(mapSettingsJSON, &settings);
// Initialize the playerData array being modified by autostart
// with the real map data, so sensible values are present:
scriptInterface.GetProperty(settings, "PlayerData", &playerData);
if (mapDirectory == L"scenarios")
mapType = "scenario";
else
mapType = "skirmish";
}
else
{
LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory));
throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types.");
}
scriptInterface.SetProperty(attrs, "mapType", mapType);
scriptInterface.SetProperty(attrs, "map", "maps/" + autoStartName);
scriptInterface.SetProperty(settings, "mapType", mapType);
scriptInterface.SetProperty(settings, "CheatsEnabled", true);
// The seed is used for both random map generation and simulation
u32 seed = 0;
if (args.Has("autostart-seed"))
{
CStr seedArg = args.Get("autostart-seed");
if (seedArg == "-1")
seed = rand();
else
seed = seedArg.ToULong();
}
scriptInterface.SetProperty(settings, "Seed", seed);
// Set seed for AIs
u32 aiseed = 0;
if (args.Has("autostart-aiseed"))
{
CStr seedArg = args.Get("autostart-aiseed");
if (seedArg == "-1")
aiseed = rand();
else
aiseed = seedArg.ToULong();
}
scriptInterface.SetProperty(settings, "AISeed", aiseed);
// Set player data for AIs
// attrs.settings = { PlayerData: [ { AI: ... }, ... ] }
// or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set
int offset = 1;
JS::RootedValue player(cx);
if (scriptInterface.GetPropertyInt(playerData, 0, &player) && player.isNull())
offset = 0;
// Set teams
if (args.Has("autostart-team"))
{
std::vector civArgs = args.GetMultiple("autostart-team");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID);
continue;
}
ScriptInterface::CreateObject(cx, &player);
}
int teamID = civArgs[i].AfterFirst(":").ToInt() - 1;
scriptInterface.SetProperty(player, "Team", teamID);
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
int ceasefire = 0;
if (args.Has("autostart-ceasefire"))
ceasefire = args.Get("autostart-ceasefire").ToInt();
scriptInterface.SetProperty(settings, "Ceasefire", ceasefire);
if (args.Has("autostart-ai"))
{
std::vector aiArgs = args.GetMultiple("autostart-ai");
for (size_t i = 0; i < aiArgs.size(); ++i)
{
int playerID = aiArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID);
continue;
}
ScriptInterface::CreateObject(cx, &player);
}
scriptInterface.SetProperty(player, "AI", aiArgs[i].AfterFirst(":"));
scriptInterface.SetProperty(player, "AIDiff", 3);
scriptInterface.SetProperty(player, "AIBehavior", "balanced");
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
// Set AI difficulty
if (args.Has("autostart-aidiff"))
{
std::vector civArgs = args.GetMultiple("autostart-aidiff");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID);
continue;
}
ScriptInterface::CreateObject(cx, &player);
}
scriptInterface.SetProperty(player, "AIDiff", civArgs[i].AfterFirst(":").ToInt());
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
// Set player data for Civs
if (args.Has("autostart-civ"))
{
if (mapDirectory != L"scenarios")
{
std::vector civArgs = args.GetMultiple("autostart-civ");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue player(cx);
if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
{
if (mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID);
continue;
}
ScriptInterface::CreateObject(cx, &player);
}
scriptInterface.SetProperty(player, "Civ", civArgs[i].AfterFirst(":"));
scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
}
}
else
LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios");
}
// Add player data to map settings
scriptInterface.SetProperty(settings, "PlayerData", playerData);
// Add map settings to game attributes
scriptInterface.SetProperty(attrs, "settings", settings);
// Get optional playername
CStrW userName = L"anonymous";
if (args.Has("autostart-playername"))
userName = args.Get("autostart-playername").FromUTF8();
// Add additional scripts to the TriggerScripts property
std::vector triggerScriptsVector;
JS::RootedValue triggerScripts(cx);
if (scriptInterface.HasProperty(settings, "TriggerScripts"))
{
scriptInterface.GetProperty(settings, "TriggerScripts", &triggerScripts);
FromJSVal_vector(cx, triggerScripts, triggerScriptsVector);
}
if (!CRenderer::IsInitialised())
{
CStr nonVisualScript = "scripts/NonVisualTrigger.js";
triggerScriptsVector.push_back(nonVisualScript.FromUTF8());
}
std::vector victoryConditions(1, "conquest");
if (args.Has("autostart-victory"))
victoryConditions = args.GetMultiple("autostart-victory");
if (victoryConditions.size() == 1 && victoryConditions[0] == "endless")
victoryConditions.clear();
scriptInterface.SetProperty(settings, "VictoryConditions", victoryConditions);
for (const CStr& victory : victoryConditions)
{
JS::RootedValue scriptData(cx);
JS::RootedValue data(cx);
JS::RootedValue victoryScripts(cx);
CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + victory.FromUTF8() + L".json";
scriptInterface.ReadJSONFile(scriptPath, &scriptData);
if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "Data", &data) && !data.isUndefined()
&& scriptInterface.GetProperty(data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined())
{
std::vector victoryScriptsVector;
FromJSVal_vector(cx, victoryScripts, victoryScriptsVector);
triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end());
}
else
{
LOGERROR("Autostart: Error reading victory script '%s'", utf8_from_wstring(scriptPath));
throw PSERROR_Game_World_MapLoadFailed("Error reading victory script.\nCheck application log for details.");
}
}
ToJSVal_vector(cx, &triggerScripts, triggerScriptsVector);
scriptInterface.SetProperty(settings, "TriggerScripts", triggerScripts);
int wonderDuration = 10;
if (args.Has("autostart-wonderduration"))
wonderDuration = args.Get("autostart-wonderduration").ToInt();
scriptInterface.SetProperty(settings, "WonderDuration", wonderDuration);
int relicDuration = 10;
if (args.Has("autostart-relicduration"))
relicDuration = args.Get("autostart-relicduration").ToInt();
scriptInterface.SetProperty(settings, "RelicDuration", relicDuration);
int relicCount = 2;
if (args.Has("autostart-reliccount"))
relicCount = args.Get("autostart-reliccount").ToInt();
scriptInterface.SetProperty(settings, "RelicCount", relicCount);
if (args.Has("autostart-host"))
{
InitPsAutostart(true, attrs);
size_t maxPlayers = 2;
if (args.Has("autostart-host-players"))
maxPlayers = args.Get("autostart-host-players").ToUInt();
g_NetServer = new CNetServer(false, maxPlayers);
g_NetServer->UpdateGameAttributes(&attrs, scriptInterface);
bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT);
ENSURE(ok);
g_NetClient = new CNetClient(g_Game, true);
g_NetClient->SetUserName(userName);
g_NetClient->SetupConnection("127.0.0.1", PS_DEFAULT_PORT, nullptr);
}
else if (args.Has("autostart-client"))
{
InitPsAutostart(true, attrs);
g_NetClient = new CNetClient(g_Game, false);
g_NetClient->SetUserName(userName);
CStr ip = args.Get("autostart-client");
if (ip.empty())
ip = "127.0.0.1";
bool ok = g_NetClient->SetupConnection(ip, PS_DEFAULT_PORT, nullptr);
ENSURE(ok);
}
else
{
g_Game->SetPlayerID(args.Has("autostart-player") ? args.Get("autostart-player").ToInt() : 1);
g_Game->StartGame(&attrs, "");
if (CRenderer::IsInitialised())
{
InitPsAutostart(false, attrs);
}
else
{
// TODO: Non progressive load can fail - need a decent way to handle this
LDR_NonprogressiveLoad();
ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK);
}
}
return true;
}
bool AutostartVisualReplay(const std::string& replayFile)
{
if (!FileExists(OsPath(replayFile)))
return false;
g_Game = new CGame(false);
g_Game->SetPlayerID(-1);
g_Game->StartVisualReplay(replayFile);
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue attrs(cx, g_Game->GetSimulation2()->GetInitAttributes());
InitPsAutostart(false, attrs);
return true;
}
void CancelLoad(const CStrW& message)
{
shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface();
JSContext* cx = pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue global(cx, pScriptInterface->GetGlobalObject());
LDR_Cancel();
if (g_GUI &&
g_GUI->GetPageCount() &&
pScriptInterface->HasProperty(global, "cancelOnLoadGameError"))
pScriptInterface->CallFunctionVoid(global, "cancelOnLoadGameError", message);
}
bool InDevelopmentCopy()
{
if (!g_CheckedIfInDevelopmentCopy)
{
g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK);
g_CheckedIfInDevelopmentCopy = true;
}
return g_InDevelopmentCopy;
}
Index: ps/trunk/source/ps/Replay.cpp
===================================================================
--- ps/trunk/source/ps/Replay.cpp (revision 24170)
+++ ps/trunk/source/ps/Replay.cpp (revision 24171)
@@ -1,343 +1,343 @@
/* Copyright (C) 2019 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 "Replay.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/timer.h"
#include "lib/file/file_system.h"
#include "lib/res/h_mgr.h"
#include "lib/tex/tex.h"
#include "ps/Game.h"
#include "ps/CLogger.h"
#include "ps/Loader.h"
#include "ps/Mod.h"
#include "ps/Profile.h"
#include "ps/ProfileViewer.h"
#include "ps/Pyrogenesis.h"
#include "ps/Mod.h"
#include "ps/Util.h"
#include "ps/VisualReplay.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptRuntime.h"
#include "scriptinterface/ScriptStats.h"
#include "simulation2/components/ICmpGuiInterface.h"
#include "simulation2/helpers/Player.h"
#include "simulation2/helpers/SimulationCommand.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/CmpPtr.h"
#include
#include
/**
* Number of turns between two saved profiler snapshots.
* Keep in sync with source/tools/replayprofile/graph.js
*/
static const int PROFILE_TURN_INTERVAL = 20;
CReplayLogger::CReplayLogger(const ScriptInterface& scriptInterface) :
m_ScriptInterface(scriptInterface), m_Stream(NULL)
{
}
CReplayLogger::~CReplayLogger()
{
delete m_Stream;
}
void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
{
JSContext* cx = m_ScriptInterface.GetContext();
JSAutoRequest rq(cx);
// Add timestamp, since the file-modification-date can change
m_ScriptInterface.SetProperty(attribs, "timestamp", (double)std::time(nullptr));
// Add engine version and currently loaded mods for sanity checks when replaying
m_ScriptInterface.SetProperty(attribs, "engine_version", engine_version);
JS::RootedValue mods(cx, Mod::GetLoadedModsWithVersions(m_ScriptInterface));
m_ScriptInterface.SetProperty(attribs, "mods", mods);
m_Directory = createDateIndexSubdirectory(VisualReplay::GetDirectoryPath());
debug_printf("Writing replay to %s\n", m_Directory.string8().c_str());
m_Stream = new std::ofstream(OsString(m_Directory / L"commands.txt").c_str(), std::ofstream::out | std::ofstream::trunc);
*m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n";
}
void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector& commands)
{
JSContext* cx = m_ScriptInterface.GetContext();
JSAutoRequest rq(cx);
*m_Stream << "turn " << n << " " << turnLength << "\n";
for (SimulationCommand& command : commands)
*m_Stream << "cmd " << command.player << " " << m_ScriptInterface.StringifyJSON(&command.data, false) << "\n";
*m_Stream << "end\n";
m_Stream->flush();
}
void CReplayLogger::Hash(const std::string& hash, bool quick)
{
if (quick)
*m_Stream << "hash-quick " << Hexify(hash) << "\n";
else
*m_Stream << "hash " << Hexify(hash) << "\n";
}
void CReplayLogger::SaveMetadata(const CSimulation2& simulation)
{
CmpPtr cmpGuiInterface(simulation, SYSTEM_ENTITY);
if (!cmpGuiInterface)
{
LOGERROR("Could not save replay metadata!");
return;
}
ScriptInterface& scriptInterface = simulation.GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue arg(cx);
JS::RootedValue metadata(cx);
cmpGuiInterface->ScriptCall(INVALID_PLAYER, L"GetReplayMetadata", arg, &metadata);
const OsPath fileName = g_Game->GetReplayLogger().GetDirectory() / L"metadata.json";
CreateDirectories(fileName.Parent(), 0700);
std::ofstream stream (OsString(fileName).c_str(), std::ofstream::out | std::ofstream::trunc);
stream << scriptInterface.StringifyJSON(&metadata, false);
stream.close();
debug_printf("Saved replay metadata to %s\n", fileName.string8().c_str());
}
OsPath CReplayLogger::GetDirectory() const
{
return m_Directory;
}
////////////////////////////////////////////////////////////////
CReplayPlayer::CReplayPlayer() :
m_Stream(NULL)
{
}
CReplayPlayer::~CReplayPlayer()
{
delete m_Stream;
}
void CReplayPlayer::Load(const OsPath& path)
{
ENSURE(!m_Stream);
m_Stream = new std::ifstream(OsString(path).c_str());
ENSURE(m_Stream->good());
}
CStr CReplayPlayer::ModListToString(const std::vector>& list) const
{
CStr text;
for (const std::vector& mod : list)
text += mod[0] + " (" + mod[1] + ")\n";
return text;
}
void CReplayPlayer::CheckReplayMods(const ScriptInterface& scriptInterface, JS::HandleValue attribs) const
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
std::vector> replayMods;
scriptInterface.GetProperty(attribs, "mods", replayMods);
std::vector> enabledMods;
JS::RootedValue enabledModsJS(cx, Mod::GetLoadedModsWithVersions(scriptInterface));
scriptInterface.FromJSVal(cx, enabledModsJS, enabledMods);
CStr warn;
if (replayMods.size() != enabledMods.size())
warn = "The number of enabled mods does not match the mods of the replay.";
else
for (size_t i = 0; i < replayMods.size(); ++i)
{
if (replayMods[i][0] != enabledMods[i][0])
{
warn = "The enabled mods don't match the mods of the replay.";
break;
}
else if (replayMods[i][1] != enabledMods[i][1])
{
warn = "The mod '" + replayMods[i][0] + "' with version '" + replayMods[i][1] + "' is required by the replay file, but version '" + enabledMods[i][1] + "' is present!";
break;
}
}
if (!warn.empty())
LOGWARNING("%s\nThe mods of the replay are:\n%s\nThese mods are enabled:\n%s", warn, ModListToString(replayMods), ModListToString(enabledMods));
}
void CReplayPlayer::Replay(const bool serializationtest, const int rejointestturn, const bool ooslog, const bool testHashFull, const bool testHashQuick)
{
ENSURE(m_Stream);
new CProfileViewer;
new CProfileManager;
g_ScriptStatsTable = new CScriptStatsTable;
g_ProfileViewer.AddRootTable(g_ScriptStatsTable);
const int runtimeSize = 384 * 1024 * 1024;
const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024;
- g_ScriptRuntime = ScriptInterface::CreateRuntime(shared_ptr(), runtimeSize, heapGrowthBytesGCTrigger);
+ g_ScriptRuntime = ScriptRuntime::CreateRuntime(runtimeSize, heapGrowthBytesGCTrigger);
Mod::CacheEnabledModVersions(g_ScriptRuntime);
g_Game = new CGame(false);
if (serializationtest)
g_Game->GetSimulation2()->EnableSerializationTest();
if (rejointestturn >= 0)
g_Game->GetSimulation2()->EnableRejoinTest(rejointestturn);
if (ooslog)
g_Game->GetSimulation2()->EnableOOSLog();
// Need some stuff for terrain movement costs:
// (TODO: this ought to be independent of any graphics code)
new CTerrainTextureManager;
g_TexMan.LoadTerrainTextures();
// Initialise h_mgr so it doesn't crash when emitting sounds
h_mgr_init();
std::vector commands;
u32 turn = 0;
u32 turnLength = 0;
{
JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
std::string type;
while ((*m_Stream >> type).good())
{
if (type == "start")
{
std::string line;
std::getline(*m_Stream, line);
JS::RootedValue attribs(cx);
ENSURE(g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &attribs));
CheckReplayMods(g_Game->GetSimulation2()->GetScriptInterface(), attribs);
g_Game->StartGame(&attribs, "");
// TODO: Non progressive load can fail - need a decent way to handle this
LDR_NonprogressiveLoad();
PSRETURN ret = g_Game->ReallyStartGame();
ENSURE(ret == PSRETURN_OK);
}
else if (type == "turn")
{
*m_Stream >> turn >> turnLength;
debug_printf("Turn %u (%u)...\n", turn, turnLength);
}
else if (type == "cmd")
{
player_id_t player;
*m_Stream >> player;
std::string line;
std::getline(*m_Stream, line);
JS::RootedValue data(cx);
g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &data);
g_Game->GetSimulation2()->GetScriptInterface().FreezeObject(data, true);
commands.emplace_back(SimulationCommand(player, cx, data));
}
else if (type == "hash" || type == "hash-quick")
{
std::string replayHash;
*m_Stream >> replayHash;
TestHash(type, replayHash, testHashFull, testHashQuick);
}
else if (type == "end")
{
{
g_Profiler2.RecordFrameStart();
PROFILE2("frame");
g_Profiler2.IncrementFrameNumber();
PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
g_Game->GetSimulation2()->Update(turnLength, commands);
commands.clear();
}
g_Profiler.Frame();
if (turn % PROFILE_TURN_INTERVAL == 0)
g_ProfileViewer.SaveToFile();
}
else
debug_printf("Unrecognised replay token %s\n", type.c_str());
}
}
SAFE_DELETE(m_Stream);
g_Profiler2.SaveToFile();
std::string hash;
bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, false);
ENSURE(ok);
debug_printf("# Final state: %s\n", Hexify(hash).c_str());
timer_DisplayClientTotals();
SAFE_DELETE(g_Game);
// Must be explicitly destructed here to avoid callbacks from the JSAPI trying to use g_Profiler2 when
// it's already destructed.
g_ScriptRuntime.reset();
// Clean up
delete &g_TexMan;
delete &g_Profiler;
delete &g_ProfileViewer;
SAFE_DELETE(g_ScriptStatsTable);
}
void CReplayPlayer::TestHash(const std::string& hashType, const std::string& replayHash, const bool testHashFull, const bool testHashQuick)
{
bool quick = (hashType == "hash-quick");
if ((quick && !testHashQuick) || (!quick && !testHashFull))
return;
std::string hash;
ENSURE(g_Game->GetSimulation2()->ComputeStateHash(hash, quick));
std::string hexHash = Hexify(hash);
if (hexHash == replayHash)
debug_printf("%s ok (%s)\n", hashType.c_str(), hexHash.c_str());
else
debug_printf("%s MISMATCH (%s != %s)\n", hashType.c_str(), hexHash.c_str(), replayHash.c_str());
}
Index: ps/trunk/source/scriptinterface/NativeWrapperDefns.h
===================================================================
--- ps/trunk/source/scriptinterface/NativeWrapperDefns.h (revision 24170)
+++ ps/trunk/source/scriptinterface/NativeWrapperDefns.h (revision 24171)
@@ -1,240 +1,239 @@
/* Copyright (C) 2020 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 .
*/
// Use the macro below to define types that will be passed by value to C++ functions.
// NOTE: References are used just to avoid superfluous copy constructor calls
// in the script wrapper code. They cannot be used as out-parameters.
// They are const T& by default to avoid confusion about this, especially
// because sometimes the function is not just exposed to scripts, but also
// called from C++ code.
template struct ScriptInterface::MaybeRef
{
typedef const T& Type;
};
#define PASS_BY_VALUE_IN_NATIVE_WRAPPER(T) \
template <> struct ScriptInterface::MaybeRef \
{ \
typedef T Type; \
}; \
PASS_BY_VALUE_IN_NATIVE_WRAPPER(JS::HandleValue)
PASS_BY_VALUE_IN_NATIVE_WRAPPER(bool)
PASS_BY_VALUE_IN_NATIVE_WRAPPER(int)
PASS_BY_VALUE_IN_NATIVE_WRAPPER(uint8_t)
PASS_BY_VALUE_IN_NATIVE_WRAPPER(uint16_t)
PASS_BY_VALUE_IN_NATIVE_WRAPPER(uint32_t)
PASS_BY_VALUE_IN_NATIVE_WRAPPER(fixed)
PASS_BY_VALUE_IN_NATIVE_WRAPPER(float)
PASS_BY_VALUE_IN_NATIVE_WRAPPER(double)
#undef PASS_BY_VALUE_IN_NATIVE_WRAPPER
// This works around a bug in Visual Studio (error C2244 if ScriptInterface:: is included in the
// type specifier of MaybeRef::Type for parameters inside the member function declaration).
// It's probably the bug described here, but I'm not quite sure (at least the example there still
// cause error C2244):
// https://connect.microsoft.com/VisualStudio/feedback/details/611863/vs2010-c-fails-with-error-c2244-gcc-4-3-4-compiles-ok
//
// TODO: When dropping support for VS 2015, check if this bug is still present in the supported
// Visual Studio versions (replace the macro definitions in NativeWrapperDecls.h with these ones,
// remove them from here and check if this causes error C2244 when compiling.
#undef NUMBERED_LIST_TAIL_MAYBE_REF
#undef NUMBERED_LIST_BALANCED_MAYBE_REF
#define NUMBERED_LIST_TAIL_MAYBE_REF(z, i, data) , typename ScriptInterface::MaybeRef::Type
#define NUMBERED_LIST_BALANCED_MAYBE_REF(z, i, data) BOOST_PP_COMMA_IF(i) typename ScriptInterface::MaybeRef::Type
// (NativeWrapperDecls.h set up a lot of the macros we use here)
// ScriptInterface_NativeWrapper::call(cx, rval, fptr, args...) will call fptr(cbdata, args...),
// and if T != void then it will store the result in rval:
// Templated on the return type so void can be handled separately
template
struct ScriptInterface_NativeWrapper
{
template
static void call(JSContext* cx, JS::MutableHandleValue rval, F fptr, Ts... params)
{
ScriptInterface::AssignOrToJSValUnrooted(cx, rval, fptr(ScriptInterface::GetScriptInterfaceAndCBData(cx), params...));
}
};
// Overloaded to ignore the return value from void functions
template <>
struct ScriptInterface_NativeWrapper
{
template
static void call(JSContext* cx, JS::MutableHandleValue UNUSED(rval), F fptr, Ts... params)
{
fptr(ScriptInterface::GetScriptInterfaceAndCBData(cx), params...);
}
};
// Same idea but for method calls:
template
struct ScriptInterface_NativeMethodWrapper
{
template
static void call(JSContext* cx, JS::MutableHandleValue rval, TC* c, F fptr, Ts... params)
{
ScriptInterface::AssignOrToJSValUnrooted(cx, rval, (c->*fptr)(params...));
}
};
template
struct ScriptInterface_NativeMethodWrapper
{
template
static void call(JSContext* UNUSED(cx), JS::MutableHandleValue UNUSED(rval), TC* c, F fptr, Ts... params)
{
(c->*fptr)(params...);
}
};
// JSFastNative-compatible function that wraps the function identified in the template argument list
#define OVERLOADS(z, i, data) \
template \
bool ScriptInterface::call(JSContext* cx, uint argc, JS::Value* vp) \
{ \
JS::CallArgs args = JS::CallArgsFromVp(argc, vp); \
JSAutoRequest rq(cx); \
BOOST_PP_REPEAT_##z (i, CONVERT_ARG, ~) \
JS::RootedValue rval(cx); \
ScriptInterface_NativeWrapper::template call(cx, &rval, fptr A0_TAIL(z,i)); \
args.rval().set(rval); \
return !ScriptInterface::IsExceptionPending(cx); \
}
BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~)
#undef OVERLOADS
// Same idea but for methods
#define OVERLOADS(z, i, data) \
template \
bool ScriptInterface::callMethod(JSContext* cx, uint argc, JS::Value* vp) \
{ \
JS::CallArgs args = JS::CallArgsFromVp(argc, vp); \
JSAutoRequest rq(cx); \
TC* c = ScriptInterface::GetPrivate(cx, args, CLS); \
if (! c) return false; \
BOOST_PP_REPEAT_##z (i, CONVERT_ARG, ~) \
JS::RootedValue rval(cx); \
ScriptInterface_NativeMethodWrapper::template call(cx, &rval, c, fptr A0_TAIL(z,i)); \
args.rval().set(rval); \
return !ScriptInterface::IsExceptionPending(cx); \
}
BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~)
#undef OVERLOADS
// const methods
#define OVERLOADS(z, i, data) \
template \
bool ScriptInterface::callMethodConst(JSContext* cx, uint argc, JS::Value* vp) \
{ \
JS::CallArgs args = JS::CallArgsFromVp(argc, vp); \
JSAutoRequest rq(cx); \
TC* c = ScriptInterface::GetPrivate(cx, args, CLS); \
if (! c) return false; \
BOOST_PP_REPEAT_##z (i, CONVERT_ARG, ~) \
JS::RootedValue rval(cx); \
ScriptInterface_NativeMethodWrapper::template call(cx, &rval, c, fptr A0_TAIL(z,i)); \
args.rval().set(rval); \
return !ScriptInterface::IsExceptionPending(cx); \
}
BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~)
#undef OVERLOADS
template
static void AssignOrToJSValHelper(JSContext* cx, JS::AutoValueVector& argv, const T& a, const Ts&... params)
{
ScriptInterface::AssignOrToJSVal(cx, argv[i], a);
AssignOrToJSValHelper(cx, argv, params...);
}
template
static void AssignOrToJSValHelper(JSContext* UNUSED(cx), JS::AutoValueVector& UNUSED(argv))
{
cassert(sizeof...(Ts) == 0);
// Nop, for terminating the template recursion.
}
template
bool ScriptInterface::CallFunction(JS::HandleValue val, const char* name, R& ret, const Ts&... params) const
{
JSContext* cx = GetContext();
JSAutoRequest rq(cx);
JS::RootedValue jsRet(cx);
JS::AutoValueVector argv(cx);
argv.resize(sizeof...(Ts));
AssignOrToJSValHelper<0>(cx, argv, params...);
- bool ok = CallFunction_(val, name, argv, &jsRet);
- if (!ok)
+ if (!CallFunction_(val, name, argv, &jsRet))
return false;
return FromJSVal(cx, jsRet, ret);
}
template
bool ScriptInterface::CallFunction(JS::HandleValue val, const char* name, JS::Rooted* ret, const Ts&... params) const
{
JSContext* cx = GetContext();
JSAutoRequest rq(cx);
JS::MutableHandle jsRet(ret);
JS::AutoValueVector argv(cx);
argv.resize(sizeof...(Ts));
AssignOrToJSValHelper<0>(cx, argv, params...);
return CallFunction_(val, name, argv, jsRet);
}
template
bool ScriptInterface::CallFunction(JS::HandleValue val, const char* name, JS::MutableHandle ret, const Ts&... params) const
{
JSContext* cx = GetContext();
JSAutoRequest rq(cx);
JS::AutoValueVector argv(cx);
argv.resize(sizeof...(Ts));
AssignOrToJSValHelper<0>(cx, argv, params...);
return CallFunction_(val, name, argv, ret);
}
// Call the named property on the given object, with void return type
template
bool ScriptInterface::CallFunctionVoid(JS::HandleValue val, const char* name, const Ts&... params) const
{
JSContext* cx = GetContext();
JSAutoRequest rq(cx);
JS::RootedValue jsRet(cx);
JS::AutoValueVector argv(cx);
argv.resize(sizeof...(Ts));
AssignOrToJSValHelper<0>(cx, argv, params...);
return CallFunction_(val, name, argv, &jsRet);
}
// Clean up our mess
#undef NUMBERED_LIST_HEAD
#undef NUMBERED_LIST_TAIL
#undef NUMBERED_LIST_TAIL_MAYBE_REF
#undef NUMBERED_LIST_BALANCED
#undef NUMBERED_LIST_BALANCED_MAYBE_REF
#undef CONVERT_ARG
#undef TYPENAME_T0_HEAD
#undef T0
#undef T0_MAYBE_REF
#undef T0_TAIL
#undef T0_TAIL_MAYBE_REF
#undef A0_TAIL
Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp
===================================================================
--- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24170)
+++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24171)
@@ -1,1121 +1,1111 @@
/* Copyright (C) 2020 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 "ScriptInterface.h"
#include "ScriptRuntime.h"
#include "ScriptStats.h"
#include "lib/debug.h"
#include "lib/utf8.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Profile.h"
#include "ps/utf16string.h"
#include
#include