Index: ps/trunk/source/network/scripting/JSInterface_Network.cpp
===================================================================
--- ps/trunk/source/network/scripting/JSInterface_Network.cpp (revision 24175)
+++ ps/trunk/source/network/scripting/JSInterface_Network.cpp (revision 24176)
@@ -1,245 +1,243 @@
-/* Copyright (C) 2019 Wildfire Games.
+/* 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 "JSInterface_Network.h"
#include "lib/external_libraries/enet.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/types.h"
#include "lobby/IXmppClient.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "network/NetServer.h"
#include "network/StunClient.h"
#include "ps/CLogger.h"
#include "ps/Game.h"
#include "scriptinterface/ScriptInterface.h"
u16 JSI_Network::GetDefaultPort(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return PS_DEFAULT_PORT;
}
bool JSI_Network::HasNetServer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_NetServer;
}
bool JSI_Network::HasNetClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_NetClient;
}
JS::Value JSI_Network::FindStunEndpoint(ScriptInterface::CxPrivate* pCxPrivate, int port)
{
return StunClient::FindStunEndpointHost(*(pCxPrivate->pScriptInterface), port);
}
void JSI_Network::StartNetworkHost(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName)
{
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
// Always use lobby authentication for lobby matches to prevent impersonation and smurfing, in particular through mods that implemented an UI for arbitrary or other players nicknames.
g_NetServer = new CNetServer(static_cast(g_XmppClient));
if (!g_NetServer->SetupConnection(serverPort))
{
pCxPrivate->pScriptInterface->ReportError("Failed to start server");
SAFE_DELETE(g_NetServer);
return;
}
g_Game = new CGame(true);
g_NetClient = new CNetClient(g_Game, true);
g_NetClient->SetUserName(playerName);
g_NetClient->SetHostingPlayerName(hostLobbyName);
if (!g_NetClient->SetupConnection("127.0.0.1", serverPort, nullptr))
{
pCxPrivate->pScriptInterface->ReportError("Failed to connect to server");
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
}
void JSI_Network::StartNetworkJoin(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& playerName, const CStr& serverAddress, u16 serverPort, bool useSTUN, const CStr& hostJID)
{
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
ENetHost* enetClient = nullptr;
if (g_XmppClient && useSTUN)
{
// Find an unused port
for (int i = 0; i < 5 && !enetClient; ++i)
{
// Ports below 1024 are privileged on unix
u16 port = 1024 + rand() % (UINT16_MAX - 1024);
ENetAddress hostAddr{ENET_HOST_ANY, port};
enetClient = enet_host_create(&hostAddr, 1, 1, 0, 0);
++hostAddr.port;
}
if (!enetClient)
{
pCxPrivate->pScriptInterface->ReportError("Could not find an unused port for the enet STUN client");
return;
}
StunClient::StunEndpoint stunEndpoint;
if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint))
{
pCxPrivate->pScriptInterface->ReportError("Could not find the STUN endpoint");
return;
}
g_XmppClient->SendStunEndpointToHost(stunEndpoint, hostJID);
SDL_Delay(1000);
}
g_Game = new CGame(true);
g_NetClient = new CNetClient(g_Game, false);
g_NetClient->SetUserName(playerName);
g_NetClient->SetHostingPlayerName(hostJID.substr(0, hostJID.find("@")));
if (g_XmppClient && useSTUN)
StunClient::SendHolePunchingMessages(*enetClient, serverAddress, serverPort);
if (!g_NetClient->SetupConnection(serverAddress, serverPort, enetClient))
{
pCxPrivate->pScriptInterface->ReportError("Failed to connect to server");
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
}
void JSI_Network::DisconnectNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
// TODO: we ought to do async reliable disconnections
SAFE_DELETE(g_NetServer);
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
CStr JSI_Network::GetPlayerGUID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (!g_NetClient)
return "local";
return g_NetClient->GetGUID();
}
JS::Value JSI_Network::PollNetworkClient(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_NetClient)
return JS::UndefinedValue();
// Convert from net client context to GUI script context
- JSContext* cxNet = g_NetClient->GetScriptInterface().GetContext();
- JSAutoRequest rqNet(cxNet);
- JS::RootedValue pollNet(cxNet);
+ ScriptInterface::Request rqNet(g_NetClient->GetScriptInterface());
+ JS::RootedValue pollNet(rqNet.cx);
g_NetClient->GuiPoll(&pollNet);
return pCxPrivate->pScriptInterface->CloneValueFromOtherContext(g_NetClient->GetScriptInterface(), pollNet);
}
void JSI_Network::SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1)
{
ENSURE(g_NetClient);
// TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere (with no obvious reason).
- JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
- JSAutoRequest rq(cx);
- JS::RootedValue attribs(cx, attribs1);
+ ScriptInterface::Request rq(pCxPrivate);
+ JS::RootedValue attribs(rq.cx, attribs1);
g_NetClient->SendGameSetupMessage(&attribs, *(pCxPrivate->pScriptInterface));
}
void JSI_Network::AssignNetworkPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int playerID, const CStr& guid)
{
ENSURE(g_NetClient);
g_NetClient->SendAssignPlayerMessage(playerID, guid);
}
void JSI_Network::KickPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& playerName, bool ban)
{
ENSURE(g_NetClient);
g_NetClient->SendKickPlayerMessage(playerName, ban);
}
void JSI_Network::SendNetworkChat(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& message)
{
ENSURE(g_NetClient);
g_NetClient->SendChatMessage(message);
}
void JSI_Network::SendNetworkReady(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int message)
{
ENSURE(g_NetClient);
g_NetClient->SendReadyMessage(message);
}
void JSI_Network::ClearAllPlayerReady (ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_NetClient);
g_NetClient->SendClearAllReadyMessage();
}
void JSI_Network::StartNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_NetClient);
g_NetClient->SendStartGameMessage();
}
void JSI_Network::SetTurnLength(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int length)
{
if (g_NetServer)
g_NetServer->SetTurnLength(length);
else
LOGERROR("Only network host can change turn length");
}
void JSI_Network::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("GetDefaultPort");
scriptInterface.RegisterFunction("HasNetServer");
scriptInterface.RegisterFunction("HasNetClient");
scriptInterface.RegisterFunction("FindStunEndpoint");
scriptInterface.RegisterFunction("StartNetworkHost");
scriptInterface.RegisterFunction("StartNetworkJoin");
scriptInterface.RegisterFunction("DisconnectNetworkGame");
scriptInterface.RegisterFunction("GetPlayerGUID");
scriptInterface.RegisterFunction("PollNetworkClient");
scriptInterface.RegisterFunction("SetNetworkGameAttributes");
scriptInterface.RegisterFunction("AssignNetworkPlayer");
scriptInterface.RegisterFunction("KickPlayer");
scriptInterface.RegisterFunction("SendNetworkChat");
scriptInterface.RegisterFunction("SendNetworkReady");
scriptInterface.RegisterFunction("ClearAllPlayerReady");
scriptInterface.RegisterFunction("StartNetworkGame");
scriptInterface.RegisterFunction("SetTurnLength");
}
Index: ps/trunk/source/network/tests/test_NetMessage.h
===================================================================
--- ps/trunk/source/network/tests/test_NetMessage.h (revision 24175)
+++ ps/trunk/source/network/tests/test_NetMessage.h (revision 24176)
@@ -1,52 +1,51 @@
-/* Copyright (C) 2019 Wildfire Games.
+/* 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 "lib/self_test.h"
#include "network/NetMessage.h"
#include "scriptinterface/ScriptInterface.h"
class TestNetMessage : public CxxTest::TestSuite
{
public:
void test_sim()
{
ScriptInterface script("Test", "Test", g_ScriptRuntime);
- JSContext* cx = script.GetContext();
- JSAutoRequest rq(cx);
+ ScriptInterface::Request rq(script);
- JS::RootedValue val(cx);
- ScriptInterface::CreateArray(cx, &val);
+ JS::RootedValue val(rq.cx);
+ ScriptInterface::CreateArray(rq, &val);
script.SetPropertyInt(val, 0, 4);
CSimulationMessage msg(script, 1, 2, 3, val);
TS_ASSERT_STR_EQUALS(msg.ToString(), "CSimulationMessage { m_Client: 1, m_Player: 2, m_Turn: 3, m_Data: [4] }");
size_t len = msg.GetSerializedLength();
u8* buf = new u8[len+1];
buf[len] = '!';
TS_ASSERT_EQUALS(msg.Serialize(buf) - (buf+len), 0);
TS_ASSERT_EQUALS(buf[len], '!');
CNetMessage* msg2 = CNetMessageFactory::CreateMessage(buf, len, script);
TS_ASSERT_STR_EQUALS(((CSimulationMessage*)msg2)->ToString(), "CSimulationMessage { m_Client: 1, m_Player: 2, m_Turn: 3, m_Data: [4] }");
delete msg2;
delete[] buf;
}
};
Index: ps/trunk/source/ps/Game.cpp
===================================================================
--- ps/trunk/source/ps/Game.cpp (revision 24175)
+++ ps/trunk/source/ps/Game.cpp (revision 24176)
@@ -1,471 +1,469 @@
/* 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 "Game.h"
#include "graphics/GameView.h"
#include "graphics/LOSTexture.h"
#include "graphics/ParticleManager.h"
#include "graphics/UnitManager.h"
#include "gui/GUIManager.h"
#include "gui/CGUI.h"
#include "lib/config2.h"
#include "lib/timer.h"
#include "network/NetClient.h"
#include "network/NetServer.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/Loader.h"
#include "ps/LoaderThunks.h"
#include "ps/Profile.h"
#include "ps/Replay.h"
#include "ps/Shapes.h"
#include "ps/World.h"
#include "ps/GameSetup/GameSetup.h"
#include "renderer/Renderer.h"
#include "renderer/TimeManager.h"
#include "renderer/WaterManager.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpPlayer.h"
#include "simulation2/components/ICmpPlayerManager.h"
#include "simulation2/system/ReplayTurnManager.h"
#include "soundmanager/ISoundManager.h"
#include "tools/atlas/GameInterface/GameLoop.h"
extern bool g_GameRestarted;
extern GameLoopState* g_AtlasGameLoop;
/**
* Globally accessible pointer to the CGame object.
**/
CGame *g_Game=NULL;
const CStr CGame::EventNameSimulationUpdate = "SimulationUpdate";
/**
* Constructor
*
**/
CGame::CGame(bool replayLog):
m_World(new CWorld(this)),
m_Simulation2(new CSimulation2(&m_World->GetUnitManager(), g_ScriptRuntime, m_World->GetTerrain())),
m_GameView(CRenderer::IsInitialised() ? new CGameView(this) : nullptr),
m_GameStarted(false),
m_Paused(false),
m_SimRate(1.0f),
m_PlayerID(-1),
m_ViewedPlayerID(-1),
m_IsSavedGame(false),
m_IsVisualReplay(false),
m_ReplayStream(NULL)
{
// TODO: should use CDummyReplayLogger unless activated by cmd-line arg, perhaps?
if (replayLog)
m_ReplayLogger = new CReplayLogger(m_Simulation2->GetScriptInterface());
else
m_ReplayLogger = new CDummyReplayLogger();
// Need to set the CObjectManager references after various objects have
// been initialised, so do it here rather than via the initialisers above.
if (m_GameView)
m_World->GetUnitManager().SetObjectManager(m_GameView->GetObjectManager());
m_TurnManager = new CLocalTurnManager(*m_Simulation2, GetReplayLogger()); // this will get replaced if we're a net server/client
m_Simulation2->LoadDefaultScripts();
}
/**
* Destructor
*
**/
CGame::~CGame()
{
// Again, the in-game call tree is going to be different to the main menu one.
if (CProfileManager::IsInitialised())
g_Profiler.StructuralReset();
if (m_ReplayLogger && m_GameStarted)
m_ReplayLogger->SaveMetadata(*m_Simulation2);
delete m_TurnManager;
delete m_GameView;
delete m_Simulation2;
delete m_World;
delete m_ReplayLogger;
delete m_ReplayStream;
}
void CGame::SetTurnManager(CTurnManager* turnManager)
{
if (m_TurnManager)
delete m_TurnManager;
m_TurnManager = turnManager;
if (m_TurnManager)
m_TurnManager->SetPlayerID(m_PlayerID);
}
int CGame::LoadVisualReplayData()
{
ENSURE(m_IsVisualReplay);
ENSURE(!m_ReplayPath.empty());
ENSURE(m_ReplayStream);
CReplayTurnManager* replayTurnMgr = static_cast(GetTurnManager());
u32 currentTurn = 0;
std::string type;
while ((*m_ReplayStream >> type).good())
{
if (type == "turn")
{
u32 turn = 0;
u32 turnLength = 0;
*m_ReplayStream >> turn >> turnLength;
ENSURE(turn == currentTurn && "You tried to replay a commands.txt file of a rejoined client. Please use the host's file.");
replayTurnMgr->StoreReplayTurnLength(currentTurn, turnLength);
}
else if (type == "cmd")
{
player_id_t player;
*m_ReplayStream >> player;
std::string line;
std::getline(*m_ReplayStream, line);
replayTurnMgr->StoreReplayCommand(currentTurn, player, line);
}
else if (type == "hash" || type == "hash-quick")
{
bool quick = (type == "hash-quick");
std::string replayHash;
*m_ReplayStream >> replayHash;
replayTurnMgr->StoreReplayHash(currentTurn, replayHash, quick);
}
else if (type == "end")
++currentTurn;
else
CancelLoad(L"Failed to load replay data (unrecognized content)");
}
SAFE_DELETE(m_ReplayStream);
m_FinalReplayTurn = currentTurn > 0 ? currentTurn - 1 : 0;
replayTurnMgr->StoreFinalReplayTurn(m_FinalReplayTurn);
return 0;
}
bool CGame::StartVisualReplay(const OsPath& replayPath)
{
debug_printf("Starting to replay %s\n", replayPath.string8().c_str());
m_IsVisualReplay = true;
SetTurnManager(new CReplayTurnManager(*m_Simulation2, GetReplayLogger()));
m_ReplayPath = replayPath;
m_ReplayStream = new std::ifstream(OsString(replayPath).c_str());
std::string type;
ENSURE((*m_ReplayStream >> type).good() && type == "start");
std::string line;
std::getline(*m_ReplayStream, line);
const ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface();
- JSContext* cx = scriptInterface.GetContext();
- JSAutoRequest rq(cx);
+ ScriptInterface::Request rq(scriptInterface);
- JS::RootedValue attribs(cx);
+ JS::RootedValue attribs(rq.cx);
scriptInterface.ParseJSON(line, &attribs);
StartGame(&attribs, "");
return true;
}
/**
* Initializes the game with the set of attributes provided.
* Makes calls to initialize the game view, world, and simulation objects.
* Calls are made to facilitate progress reporting of the initialization.
**/
void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& savedState)
{
const ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface();
- JSContext* cx = scriptInterface.GetContext();
- JSAutoRequest rq(cx);
+ ScriptInterface::Request rq(scriptInterface);
m_InitialSavedState = savedState;
m_IsSavedGame = !savedState.empty();
m_Simulation2->SetInitAttributes(attribs);
std::string mapType;
scriptInterface.GetProperty(attribs, "mapType", mapType);
float speed;
if (scriptInterface.HasProperty(attribs, "gameSpeed") && scriptInterface.GetProperty(attribs, "gameSpeed", speed))
SetSimRate(speed);
LDR_BeginRegistering();
RegMemFun(m_Simulation2, &CSimulation2::ProgressiveLoad, L"Simulation init", 1000);
// RC, 040804 - GameView needs to be initialized before World, otherwise GameView initialization
// overwrites anything stored in the map file that gets loaded by CWorld::Initialize with default
// values. At the minute, it's just lighting settings, but could be extended to store camera position.
// Storing lighting settings in the game view seems a little odd, but it's no big deal; maybe move it at
// some point to be stored in the world object?
if (m_GameView)
m_GameView->RegisterInit();
if (mapType == "random")
{
// Load random map attributes
std::wstring scriptFile;
- JS::RootedValue settings(cx);
+ JS::RootedValue settings(rq.cx);
scriptInterface.GetProperty(attribs, "script", scriptFile);
scriptInterface.GetProperty(attribs, "settings", &settings);
m_World->RegisterInitRMS(scriptFile, scriptInterface.GetJSRuntime(), settings, m_PlayerID);
}
else
{
std::wstring mapFile;
- JS::RootedValue settings(cx);
+ JS::RootedValue settings(rq.cx);
scriptInterface.GetProperty(attribs, "map", mapFile);
scriptInterface.GetProperty(attribs, "settings", &settings);
m_World->RegisterInit(mapFile, scriptInterface.GetJSRuntime(), settings, m_PlayerID);
}
if (m_GameView)
RegMemFun(g_Renderer.GetSingletonPtr()->GetWaterManager(), &WaterManager::LoadWaterTextures, L"LoadWaterTextures", 80);
if (m_IsSavedGame)
RegMemFun(this, &CGame::LoadInitialState, L"Loading game", 1000);
if (m_IsVisualReplay)
RegMemFun(this, &CGame::LoadVisualReplayData, L"Loading visual replay data", 1000);
LDR_EndRegistering();
}
int CGame::LoadInitialState()
{
ENSURE(m_IsSavedGame);
ENSURE(!m_InitialSavedState.empty());
std::string state;
m_InitialSavedState.swap(state); // deletes the original to save a bit of memory
std::stringstream stream(state);
bool ok = m_Simulation2->DeserializeState(stream);
if (!ok)
{
CancelLoad(L"Failed to load saved game state. It might have been\nsaved with an incompatible version of the game.");
return 0;
}
return 0;
}
/**
* Game initialization has been completed. Set game started flag and start the session.
*
* @return PSRETURN 0
**/
PSRETURN CGame::ReallyStartGame()
{
// Call the script function InitGame only for new games, not saved games
if (!m_IsSavedGame)
{
// Perform some simulation initializations (replace skirmish entities, explore territories, etc.)
// that needs to be done before setting up the AI and shouldn't be done in Atlas
if (!g_AtlasGameLoop->running)
m_Simulation2->PreInitGame();
m_Simulation2->InitGame();
}
// We need to do an initial Interpolate call to set up all the models etc,
// because Update might never interpolate (e.g. if the game starts paused)
// and we could end up rendering before having set up any models (so they'd
// all be invisible)
Interpolate(0, 0);
m_GameStarted=true;
// Render a frame to begin loading assets
if (CRenderer::IsInitialised())
Render();
if (g_NetClient)
g_NetClient->LoadFinished();
// Call the reallyStartGame GUI function, but only if it exists
if (g_GUI && g_GUI->GetPageCount())
{
shared_ptr scriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface();
- JSContext* cx = scriptInterface->GetContext();
- JSAutoRequest rq(cx);
- JS::RootedValue global(cx, scriptInterface->GetGlobalObject());
+ ScriptInterface::Request rq(scriptInterface);
+
+ JS::RootedValue global(rq.cx, scriptInterface->GetGlobalObject());
if (scriptInterface->HasProperty(global, "reallyStartGame"))
scriptInterface->CallFunctionVoid(global, "reallyStartGame");
}
debug_printf("GAME STARTED, ALL INIT COMPLETE\n");
// The call tree we've built for pregame probably isn't useful in-game.
if (CProfileManager::IsInitialised())
g_Profiler.StructuralReset();
g_GameRestarted = true;
return 0;
}
int CGame::GetPlayerID()
{
return m_PlayerID;
}
void CGame::SetPlayerID(player_id_t playerID)
{
m_PlayerID = playerID;
m_ViewedPlayerID = playerID;
if (m_TurnManager)
m_TurnManager->SetPlayerID(m_PlayerID);
}
int CGame::GetViewedPlayerID()
{
return m_ViewedPlayerID;
}
void CGame::SetViewedPlayerID(player_id_t playerID)
{
m_ViewedPlayerID = playerID;
}
void CGame::StartGame(JS::MutableHandleValue attribs, const std::string& savedState)
{
if (m_ReplayLogger)
m_ReplayLogger->StartGame(attribs);
RegisterInit(attribs, savedState);
}
// TODO: doInterpolate is optional because Atlas interpolates explicitly,
// so that it has more control over the update rate. The game might want to
// do the same, and then doInterpolate should be redundant and removed.
void CGame::Update(const double deltaRealTime, bool doInterpolate)
{
if (m_Paused || !m_TurnManager)
return;
const double deltaSimTime = deltaRealTime * m_SimRate;
if (deltaSimTime)
{
// To avoid confusing the profiler, we need to trigger the new turn
// while we're not nested inside any PROFILE blocks
if (m_TurnManager->WillUpdate(deltaSimTime))
g_Profiler.Turn();
// At the normal sim rate, we currently want to render at least one
// frame per simulation turn, so let maxTurns be 1. But for fast-forward
// sim rates we want to allow more, so it's not bounded by framerate,
// so just use the sim rate itself as the number of turns per frame.
size_t maxTurns = (size_t)m_SimRate;
if (m_TurnManager->Update(deltaSimTime, maxTurns))
{
{
PROFILE3("gui sim update");
g_GUI->SendEventToAll(EventNameSimulationUpdate);
}
GetView()->GetLOSTexture().MakeDirty();
}
if (CRenderer::IsInitialised())
g_Renderer.GetTimeManager().Update(deltaSimTime);
}
if (doInterpolate)
m_TurnManager->Interpolate(deltaSimTime, deltaRealTime);
}
void CGame::Interpolate(float simFrameLength, float realFrameLength)
{
if (!m_TurnManager)
return;
m_TurnManager->Interpolate(simFrameLength, realFrameLength);
}
static CColor BrokenColor(0.3f, 0.3f, 0.3f, 1.0f);
void CGame::CachePlayerColors()
{
m_PlayerColors.clear();
CmpPtr cmpPlayerManager(*m_Simulation2, SYSTEM_ENTITY);
if (!cmpPlayerManager)
return;
int numPlayers = cmpPlayerManager->GetNumPlayers();
m_PlayerColors.resize(numPlayers);
for (int i = 0; i < numPlayers; ++i)
{
CmpPtr cmpPlayer(*m_Simulation2, cmpPlayerManager->GetPlayerByID(i));
if (!cmpPlayer)
m_PlayerColors[i] = BrokenColor;
else
m_PlayerColors[i] = cmpPlayer->GetDisplayedColor();
}
}
CColor CGame::GetPlayerColor(player_id_t player) const
{
if (player < 0 || player >= (int)m_PlayerColors.size())
return BrokenColor;
return m_PlayerColors[player];
}
bool CGame::IsGameFinished() const
{
for (const std::pair& p : m_Simulation2->GetEntitiesWithInterface(IID_Player))
{
CmpPtr cmpPlayer(*m_Simulation2, p.first);
if (cmpPlayer && cmpPlayer->GetState() == "won")
return true;
}
return false;
}
Index: ps/trunk/source/ps/GameSetup/HWDetect.cpp
===================================================================
--- ps/trunk/source/ps/GameSetup/HWDetect.cpp (revision 24175)
+++ ps/trunk/source/ps/GameSetup/HWDetect.cpp (revision 24176)
@@ -1,720 +1,718 @@
/* 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/ScriptInterface.h"
#include "lib/ogl.h"
#if CONFIG2_AUDIO
#include "lib/snd.h"
#endif
#include "lib/svn_revision.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/res/graphics/ogl_tex.h"
#include "lib/posix/posix_utsname.h"
#include "lib/sysdep/cpu.h"
#include "lib/sysdep/gfx.h"
#include "lib/sysdep/numa.h"
#include "lib/sysdep/os_cpu.h"
#if ARCH_X86_X64
# include "lib/sysdep/arch/x86_x64/cache.h"
# include "lib/sysdep/arch/x86_x64/topology.h"
#endif
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Filesystem.h"
#include "ps/GameSetup/Config.h"
#include "ps/Profile.h"
#include "ps/scripting/JSInterface_ConfigDB.h"
#include "ps/scripting/JSInterface_Debug.h"
#include "ps/UserReport.h"
#include "ps/VideoMode.h"
// TODO: Support OpenGL platforms which don’t use GLX as well.
#if defined(SDL_VIDEO_DRIVER_X11) && !CONFIG2_GLES
#include
#include
// Define the GLX_MESA_query_renderer macros if built with
// an old Mesa (<10.0) that doesn't provide them
#ifndef GLX_MESA_query_renderer
#define GLX_MESA_query_renderer 1
#define GLX_RENDERER_VENDOR_ID_MESA 0x8183
#define GLX_RENDERER_DEVICE_ID_MESA 0x8184
#define GLX_RENDERER_VERSION_MESA 0x8185
#define GLX_RENDERER_ACCELERATED_MESA 0x8186
#define GLX_RENDERER_VIDEO_MEMORY_MESA 0x8187
#define GLX_RENDERER_UNIFIED_MEMORY_ARCHITECTURE_MESA 0x8188
#define GLX_RENDERER_PREFERRED_PROFILE_MESA 0x8189
#define GLX_RENDERER_OPENGL_CORE_PROFILE_VERSION_MESA 0x818A
#define GLX_RENDERER_OPENGL_COMPATIBILITY_PROFILE_VERSION_MESA 0x818B
#define GLX_RENDERER_OPENGL_ES_PROFILE_VERSION_MESA 0x818C
#define GLX_RENDERER_OPENGL_ES2_PROFILE_VERSION_MESA 0x818D
#define GLX_RENDERER_ID_MESA 0x818E
#endif /* GLX_MESA_query_renderer */
#endif
static void ReportSDL(const ScriptInterface& scriptInterface, JS::HandleValue settings);
static void ReportGLLimits(const ScriptInterface& scriptInterface, JS::HandleValue settings);
#if ARCH_X86_X64
void ConvertCaches(const ScriptInterface& scriptInterface, x86_x64::IdxCache idxCache, JS::MutableHandleValue ret)
{
- JSContext* cx = scriptInterface.GetContext();
- JSAutoRequest rq(cx);
+ ScriptInterface::Request rq(scriptInterface);
- ScriptInterface::CreateArray(cx, ret);
+ ScriptInterface::CreateArray(rq, ret);
for (size_t idxLevel = 0; idxLevel < x86_x64::Cache::maxLevels; ++idxLevel)
{
const x86_x64::Cache* pcache = x86_x64::Caches(idxCache+idxLevel);
if (pcache->m_Type == x86_x64::Cache::kNull || pcache->m_NumEntries == 0)
continue;
- JS::RootedValue cache(cx);
+ JS::RootedValue cache(rq.cx);
ScriptInterface::CreateObject(
- cx,
+ rq,
&cache,
"type", static_cast(pcache->m_Type),
"level", static_cast(pcache->m_Level),
"associativity", static_cast(pcache->m_Associativity),
"linesize", static_cast(pcache->m_EntrySize),
"sharedby", static_cast(pcache->m_SharedBy),
"totalsize", static_cast(pcache->TotalSize()));
scriptInterface.SetPropertyInt(ret, idxLevel, cache);
}
}
void ConvertTLBs(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
- JSContext* cx = scriptInterface.GetContext();
- JSAutoRequest rq(cx);
+ ScriptInterface::Request rq(scriptInterface);
- ScriptInterface::CreateArray(cx, ret);
+ ScriptInterface::CreateArray(rq, ret);
for(size_t i = 0; ; i++)
{
const x86_x64::Cache* ptlb = x86_x64::Caches(x86_x64::TLB+i);
if (!ptlb)
break;
- JS::RootedValue tlb(cx);
+ JS::RootedValue tlb(rq.cx);
ScriptInterface::CreateObject(
- cx,
+ rq,
&tlb,
"type", static_cast(ptlb->m_Type),
"level", static_cast(ptlb->m_Level),
"associativity", static_cast(ptlb->m_Associativity),
"pagesize", static_cast(ptlb->m_EntrySize),
"entries", static_cast(ptlb->m_NumEntries));
scriptInterface.SetPropertyInt(ret, i, tlb);
}
}
#endif
void SetDisableAudio(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), bool disabled)
{
g_DisableAudio = disabled;
}
void RunHardwareDetection()
{
TIMER(L"RunHardwareDetection");
ScriptInterface scriptInterface("Engine", "HWDetect", g_ScriptRuntime);
- JSContext* cx = scriptInterface.GetContext();
- JSAutoRequest rq(cx);
+
+ ScriptInterface::Request rq(scriptInterface);
JSI_Debug::RegisterScriptFunctions(scriptInterface); // Engine.DisplayErrorDialog
JSI_ConfigDB::RegisterScriptFunctions(scriptInterface);
scriptInterface.RegisterFunction("SetDisableAudio");
// Load the detection script:
const wchar_t* scriptName = L"hwdetect/hwdetect.js";
CVFSFile file;
if (file.Load(g_VFS, scriptName) != PSRETURN_OK)
{
LOGERROR("Failed to load hardware detection script");
return;
}
std::string code = file.DecodeUTF8(); // assume it's UTF-8
scriptInterface.LoadScript(scriptName, code);
// Collect all the settings we'll pass to the script:
// (We'll use this same data for the opt-in online reporting system, so it
// includes some fields that aren't directly useful for the hwdetect script)
- JS::RootedValue settings(cx);
- ScriptInterface::CreateObject(cx, &settings);
+ JS::RootedValue settings(rq.cx);
+ ScriptInterface::CreateObject(rq, &settings);
scriptInterface.SetProperty(settings, "os_unix", OS_UNIX);
scriptInterface.SetProperty(settings, "os_bsd", OS_BSD);
scriptInterface.SetProperty(settings, "os_linux", OS_LINUX);
scriptInterface.SetProperty(settings, "os_android", OS_ANDROID);
scriptInterface.SetProperty(settings, "os_macosx", OS_MACOSX);
scriptInterface.SetProperty(settings, "os_win", OS_WIN);
scriptInterface.SetProperty(settings, "arch_ia32", ARCH_IA32);
scriptInterface.SetProperty(settings, "arch_amd64", ARCH_AMD64);
scriptInterface.SetProperty(settings, "arch_arm", ARCH_ARM);
scriptInterface.SetProperty(settings, "arch_aarch64", ARCH_AARCH64);
#ifdef NDEBUG
scriptInterface.SetProperty(settings, "build_debug", 0);
#else
scriptInterface.SetProperty(settings, "build_debug", 1);
#endif
scriptInterface.SetProperty(settings, "build_opengles", CONFIG2_GLES);
scriptInterface.SetProperty(settings, "build_datetime", std::string(__DATE__ " " __TIME__));
scriptInterface.SetProperty(settings, "build_revision", std::wstring(svn_revision));
scriptInterface.SetProperty(settings, "build_msc", (int)MSC_VERSION);
scriptInterface.SetProperty(settings, "build_icc", (int)ICC_VERSION);
scriptInterface.SetProperty(settings, "build_gcc", (int)GCC_VERSION);
scriptInterface.SetProperty(settings, "build_clang", (int)CLANG_VERSION);
scriptInterface.SetProperty(settings, "gfx_card", gfx::CardName());
scriptInterface.SetProperty(settings, "gfx_drv_ver", gfx::DriverInfo());
#if CONFIG2_AUDIO
scriptInterface.SetProperty(settings, "snd_card", snd_card);
scriptInterface.SetProperty(settings, "snd_drv_ver", snd_drv_ver);
#endif
ReportSDL(scriptInterface, settings);
ReportGLLimits(scriptInterface, settings);
scriptInterface.SetProperty(settings, "video_desktop_xres", g_VideoMode.GetDesktopXRes());
scriptInterface.SetProperty(settings, "video_desktop_yres", g_VideoMode.GetDesktopYRes());
scriptInterface.SetProperty(settings, "video_desktop_bpp", g_VideoMode.GetDesktopBPP());
scriptInterface.SetProperty(settings, "video_desktop_freq", g_VideoMode.GetDesktopFreq());
struct utsname un;
uname(&un);
scriptInterface.SetProperty(settings, "uname_sysname", std::string(un.sysname));
scriptInterface.SetProperty(settings, "uname_release", std::string(un.release));
scriptInterface.SetProperty(settings, "uname_version", std::string(un.version));
scriptInterface.SetProperty(settings, "uname_machine", std::string(un.machine));
#if OS_LINUX
{
std::ifstream ifs("/etc/lsb-release");
if (ifs.good())
{
std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator());
scriptInterface.SetProperty(settings, "linux_release", str);
}
}
#endif
scriptInterface.SetProperty(settings, "cpu_identifier", std::string(cpu_IdentifierString()));
scriptInterface.SetProperty(settings, "cpu_frequency", os_cpu_ClockFrequency());
scriptInterface.SetProperty(settings, "cpu_pagesize", (u32)os_cpu_PageSize());
scriptInterface.SetProperty(settings, "cpu_largepagesize", (u32)os_cpu_LargePageSize());
scriptInterface.SetProperty(settings, "cpu_numprocs", (u32)os_cpu_NumProcessors());
#if ARCH_X86_X64
scriptInterface.SetProperty(settings, "cpu_numpackages", (u32)topology::NumPackages());
scriptInterface.SetProperty(settings, "cpu_coresperpackage", (u32)topology::CoresPerPackage());
scriptInterface.SetProperty(settings, "cpu_logicalpercore", (u32)topology::LogicalPerCore());
scriptInterface.SetProperty(settings, "cpu_numcaches", (u32)topology::NumCaches());
#endif
scriptInterface.SetProperty(settings, "numa_numnodes", (u32)numa_NumNodes());
scriptInterface.SetProperty(settings, "numa_factor", numa_Factor());
scriptInterface.SetProperty(settings, "numa_interleaved", numa_IsMemoryInterleaved());
scriptInterface.SetProperty(settings, "ram_total", (u32)os_cpu_MemorySize());
scriptInterface.SetProperty(settings, "ram_total_os", (u32)os_cpu_QueryMemorySize());
#if ARCH_X86_X64
scriptInterface.SetProperty(settings, "x86_vendor", (u32)x86_x64::Vendor());
scriptInterface.SetProperty(settings, "x86_model", (u32)x86_x64::Model());
scriptInterface.SetProperty(settings, "x86_family", (u32)x86_x64::Family());
u32 caps0, caps1, caps2, caps3;
x86_x64::GetCapBits(&caps0, &caps1, &caps2, &caps3);
scriptInterface.SetProperty(settings, "x86_caps[0]", caps0);
scriptInterface.SetProperty(settings, "x86_caps[1]", caps1);
scriptInterface.SetProperty(settings, "x86_caps[2]", caps2);
scriptInterface.SetProperty(settings, "x86_caps[3]", caps3);
- JS::RootedValue tmpVal(cx);
+ JS::RootedValue tmpVal(rq.cx);
ConvertCaches(scriptInterface, x86_x64::L1I, &tmpVal);
scriptInterface.SetProperty(settings, "x86_icaches", tmpVal);
ConvertCaches(scriptInterface, x86_x64::L1D, &tmpVal);
scriptInterface.SetProperty(settings, "x86_dcaches", tmpVal);
ConvertTLBs(scriptInterface, &tmpVal);
scriptInterface.SetProperty(settings, "x86_tlbs", tmpVal);
#endif
scriptInterface.SetProperty(settings, "timer_resolution", timer_Resolution());
// The version should be increased for every meaningful change.
const int reportVersion = 13;
// Send the same data to the reporting system
g_UserReporter.SubmitReport(
"hwdetect",
reportVersion,
scriptInterface.StringifyJSON(&settings, false),
scriptInterface.StringifyJSON(&settings, true));
// Run the detection script:
- JS::RootedValue global(cx, scriptInterface.GetGlobalObject());
+ JS::RootedValue global(rq.cx, scriptInterface.GetGlobalObject());
scriptInterface.CallFunctionVoid(global, "RunHardwareDetection", settings);
}
static void ReportSDL(const ScriptInterface& scriptInterface, JS::HandleValue settings)
{
SDL_version build, runtime;
SDL_VERSION(&build);
char version[16];
snprintf(version, ARRAY_SIZE(version), "%d.%d.%d", build.major, build.minor, build.patch);
scriptInterface.SetProperty(settings, "sdl_build_version", version);
SDL_GetVersion(&runtime);
snprintf(version, ARRAY_SIZE(version), "%d.%d.%d", runtime.major, runtime.minor, runtime.patch);
scriptInterface.SetProperty(settings, "sdl_runtime_version", version);
// This is null in atlas (and further the call triggers an assertion).
const char* backend = g_VideoMode.GetWindow() ? GetSDLSubsystem(g_VideoMode.GetWindow()) : "none";
scriptInterface.SetProperty(settings, "sdl_video_backend", backend ? backend : "unknown");
}
static void ReportGLLimits(const ScriptInterface& scriptInterface, JS::HandleValue settings)
{
const char* errstr = "(error)";
#define INTEGER(id) do { \
GLint i = -1; \
glGetIntegerv(GL_##id, &i); \
if (ogl_SquelchError(GL_INVALID_ENUM)) \
scriptInterface.SetProperty(settings, "GL_" #id, errstr); \
else \
scriptInterface.SetProperty(settings, "GL_" #id, i); \
} while (false)
#define INTEGER2(id) do { \
GLint i[2] = { -1, -1 }; \
glGetIntegerv(GL_##id, i); \
if (ogl_SquelchError(GL_INVALID_ENUM)) { \
scriptInterface.SetProperty(settings, "GL_" #id "[0]", errstr); \
scriptInterface.SetProperty(settings, "GL_" #id "[1]", errstr); \
} else { \
scriptInterface.SetProperty(settings, "GL_" #id "[0]", i[0]); \
scriptInterface.SetProperty(settings, "GL_" #id "[1]", i[1]); \
} \
} while (false)
#define FLOAT(id) do { \
GLfloat f = std::numeric_limits::quiet_NaN(); \
glGetFloatv(GL_##id, &f); \
if (ogl_SquelchError(GL_INVALID_ENUM)) \
scriptInterface.SetProperty(settings, "GL_" #id, errstr); \
else \
scriptInterface.SetProperty(settings, "GL_" #id, f); \
} while (false)
#define FLOAT2(id) do { \
GLfloat f[2] = { std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN() }; \
glGetFloatv(GL_##id, f); \
if (ogl_SquelchError(GL_INVALID_ENUM)) { \
scriptInterface.SetProperty(settings, "GL_" #id "[0]", errstr); \
scriptInterface.SetProperty(settings, "GL_" #id "[1]", errstr); \
} else { \
scriptInterface.SetProperty(settings, "GL_" #id "[0]", f[0]); \
scriptInterface.SetProperty(settings, "GL_" #id "[1]", f[1]); \
} \
} while (false)
#define STRING(id) do { \
const char* c = (const char*)glGetString(GL_##id); \
if (!c) c = ""; \
if (ogl_SquelchError(GL_INVALID_ENUM)) c = errstr; \
scriptInterface.SetProperty(settings, "GL_" #id, std::string(c)); \
} while (false)
#define QUERY(target, pname) do { \
GLint i = -1; \
pglGetQueryivARB(GL_##target, GL_##pname, &i); \
if (ogl_SquelchError(GL_INVALID_ENUM)) \
scriptInterface.SetProperty(settings, "GL_" #target ".GL_" #pname, errstr); \
else \
scriptInterface.SetProperty(settings, "GL_" #target ".GL_" #pname, i); \
} while (false)
#define VERTEXPROGRAM(id) do { \
GLint i = -1; \
pglGetProgramivARB(GL_VERTEX_PROGRAM_ARB, GL_##id, &i); \
if (ogl_SquelchError(GL_INVALID_ENUM)) \
scriptInterface.SetProperty(settings, "GL_VERTEX_PROGRAM_ARB.GL_" #id, errstr); \
else \
scriptInterface.SetProperty(settings, "GL_VERTEX_PROGRAM_ARB.GL_" #id, i); \
} while (false)
#define FRAGMENTPROGRAM(id) do { \
GLint i = -1; \
pglGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_##id, &i); \
if (ogl_SquelchError(GL_INVALID_ENUM)) \
scriptInterface.SetProperty(settings, "GL_FRAGMENT_PROGRAM_ARB.GL_" #id, errstr); \
else \
scriptInterface.SetProperty(settings, "GL_FRAGMENT_PROGRAM_ARB.GL_" #id, i); \
} while (false)
#define BOOL(id) INTEGER(id)
ogl_WarnIfError();
// Core OpenGL 1.3:
// (We don't bother checking extension strings for anything older than 1.3;
// it'll just produce harmless warnings)
STRING(VERSION);
STRING(VENDOR);
STRING(RENDERER);
STRING(EXTENSIONS);
#if !CONFIG2_GLES
INTEGER(MAX_LIGHTS);
INTEGER(MAX_CLIP_PLANES);
// Skip MAX_COLOR_MATRIX_STACK_DEPTH (only in imaging subset)
INTEGER(MAX_MODELVIEW_STACK_DEPTH);
INTEGER(MAX_PROJECTION_STACK_DEPTH);
INTEGER(MAX_TEXTURE_STACK_DEPTH);
#endif
INTEGER(SUBPIXEL_BITS);
#if !CONFIG2_GLES
INTEGER(MAX_3D_TEXTURE_SIZE);
#endif
INTEGER(MAX_TEXTURE_SIZE);
INTEGER(MAX_CUBE_MAP_TEXTURE_SIZE);
#if !CONFIG2_GLES
INTEGER(MAX_PIXEL_MAP_TABLE);
INTEGER(MAX_NAME_STACK_DEPTH);
INTEGER(MAX_LIST_NESTING);
INTEGER(MAX_EVAL_ORDER);
#endif
INTEGER2(MAX_VIEWPORT_DIMS);
#if !CONFIG2_GLES
INTEGER(MAX_ATTRIB_STACK_DEPTH);
INTEGER(MAX_CLIENT_ATTRIB_STACK_DEPTH);
INTEGER(AUX_BUFFERS);
BOOL(RGBA_MODE);
BOOL(INDEX_MODE);
BOOL(DOUBLEBUFFER);
BOOL(STEREO);
#endif
FLOAT2(ALIASED_POINT_SIZE_RANGE);
#if !CONFIG2_GLES
FLOAT2(SMOOTH_POINT_SIZE_RANGE);
FLOAT(SMOOTH_POINT_SIZE_GRANULARITY);
#endif
FLOAT2(ALIASED_LINE_WIDTH_RANGE);
#if !CONFIG2_GLES
FLOAT2(SMOOTH_LINE_WIDTH_RANGE);
FLOAT(SMOOTH_LINE_WIDTH_GRANULARITY);
// Skip MAX_CONVOLUTION_WIDTH, MAX_CONVOLUTION_HEIGHT (only in imaging subset)
INTEGER(MAX_ELEMENTS_INDICES);
INTEGER(MAX_ELEMENTS_VERTICES);
INTEGER(MAX_TEXTURE_UNITS);
#endif
INTEGER(SAMPLE_BUFFERS);
INTEGER(SAMPLES);
// TODO: compressed texture formats
INTEGER(RED_BITS);
INTEGER(GREEN_BITS);
INTEGER(BLUE_BITS);
INTEGER(ALPHA_BITS);
#if !CONFIG2_GLES
INTEGER(INDEX_BITS);
#endif
INTEGER(DEPTH_BITS);
INTEGER(STENCIL_BITS);
#if !CONFIG2_GLES
INTEGER(ACCUM_RED_BITS);
INTEGER(ACCUM_GREEN_BITS);
INTEGER(ACCUM_BLUE_BITS);
INTEGER(ACCUM_ALPHA_BITS);
#endif
#if !CONFIG2_GLES
// Core OpenGL 2.0 (treated as extensions):
if (ogl_HaveExtension("GL_EXT_texture_lod_bias"))
{
FLOAT(MAX_TEXTURE_LOD_BIAS_EXT);
}
if (ogl_HaveExtension("GL_ARB_occlusion_query"))
{
QUERY(SAMPLES_PASSED, QUERY_COUNTER_BITS);
}
if (ogl_HaveExtension("GL_ARB_shading_language_100"))
{
STRING(SHADING_LANGUAGE_VERSION_ARB);
}
if (ogl_HaveExtension("GL_ARB_vertex_shader"))
{
INTEGER(MAX_VERTEX_ATTRIBS_ARB);
INTEGER(MAX_VERTEX_UNIFORM_COMPONENTS_ARB);
INTEGER(MAX_VARYING_FLOATS_ARB);
INTEGER(MAX_COMBINED_TEXTURE_IMAGE_UNITS_ARB);
INTEGER(MAX_VERTEX_TEXTURE_IMAGE_UNITS_ARB);
}
if (ogl_HaveExtension("GL_ARB_fragment_shader"))
{
INTEGER(MAX_FRAGMENT_UNIFORM_COMPONENTS_ARB);
}
if (ogl_HaveExtension("GL_ARB_vertex_shader") || ogl_HaveExtension("GL_ARB_fragment_shader") ||
ogl_HaveExtension("GL_ARB_vertex_program") || ogl_HaveExtension("GL_ARB_fragment_program"))
{
INTEGER(MAX_TEXTURE_IMAGE_UNITS_ARB);
INTEGER(MAX_TEXTURE_COORDS_ARB);
}
if (ogl_HaveExtension("GL_ARB_draw_buffers"))
{
INTEGER(MAX_DRAW_BUFFERS_ARB);
}
// Core OpenGL 3.0:
if (ogl_HaveExtension("GL_EXT_gpu_shader4"))
{
INTEGER(MIN_PROGRAM_TEXEL_OFFSET); // no _EXT version of these in glext.h
INTEGER(MAX_PROGRAM_TEXEL_OFFSET);
}
if (ogl_HaveExtension("GL_EXT_framebuffer_object"))
{
INTEGER(MAX_COLOR_ATTACHMENTS_EXT);
INTEGER(MAX_RENDERBUFFER_SIZE_EXT);
}
if (ogl_HaveExtension("GL_EXT_framebuffer_multisample"))
{
INTEGER(MAX_SAMPLES_EXT);
}
if (ogl_HaveExtension("GL_EXT_texture_array"))
{
INTEGER(MAX_ARRAY_TEXTURE_LAYERS_EXT);
}
if (ogl_HaveExtension("GL_EXT_transform_feedback"))
{
INTEGER(MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS_EXT);
INTEGER(MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS_EXT);
INTEGER(MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS_EXT);
}
// Other interesting extensions:
if (ogl_HaveExtension("GL_EXT_timer_query") || ogl_HaveExtension("GL_ARB_timer_query"))
{
QUERY(TIME_ELAPSED, QUERY_COUNTER_BITS);
}
if (ogl_HaveExtension("GL_ARB_timer_query"))
{
QUERY(TIMESTAMP, QUERY_COUNTER_BITS);
}
if (ogl_HaveExtension("GL_EXT_texture_filter_anisotropic"))
{
FLOAT(MAX_TEXTURE_MAX_ANISOTROPY_EXT);
}
if (ogl_HaveExtension("GL_ARB_texture_rectangle"))
{
INTEGER(MAX_RECTANGLE_TEXTURE_SIZE_ARB);
}
if (ogl_HaveExtension("GL_ARB_vertex_program") || ogl_HaveExtension("GL_ARB_fragment_program"))
{
INTEGER(MAX_PROGRAM_MATRICES_ARB);
INTEGER(MAX_PROGRAM_MATRIX_STACK_DEPTH_ARB);
}
if (ogl_HaveExtension("GL_ARB_vertex_program"))
{
VERTEXPROGRAM(MAX_PROGRAM_ENV_PARAMETERS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_LOCAL_PARAMETERS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_INSTRUCTIONS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_TEMPORARIES_ARB);
VERTEXPROGRAM(MAX_PROGRAM_PARAMETERS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_ATTRIBS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_ADDRESS_REGISTERS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_NATIVE_INSTRUCTIONS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEMPORARIES_ARB);
VERTEXPROGRAM(MAX_PROGRAM_NATIVE_PARAMETERS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ATTRIBS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB);
if (ogl_HaveExtension("GL_ARB_fragment_program"))
{
// The spec seems to say these should be supported, but
// Mesa complains about them so let's not bother
/*
VERTEXPROGRAM(MAX_PROGRAM_ALU_INSTRUCTIONS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_TEX_INSTRUCTIONS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_TEX_INDIRECTIONS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ALU_INSTRUCTIONS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEX_INSTRUCTIONS_ARB);
VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEX_INDIRECTIONS_ARB);
*/
}
}
if (ogl_HaveExtension("GL_ARB_fragment_program"))
{
FRAGMENTPROGRAM(MAX_PROGRAM_ENV_PARAMETERS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_LOCAL_PARAMETERS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_INSTRUCTIONS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_ALU_INSTRUCTIONS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_TEX_INSTRUCTIONS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_TEX_INDIRECTIONS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_TEMPORARIES_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_PARAMETERS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_ATTRIBS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_INSTRUCTIONS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ALU_INSTRUCTIONS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEX_INSTRUCTIONS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEX_INDIRECTIONS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEMPORARIES_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_PARAMETERS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ATTRIBS_ARB);
if (ogl_HaveExtension("GL_ARB_vertex_program"))
{
// The spec seems to say these should be supported, but
// Intel drivers on Windows complain about them so let's not bother
/*
FRAGMENTPROGRAM(MAX_PROGRAM_ADDRESS_REGISTERS_ARB);
FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB);
*/
}
}
if (ogl_HaveExtension("GL_ARB_geometry_shader4"))
{
INTEGER(MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB);
INTEGER(MAX_GEOMETRY_OUTPUT_VERTICES_ARB);
INTEGER(MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS_ARB);
INTEGER(MAX_GEOMETRY_UNIFORM_COMPONENTS_ARB);
INTEGER(MAX_GEOMETRY_VARYING_COMPONENTS_ARB);
INTEGER(MAX_VERTEX_VARYING_COMPONENTS_ARB);
}
#else // CONFIG2_GLES
// Core OpenGL ES 2.0:
STRING(SHADING_LANGUAGE_VERSION);
INTEGER(MAX_VERTEX_ATTRIBS);
INTEGER(MAX_VERTEX_UNIFORM_VECTORS);
INTEGER(MAX_VARYING_VECTORS);
INTEGER(MAX_COMBINED_TEXTURE_IMAGE_UNITS);
INTEGER(MAX_VERTEX_TEXTURE_IMAGE_UNITS);
INTEGER(MAX_FRAGMENT_UNIFORM_VECTORS);
INTEGER(MAX_TEXTURE_IMAGE_UNITS);
INTEGER(MAX_RENDERBUFFER_SIZE);
#endif // CONFIG2_GLES
// TODO: Support OpenGL platforms which don’t use GLX as well.
#if defined(SDL_VIDEO_DRIVER_X11) && !CONFIG2_GLES
#define GLXQCR_INTEGER(id) do { \
unsigned int i = UINT_MAX; \
if (pglXQueryCurrentRendererIntegerMESA(id, &i)) \
scriptInterface.SetProperty(settings, #id, i); \
} while (false)
#define GLXQCR_INTEGER2(id) do { \
unsigned int i[2] = { UINT_MAX, UINT_MAX }; \
if (pglXQueryCurrentRendererIntegerMESA(id, i)) { \
scriptInterface.SetProperty(settings, #id "[0]", i[0]); \
scriptInterface.SetProperty(settings, #id "[1]", i[1]); \
} \
} while (false)
#define GLXQCR_INTEGER3(id) do { \
unsigned int i[3] = { UINT_MAX, UINT_MAX, UINT_MAX }; \
if (pglXQueryCurrentRendererIntegerMESA(id, i)) { \
scriptInterface.SetProperty(settings, #id "[0]", i[0]); \
scriptInterface.SetProperty(settings, #id "[1]", i[1]); \
scriptInterface.SetProperty(settings, #id "[2]", i[2]); \
} \
} while (false)
#define GLXQCR_STRING(id) do { \
const char* str = pglXQueryCurrentRendererStringMESA(id); \
if (str) \
scriptInterface.SetProperty(settings, #id ".string", str); \
} while (false)
SDL_SysWMinfo wminfo;
SDL_VERSION(&wminfo.version);
const int ret = SDL_GetWindowWMInfo(g_VideoMode.GetWindow(), &wminfo);
if (ret && wminfo.subsystem == SDL_SYSWM_X11)
{
Display* dpy = wminfo.info.x11.display;
int scrnum = DefaultScreen(dpy);
const char* glxexts = glXQueryExtensionsString(dpy, scrnum);
scriptInterface.SetProperty(settings, "glx_extensions", glxexts);
if (strstr(glxexts, "GLX_MESA_query_renderer") && pglXQueryCurrentRendererIntegerMESA && pglXQueryCurrentRendererStringMESA)
{
GLXQCR_INTEGER(GLX_RENDERER_VENDOR_ID_MESA);
GLXQCR_INTEGER(GLX_RENDERER_DEVICE_ID_MESA);
GLXQCR_INTEGER3(GLX_RENDERER_VERSION_MESA);
GLXQCR_INTEGER(GLX_RENDERER_ACCELERATED_MESA);
GLXQCR_INTEGER(GLX_RENDERER_VIDEO_MEMORY_MESA);
GLXQCR_INTEGER(GLX_RENDERER_UNIFIED_MEMORY_ARCHITECTURE_MESA);
GLXQCR_INTEGER(GLX_RENDERER_PREFERRED_PROFILE_MESA);
GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_CORE_PROFILE_VERSION_MESA);
GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_COMPATIBILITY_PROFILE_VERSION_MESA);
GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_ES_PROFILE_VERSION_MESA);
GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_ES2_PROFILE_VERSION_MESA);
GLXQCR_STRING(GLX_RENDERER_VENDOR_ID_MESA);
GLXQCR_STRING(GLX_RENDERER_DEVICE_ID_MESA);
}
}
#endif // SDL_VIDEO_DRIVER_X11
}
Index: ps/trunk/source/ps/ModInstaller.cpp
===================================================================
--- ps/trunk/source/ps/ModInstaller.cpp (revision 24175)
+++ ps/trunk/source/ps/ModInstaller.cpp (revision 24176)
@@ -1,110 +1,113 @@
-/* Copyright (C) 2018 Wildfire Games.
+/* 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 "ModInstaller.h"
#include "lib/file/vfs/vfs_util.h"
#include "ps/Filesystem.h"
#include "ps/XML/Xeromyces.h"
#include
CModInstaller::CModInstaller(const OsPath& modsdir, const OsPath& tempdir) :
m_ModsDir(modsdir), m_TempDir(tempdir / "_modscache"), m_CacheDir("cache/")
{
m_VFS = CreateVfs();
CreateDirectories(m_TempDir, 0700);
}
CModInstaller::~CModInstaller()
{
m_VFS.reset();
DeleteDirectory(m_TempDir);
}
CModInstaller::ModInstallationResult CModInstaller::Install(
const OsPath& mod,
const std::shared_ptr& scriptRuntime,
bool keepFile)
{
const OsPath modTemp = m_TempDir / mod.Basename() / mod.Filename().ChangeExtension(L".zip");
CreateDirectories(modTemp.Parent(), 0700);
if (keepFile)
CopyFile(mod, modTemp, true);
else
wrename(mod, modTemp);
// Load the mod to VFS
if (m_VFS->Mount(m_CacheDir, m_TempDir / "") != INFO::OK)
return FAIL_ON_VFS_MOUNT;
CVFSFile modinfo;
PSRETURN modinfo_status = modinfo.Load(m_VFS, m_CacheDir / modTemp.Basename() / "mod.json", false);
m_VFS->Clear();
if (modinfo_status != PSRETURN_OK)
return FAIL_ON_MOD_LOAD;
// Extract the name of the mod
- ScriptInterface scriptInterface("Engine", "ModInstaller", scriptRuntime);
- JSContext* cx = scriptInterface.GetContext();
- JS::RootedValue json_val(cx);
- if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json_val))
- return FAIL_ON_PARSE_JSON;
- JS::RootedObject json_obj(cx, json_val.toObjectOrNull());
- JS::RootedValue name_val(cx);
- if (!JS_GetProperty(cx, json_obj, "name", &name_val))
- return FAIL_ON_EXTRACT_NAME;
CStr modName;
- ScriptInterface::FromJSVal(cx, name_val, modName);
- if (modName.empty())
- return FAIL_ON_EXTRACT_NAME;
+ {
+ ScriptInterface scriptInterface("Engine", "ModInstaller", scriptRuntime);
+ ScriptInterface::Request rq(scriptInterface);
+
+ JS::RootedValue json_val(rq.cx);
+ if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json_val))
+ return FAIL_ON_PARSE_JSON;
+ JS::RootedObject json_obj(rq.cx, json_val.toObjectOrNull());
+ JS::RootedValue name_val(rq.cx);
+ if (!JS_GetProperty(rq.cx, json_obj, "name", &name_val))
+ return FAIL_ON_EXTRACT_NAME;
+ ScriptInterface::FromJSVal(rq, name_val, modName);
+ if (modName.empty())
+ return FAIL_ON_EXTRACT_NAME;
+ }
const OsPath modDir = m_ModsDir / modName;
const OsPath modPath = modDir / (modName + ".zip");
// Create a directory with the following structure:
// mod-name/
// mod-name.zip
CreateDirectories(modDir, 0700);
if (wrename(modTemp, modPath) != 0)
return FAIL_ON_MOD_MOVE;
DeleteDirectory(modTemp.Parent());
#ifdef OS_WIN
// On Windows, write the contents of mod.json to a separate file next to the archive:
// mod-name/
// mod-name.zip
// mod.json
std::ofstream mod_json((modDir / "mod.json").string8());
if (mod_json.good())
{
mod_json << modinfo.GetAsString();
mod_json.close();
}
#endif // OS_WIN
m_InstalledMods.emplace_back(modName);
return SUCCESS;
}
const std::vector& CModInstaller::GetInstalledMods() const
{
return m_InstalledMods;
}
Index: ps/trunk/source/ps/Replay.cpp
===================================================================
--- ps/trunk/source/ps/Replay.cpp (revision 24175)
+++ ps/trunk/source/ps/Replay.cpp (revision 24176)
@@ -1,343 +1,338 @@
-/* Copyright (C) 2019 Wildfire Games.
+/* 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 "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);
+ ScriptInterface::Request rq(m_ScriptInterface);
// 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));
+ JS::RootedValue mods(rq.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);
+ ScriptInterface::Request rq(m_ScriptInterface);
*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);
+ ScriptInterface::Request rq(scriptInterface);
- JS::RootedValue arg(cx);
- JS::RootedValue metadata(cx);
+ JS::RootedValue arg(rq.cx);
+ JS::RootedValue metadata(rq.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);
+ ScriptInterface::Request rq(scriptInterface);
std::vector> replayMods;
scriptInterface.GetProperty(attribs, "mods", replayMods);
std::vector> enabledMods;
- JS::RootedValue enabledModsJS(cx, Mod::GetLoadedModsWithVersions(scriptInterface));
- scriptInterface.FromJSVal(cx, enabledModsJS, enabledMods);
+ JS::RootedValue enabledModsJS(rq.cx, Mod::GetLoadedModsWithVersions(scriptInterface));
+ scriptInterface.FromJSVal(rq, 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 = 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);
+ ScriptInterface::Request rq(g_Game->GetSimulation2()->GetScriptInterface());
std::string type;
while ((*m_Stream >> type).good())
{
if (type == "start")
{
std::string line;
std::getline(*m_Stream, line);
- JS::RootedValue attribs(cx);
+ JS::RootedValue attribs(rq.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);
+ JS::RootedValue data(rq.cx);
g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &data);
g_Game->GetSimulation2()->GetScriptInterface().FreezeObject(data, true);
- commands.emplace_back(SimulationCommand(player, cx, data));
+ commands.emplace_back(SimulationCommand(player, rq.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/ps/scripting/JSInterface_Game.cpp
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_Game.cpp (revision 24175)
+++ ps/trunk/source/ps/scripting/JSInterface_Game.cpp (revision 24176)
@@ -1,189 +1,186 @@
-/* Copyright (C) 2019 Wildfire Games.
+/* 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 "JSInterface_Game.h"
#include "graphics/Terrain.h"
#include "network/NetClient.h"
#include "network/NetServer.h"
#include "ps/CLogger.h"
#include "ps/Game.h"
#include "ps/Replay.h"
#include "ps/World.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/system/TurnManager.h"
#include "simulation2/Simulation2.h"
#include "soundmanager/SoundManager.h"
extern void EndGame();
bool JSI_Game::IsGameStarted(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_Game;
}
void JSI_Game::StartGame(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs, int playerID)
{
ENSURE(!g_NetServer);
ENSURE(!g_NetClient);
ENSURE(!g_Game);
g_Game = new CGame(true);
// Convert from GUI script context to sim script context
CSimulation2* sim = g_Game->GetSimulation2();
- JSContext* cxSim = sim->GetScriptInterface().GetContext();
- JSAutoRequest rqSim(cxSim);
+ ScriptInterface::Request rqSim(sim->GetScriptInterface());
- JS::RootedValue gameAttribs(cxSim,
+ JS::RootedValue gameAttribs(rqSim.cx,
sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), attribs));
g_Game->SetPlayerID(playerID);
g_Game->StartGame(&gameAttribs, "");
}
void JSI_Game::Script_EndGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
EndGame();
}
int JSI_Game::GetPlayerID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (!g_Game)
return -1;
return g_Game->GetPlayerID();
}
void JSI_Game::SetPlayerID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int id)
{
if (!g_Game)
return;
g_Game->SetPlayerID(id);
}
void JSI_Game::SetViewedPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int id)
{
if (!g_Game)
return;
g_Game->SetViewedPlayerID(id);
}
float JSI_Game::GetSimRate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_Game->GetSimRate();
}
void JSI_Game::SetSimRate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), float rate)
{
g_Game->SetSimRate(rate);
}
bool JSI_Game::IsPaused(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_Game)
{
- JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
- JSAutoRequest rq(cx);
- JS_ReportError(cx, "Game is not started");
+ ScriptInterface::Request rq(pCxPrivate);
+ JS_ReportError(rq.cx, "Game is not started");
return false;
}
return g_Game->m_Paused;
}
void JSI_Game::SetPaused(ScriptInterface::CxPrivate* pCxPrivate, bool pause, bool sendMessage)
{
if (!g_Game)
{
- JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
- JSAutoRequest rq(cx);
- JS_ReportError(cx, "Game is not started");
+ ScriptInterface::Request rq(pCxPrivate);
+ JS_ReportError(rq.cx, "Game is not started");
return;
}
g_Game->m_Paused = pause;
#if CONFIG2_AUDIO
if (g_SoundManager)
g_SoundManager->Pause(pause);
#endif
if (g_NetClient && sendMessage)
g_NetClient->SendPausedMessage(pause);
}
bool JSI_Game::IsVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (!g_Game)
return false;
return g_Game->IsVisualReplay();
}
std::wstring JSI_Game::GetCurrentReplayDirectory(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (!g_Game)
return std::wstring();
if (g_Game->IsVisualReplay())
return g_Game->GetReplayPath().Parent().Filename().string();
return g_Game->GetReplayLogger().GetDirectory().Filename().string();
}
void JSI_Game::EnableTimeWarpRecording(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), unsigned int numTurns)
{
g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns);
}
void JSI_Game::RewindTimeWarp(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
g_Game->GetTurnManager()->RewindTimeWarp();
}
void JSI_Game::DumpTerrainMipmap(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
VfsPath filename(L"screenshots/terrainmipmap.png");
g_Game->GetWorld()->GetTerrain()->GetHeightMipmap().DumpToDisk(filename);
OsPath realPath;
g_VFS->GetRealPath(filename, realPath);
LOGMESSAGERENDER("Terrain mipmap written to '%s'", realPath.string8());
}
void JSI_Game::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("IsGameStarted");
scriptInterface.RegisterFunction("StartGame");
scriptInterface.RegisterFunction("EndGame");
scriptInterface.RegisterFunction("GetPlayerID");
scriptInterface.RegisterFunction("SetPlayerID");
scriptInterface.RegisterFunction("SetViewedPlayer");
scriptInterface.RegisterFunction("GetSimRate");
scriptInterface.RegisterFunction("SetSimRate");
scriptInterface.RegisterFunction("IsPaused");
scriptInterface.RegisterFunction("SetPaused");
scriptInterface.RegisterFunction("IsVisualReplay");
scriptInterface.RegisterFunction("GetCurrentReplayDirectory");
scriptInterface.RegisterFunction("EnableTimeWarpRecording");
scriptInterface.RegisterFunction("RewindTimeWarp");
scriptInterface.RegisterFunction("DumpTerrainMipmap");
}
Index: ps/trunk/source/ps/scripting/JSInterface_SavedGame.cpp
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_SavedGame.cpp (revision 24175)
+++ ps/trunk/source/ps/scripting/JSInterface_SavedGame.cpp (revision 24176)
@@ -1,127 +1,125 @@
-/* Copyright (C) 2019 Wildfire Games.
+/* 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 "JSInterface_SavedGame.h"
#include "network/NetClient.h"
#include "network/NetServer.h"
#include "ps/CLogger.h"
#include "ps/Game.h"
#include "ps/SavedGame.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/TurnManager.h"
JS::Value JSI_SavedGame::GetSavedGames(ScriptInterface::CxPrivate* pCxPrivate)
{
return SavedGames::GetSavedGames(*(pCxPrivate->pScriptInterface));
}
bool JSI_SavedGame::DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name)
{
return SavedGames::DeleteSavedGame(name);
}
void JSI_SavedGame::SaveGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename, const std::wstring& description, JS::HandleValue GUIMetadata)
{
shared_ptr GUIMetadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata);
if (SavedGames::Save(filename, description, *g_Game->GetSimulation2(), GUIMetadataClone) < 0)
LOGERROR("Failed to save game");
}
void JSI_SavedGame::SaveGamePrefix(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& prefix, const std::wstring& description, JS::HandleValue GUIMetadata)
{
shared_ptr GUIMetadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata);
if (SavedGames::SavePrefix(prefix, description, *g_Game->GetSimulation2(), GUIMetadataClone) < 0)
LOGERROR("Failed to save game");
}
void JSI_SavedGame::QuickSave(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), JS::HandleValue GUIMetadata)
{
if (g_NetServer || g_NetClient)
LOGERROR("Can't store quicksave during multiplayer!");
else if (g_Game)
g_Game->GetTurnManager()->QuickSave(GUIMetadata);
else
LOGERROR("Can't store quicksave if game is not running!");
}
void JSI_SavedGame::QuickLoad(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (g_NetServer || g_NetClient)
LOGERROR("Can't load quicksave during multiplayer!");
else if (g_Game)
g_Game->GetTurnManager()->QuickLoad();
else
LOGERROR("Can't load quicksave if game is not running!");
}
JS::Value JSI_SavedGame::StartSavedGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name)
{
// We need to be careful with different compartments and contexts.
// The GUI calls this function from the GUI context and expects the return value in the same context.
// The game we start from here creates another context and expects data in this context.
- JSContext* cxGui = pCxPrivate->pScriptInterface->GetContext();
- JSAutoRequest rq(cxGui);
+ ScriptInterface::Request rqGui(pCxPrivate);
ENSURE(!g_NetServer);
ENSURE(!g_NetClient);
ENSURE(!g_Game);
// Load the saved game data from disk
- JS::RootedValue guiContextMetadata(cxGui);
+ JS::RootedValue guiContextMetadata(rqGui.cx);
std::string savedState;
Status err = SavedGames::Load(name, *(pCxPrivate->pScriptInterface), &guiContextMetadata, savedState);
if (err < 0)
return JS::UndefinedValue();
g_Game = new CGame(true);
{
CSimulation2* sim = g_Game->GetSimulation2();
- JSContext* cxGame = sim->GetScriptInterface().GetContext();
- JSAutoRequest rq(cxGame);
+ ScriptInterface::Request rqGame(sim->GetScriptInterface());
- JS::RootedValue gameContextMetadata(cxGame,
+ JS::RootedValue gameContextMetadata(rqGame.cx,
sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), guiContextMetadata));
- JS::RootedValue gameInitAttributes(cxGame);
+ JS::RootedValue gameInitAttributes(rqGame.cx);
sim->GetScriptInterface().GetProperty(gameContextMetadata, "initAttributes", &gameInitAttributes);
int playerID;
sim->GetScriptInterface().GetProperty(gameContextMetadata, "playerID", playerID);
g_Game->SetPlayerID(playerID);
g_Game->StartGame(&gameInitAttributes, savedState);
}
return guiContextMetadata;
}
void JSI_SavedGame::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("GetSavedGames");
scriptInterface.RegisterFunction("DeleteSavedGame");
scriptInterface.RegisterFunction("SaveGame");
scriptInterface.RegisterFunction("SaveGamePrefix");
scriptInterface.RegisterFunction("QuickSave");
scriptInterface.RegisterFunction("QuickLoad");
scriptInterface.RegisterFunction("StartSavedGame");
}
Index: ps/trunk/source/scriptinterface/NativeWrapperDecls.h
===================================================================
--- ps/trunk/source/scriptinterface/NativeWrapperDecls.h (revision 24175)
+++ ps/trunk/source/scriptinterface/NativeWrapperDecls.h (revision 24176)
@@ -1,112 +1,112 @@
/* Copyright (C) 2017 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
#include
// MaybeRef should be private, but has to be public due to a compiler bug in clang.
// TODO: Make this private when the bug is fixed in all supported versions of clang.
template struct MaybeRef;
// Define lots of useful macros:
// Varieties of comma-separated list to fit on the head/tail/whole of another comma-separated list
#define NUMBERED_LIST_HEAD(z, i, data) data##i,
#define NUMBERED_LIST_TAIL(z, i, data) ,data##i
#define NUMBERED_LIST_TAIL_MAYBE_REF(z, i, data) , typename MaybeRef::Type
#define NUMBERED_LIST_BALANCED(z, i, data) BOOST_PP_COMMA_IF(i) data##i
#define NUMBERED_LIST_BALANCED_MAYBE_REF(z, i, data) BOOST_PP_COMMA_IF(i) typename MaybeRef::Type
// TODO: We allow optional parameters when the C++ type can be converted from JS::UndefinedValue.
// FromJSVal is expected to either set a##i or return false (otherwise we could get undefined
// behaviour because some types have undefined values when not being initialized).
// This is not very clear and also a bit fragile. Another problem is that the error reporting lacks
// a bit. SpiderMonkey will throw a JS exception and abort the execution of the current function when
// we return false here (without printing a callstack or additional detail telling that an argument
// conversion failed). So we have two TODOs here:
// 1. On the conceptual side: How to consistently work with optional parameters (or drop them completely?)
// 2. On the technical side: Improve error handling, find a better way to ensure parameters are initialized
#define CONVERT_ARG(z, i, data) \
bool typeConvRet##i; \
T##i a##i = ScriptInterface::AssignOrFromJSVal( \
- cx, \
+ rq, \
i < args.length() ? args[i] : JS::UndefinedHandleValue, \
typeConvRet##i); \
if (!typeConvRet##i) return false;
// List-generating macros, named roughly after their first list item
#define TYPENAME_T0_HEAD(z, i) BOOST_PP_REPEAT_##z (i, NUMBERED_LIST_HEAD, typename T) // "typename T0, typename T1, "
#define T0(z, i) BOOST_PP_REPEAT_##z (i, NUMBERED_LIST_BALANCED, T) // "T0, T1"
#define T0_MAYBE_REF(z, i) BOOST_PP_REPEAT_##z (i, NUMBERED_LIST_BALANCED_MAYBE_REF, T) // "const T0&, T1"
#define T0_TAIL(z, i) BOOST_PP_REPEAT_##z (i, NUMBERED_LIST_TAIL, T) // ", T0, T1"
#define T0_TAIL_MAYBE_REF(z, i) BOOST_PP_REPEAT_##z (i, NUMBERED_LIST_TAIL_MAYBE_REF, T) // ", const T0&, T1"
#define A0_TAIL(z, i) BOOST_PP_REPEAT_##z (i, NUMBERED_LIST_TAIL, a) // ", a0, a1"
// Define RegisterFunction
#define OVERLOADS(z, i, data) \
template \
void RegisterFunction(const char* name) const \
{ \
Register(name, call, nargs()); \
}
BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~)
#undef OVERLOADS
// JSFastNative-compatible function that wraps the function identified in the template argument list
// (Definition comes later, since it depends on some things we haven't defined yet)
#define OVERLOADS(z, i, data) \
template \
static bool call(JSContext* cx, uint argc, JS::Value* vp);
BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~)
#undef OVERLOADS
// Similar, for class methods
#define OVERLOADS(z, i, data) \
template \
static bool callMethod(JSContext* cx, uint argc, JS::Value* vp);
BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~)
#undef OVERLOADS
// const methods
#define OVERLOADS(z, i, data) \
template \
static bool callMethodConst(JSContext* cx, uint argc, JS::Value* vp);
BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~)
#undef OVERLOADS
// Argument-number counter
template
static size_t nargs() { return sizeof...(Ts); }
// Call the named property on the given object
template
bool CallFunction(JS::HandleValue val, const char* name, R& ret, const Ts&... params) const;
// Implicit conversion from JS::Rooted* to JS::MutableHandle does not work with template argument deduction
// (only exact type matches allowed). We need this overload to allow passing Rooted* using the & operator
// (as people would expect it to work based on the SpiderMonkey rooting guide).
template
bool CallFunction(JS::HandleValue val, const char* name, JS::Rooted* ret, const Ts&... params) const;
// This overload is for the case when a JS::MutableHandle type gets passed into CallFunction directly and
// without requiring implicit conversion.
template
bool CallFunction(JS::HandleValue val, const char* name, JS::MutableHandle ret, const Ts&... params) const;
// Call the named property on the given object, with void return type
template \
bool CallFunctionVoid(JS::HandleValue val, const char* name, const Ts&... params) const;
Index: ps/trunk/source/scriptinterface/ScriptConversions.h
===================================================================
--- ps/trunk/source/scriptinterface/ScriptConversions.h (revision 24175)
+++ ps/trunk/source/scriptinterface/ScriptConversions.h (revision 24176)
@@ -1,112 +1,109 @@
/* 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 .
*/
#ifndef INCLUDED_SCRIPTCONVERSIONS
#define INCLUDED_SCRIPTCONVERSIONS
#include "ScriptInterface.h"
#include "scriptinterface/ScriptExtraHeaders.h" // for typed arrays
#include
-template static void ToJSVal_vector(JSContext* cx, JS::MutableHandleValue ret, const std::vector& val)
+template static void ToJSVal_vector(const ScriptInterface::Request& rq, JS::MutableHandleValue ret, const std::vector& val)
{
- JSAutoRequest rq(cx);
- JS::RootedObject obj(cx, JS_NewArrayObject(cx, 0));
+ JS::RootedObject obj(rq.cx, JS_NewArrayObject(rq.cx, 0));
if (!obj)
{
ret.setUndefined();
return;
}
ENSURE(val.size() <= std::numeric_limits::max());
for (u32 i = 0; i < val.size(); ++i)
{
- JS::RootedValue el(cx);
- ScriptInterface::ToJSVal(cx, &el, val[i]);
- JS_SetElement(cx, obj, i, el);
+ JS::RootedValue el(rq.cx);
+ ScriptInterface::ToJSVal(rq, &el, val[i]);
+ JS_SetElement(rq.cx, obj, i, el);
}
ret.setObject(*obj);
}
-#define FAIL(msg) STMT(JS_ReportError(cx, msg); return false)
+#define FAIL(msg) STMT(JS_ReportError(rq.cx, msg); return false)
-template static bool FromJSVal_vector(JSContext* cx, JS::HandleValue v, std::vector& out)
+template static bool FromJSVal_vector(const ScriptInterface::Request& rq, JS::HandleValue v, std::vector& out)
{
- JSAutoRequest rq(cx);
- JS::RootedObject obj(cx);
+ JS::RootedObject obj(rq.cx);
if (!v.isObject())
FAIL("Argument must be an array");
bool isArray;
obj = &v.toObject();
- if ((!JS_IsArrayObject(cx, obj, &isArray) || !isArray) && !JS_IsTypedArrayObject(obj))
+ if ((!JS_IsArrayObject(rq.cx, obj, &isArray) || !isArray) && !JS_IsTypedArrayObject(obj))
FAIL("Argument must be an array");
u32 length;
- if (!JS_GetArrayLength(cx, obj, &length))
+ if (!JS_GetArrayLength(rq.cx, obj, &length))
FAIL("Failed to get array length");
out.reserve(length);
for (u32 i = 0; i < length; ++i)
{
- JS::RootedValue el(cx);
- if (!JS_GetElement(cx, obj, i, &el))
+ JS::RootedValue el(rq.cx);
+ if (!JS_GetElement(rq.cx, obj, i, &el))
FAIL("Failed to read array element");
T el2;
- if (!ScriptInterface::FromJSVal(cx, el, el2))
+ if (!ScriptInterface::FromJSVal(rq, el, el2))
return false;
out.push_back(el2);
}
return true;
}
#undef FAIL
#define JSVAL_VECTOR(T) \
-template<> void ScriptInterface::ToJSVal >(JSContext* cx, JS::MutableHandleValue ret, const std::vector& val) \
+template<> void ScriptInterface::ToJSVal >(const ScriptInterface::Request& rq, JS::MutableHandleValue ret, const std::vector& val) \
{ \
- ToJSVal_vector(cx, ret, val); \
+ ToJSVal_vector(rq, ret, val); \
} \
-template<> bool ScriptInterface::FromJSVal >(JSContext* cx, JS::HandleValue v, std::vector& out) \
+template<> bool ScriptInterface::FromJSVal >(const ScriptInterface::Request& rq, JS::HandleValue v, std::vector& out) \
{ \
- return FromJSVal_vector(cx, v, out); \
+ return FromJSVal_vector(rq, v, out); \
}
-template bool ScriptInterface::FromJSProperty(JSContext* cx, const JS::HandleValue val, const char* name, T& ret, bool strict)
+template bool ScriptInterface::FromJSProperty(const ScriptInterface::Request& rq, const JS::HandleValue val, const char* name, T& ret, bool strict)
{
if (!val.isObject())
return false;
- JSAutoRequest rq(cx);
- JS::RootedObject obj(cx, &val.toObject());
+ JS::RootedObject obj(rq.cx, &val.toObject());
bool hasProperty;
- if (!JS_HasProperty(cx, obj, name, &hasProperty) || !hasProperty)
+ if (!JS_HasProperty(rq.cx, obj, name, &hasProperty) || !hasProperty)
return false;
- JS::RootedValue value(cx);
- if (!JS_GetProperty(cx, obj, name, &value))
+ JS::RootedValue value(rq.cx);
+ if (!JS_GetProperty(rq.cx, obj, name, &value))
return false;
if (strict && value.isNull())
return false;
- return FromJSVal(cx, value, ret);
+ return FromJSVal(rq, value, ret);
}
#endif //INCLUDED_SCRIPTCONVERSIONS
Index: ps/trunk/source/ps/ModIo.cpp
===================================================================
--- ps/trunk/source/ps/ModIo.cpp (revision 24175)
+++ ps/trunk/source/ps/ModIo.cpp (revision 24176)
@@ -1,857 +1,855 @@
/* Copyright (C) 2020 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "precompiled.h"
#include "ModIo.h"
#include "i18n/L10n.h"
#include "lib/file/file_system.h"
#include "lib/sysdep/filesystem.h"
#include "lib/sysdep/sysdep.h"
#include "maths/MD5.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/GameSetup/Paths.h"
#include "ps/Mod.h"
#include "ps/ModInstaller.h"
#include "ps/Util.h"
#include "scriptinterface/ScriptConversions.h"
#include "scriptinterface/ScriptInterface.h"
#include
#include
ModIo* g_ModIo = nullptr;
struct DownloadCallbackData
{
DownloadCallbackData()
: fp(nullptr), md5(), hash_state(nullptr)
{
}
DownloadCallbackData(FILE* _fp)
: fp(_fp), md5()
{
hash_state = static_cast(
sodium_malloc(crypto_generichash_statebytes()));
ENSURE(hash_state);
crypto_generichash_init(hash_state, nullptr, 0U, crypto_generichash_BYTES_MAX);
}
~DownloadCallbackData()
{
if (hash_state)
sodium_free(hash_state);
}
FILE* fp;
MD5 md5;
crypto_generichash_state* hash_state;
};
ModIo::ModIo()
: m_GamesRequest("/games"), m_CallbackData(nullptr)
{
// Get config values from the default namespace.
// This can be overridden on the command line.
//
// We do this so a malicious mod cannot change the base url and
// get the user to make connections to someone else's endpoint.
// If another user of the engine wants to provide different values
// here, while still using the same engine version, they can just
// provide some shortcut/script that sets these using command line
// parameters.
std::string pk_str;
g_ConfigDB.GetValue(CFG_DEFAULT, "modio.public_key", pk_str);
g_ConfigDB.GetValue(CFG_DEFAULT, "modio.v1.baseurl", m_BaseUrl);
{
std::string api_key;
g_ConfigDB.GetValue(CFG_DEFAULT, "modio.v1.api_key", api_key);
m_ApiKey = "api_key=" + api_key;
}
{
std::string nameid;
g_ConfigDB.GetValue(CFG_DEFAULT, "modio.v1.name_id", nameid);
m_IdQuery = "name_id="+nameid;
}
m_CurlMulti = curl_multi_init();
ENSURE(m_CurlMulti);
m_Curl = curl_easy_init();
ENSURE(m_Curl);
// Capture error messages
curl_easy_setopt(m_Curl, CURLOPT_ERRORBUFFER, m_ErrorBuffer);
// Fail if the server did
curl_easy_setopt(m_Curl, CURLOPT_FAILONERROR, 1L);
// Disable signal handlers (required for multithreaded applications)
curl_easy_setopt(m_Curl, CURLOPT_NOSIGNAL, 1L);
// To minimise security risks, don't support redirects (except for file
// downloads, for which this setting will be enabled).
curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L);
// For file downloads, one redirect seems plenty for a CDN serving the files.
curl_easy_setopt(m_Curl, CURLOPT_MAXREDIRS, 1L);
m_Headers = NULL;
std::string ua = "User-Agent: pyrogenesis ";
ua += curl_version();
ua += " (https://play0ad.com/)";
m_Headers = curl_slist_append(m_Headers, ua.c_str());
curl_easy_setopt(m_Curl, CURLOPT_HTTPHEADER, m_Headers);
if (sodium_init() < 0)
ENSURE(0 && "Failed to initialize libsodium.");
size_t bin_len = 0;
if (sodium_base642bin((unsigned char*)&m_pk, sizeof m_pk, pk_str.c_str(), pk_str.size(), NULL, &bin_len, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || bin_len != sizeof m_pk)
ENSURE(0 && "Failed to decode base64 public key. Please fix your configuration or mod.io will be unusable.");
}
ModIo::~ModIo()
{
// Clean things up to avoid unpleasant surprises,
// and delete the temporary file if any.
TearDownRequest();
if (m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING)
DeleteDownloadedFile();
curl_slist_free_all(m_Headers);
curl_easy_cleanup(m_Curl);
curl_multi_cleanup(m_CurlMulti);
delete m_CallbackData;
}
size_t ModIo::ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp)
{
ModIo* self = static_cast(userp);
self->m_ResponseData += std::string((char*)buffer, (char*)buffer+size*nmemb);
return size*nmemb;
}
size_t ModIo::DownloadCallback(void* buffer, size_t size, size_t nmemb, void* userp)
{
DownloadCallbackData* data = static_cast(userp);
if (!data->fp)
return 0;
size_t len = fwrite(buffer, size, nmemb, data->fp);
// Only update the hash with data we actually managed to write.
// In case we did not write all of it we will fail the download,
// but we do not want to have a possibly valid hash in that case.
size_t written = len*size;
data->md5.Update(static_cast(buffer), written);
ENSURE(data->hash_state);
crypto_generichash_update(data->hash_state, static_cast(buffer), written);
return written;
}
int ModIo::DownloadProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t UNUSED(ultotal), curl_off_t UNUSED(ulnow))
{
DownloadProgressData* data = static_cast(clientp);
// If we got more data than curl expected, something is very wrong, abort.
if (dltotal != 0 && dlnow > dltotal)
return 1;
data->progress = dltotal == 0 ? 0 : static_cast(dlnow) / static_cast(dltotal);
return 0;
}
CURLMcode ModIo::SetupRequest(const std::string& url, bool fileDownload)
{
if (fileDownload)
{
// The download link will most likely redirect elsewhere, so allow that.
// We verify the validity of the file later.
curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 1L);
// Enable the progress meter
curl_easy_setopt(m_Curl, CURLOPT_NOPROGRESS, 0L);
// Set IO callbacks
curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, DownloadCallback);
curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, static_cast(m_CallbackData));
curl_easy_setopt(m_Curl, CURLOPT_XFERINFOFUNCTION, DownloadProgressCallback);
curl_easy_setopt(m_Curl, CURLOPT_XFERINFODATA, static_cast(&m_DownloadProgressData));
// Initialize the progress counter
m_DownloadProgressData.progress = 0;
}
else
{
// To minimise security risks, don't support redirects
curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L);
// Disable the progress meter
curl_easy_setopt(m_Curl, CURLOPT_NOPROGRESS, 1L);
// Set IO callbacks
curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, ReceiveCallback);
curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, this);
}
m_ErrorBuffer[0] = '\0';
curl_easy_setopt(m_Curl, CURLOPT_URL, url.c_str());
return curl_multi_add_handle(m_CurlMulti, m_Curl);
}
void ModIo::TearDownRequest()
{
ENSURE(curl_multi_remove_handle(m_CurlMulti, m_Curl) == CURLM_OK);
if (m_CallbackData)
{
if (m_CallbackData->fp)
fclose(m_CallbackData->fp);
m_CallbackData->fp = nullptr;
}
}
void ModIo::StartGetGameId()
{
// Don't start such a request during active downloads.
if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID ||
m_DownloadProgressData.status == DownloadProgressStatus::LISTING ||
m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING)
return;
m_GameId.clear();
CURLMcode err = SetupRequest(m_BaseUrl+m_GamesRequest+"?"+m_ApiKey+"&"+m_IdQuery, false);
if (err != CURLM_OK)
{
TearDownRequest();
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_GAMEID;
m_DownloadProgressData.error = fmt::sprintf(
g_L10n.Translate("Failure while starting querying for game id. Error: %s; %s."),
curl_multi_strerror(err), m_ErrorBuffer);
return;
}
m_DownloadProgressData.status = DownloadProgressStatus::GAMEID;
}
void ModIo::StartListMods()
{
// Don't start such a request during active downloads.
if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID ||
m_DownloadProgressData.status == DownloadProgressStatus::LISTING ||
m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING)
return;
m_ModData.clear();
if (m_GameId.empty())
{
LOGERROR("Game ID not fetched from mod.io. Call StartGetGameId first and wait for it to finish.");
return;
}
CURLMcode err = SetupRequest(m_BaseUrl+m_GamesRequest+m_GameId+"/mods?"+m_ApiKey, false);
if (err != CURLM_OK)
{
TearDownRequest();
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_LISTING;
m_DownloadProgressData.error = fmt::sprintf(
g_L10n.Translate("Failure while starting querying for mods. Error: %s; %s."),
curl_multi_strerror(err), m_ErrorBuffer);
return;
}
m_DownloadProgressData.status = DownloadProgressStatus::LISTING;
}
void ModIo::StartDownloadMod(size_t idx)
{
// Don't start such a request during active downloads.
if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID ||
m_DownloadProgressData.status == DownloadProgressStatus::LISTING ||
m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING)
return;
if (idx >= m_ModData.size())
return;
const Paths paths(g_args);
const OsPath modUserPath = paths.UserData()/"mods";
const OsPath modPath = modUserPath/m_ModData[idx].properties["name_id"];
if (!DirectoryExists(modPath) && INFO::OK != CreateDirectories(modPath, 0700, false))
{
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING;
m_DownloadProgressData.error = fmt::sprintf(
g_L10n.Translate("Could not create mod directory: %s."), modPath.string8());
return;
}
// Name the file after the name_id, since using the filename would mean that
// we could end up with multiple zip files in the folder that might not work
// as expected for a user (since a later version might remove some files
// that aren't compatible anymore with the engine version).
// So we ignore the filename provided by the API and assume that we do not
// care about handling update.zip files. If that is the case we would need
// a way to find out what files are required by the current one and which
// should be removed for everything to work. This seems to be too complicated
// so we just do not support that usage.
// NOTE: We do save the file under a slightly different name from the final
// one, to ensure that in case a download aborts and the file stays
// around, the game will not attempt to open the file which has not
// been verified.
m_DownloadFilePath = modPath/(m_ModData[idx].properties["name_id"]+".zip.temp");
delete m_CallbackData;
m_CallbackData = new DownloadCallbackData(sys_OpenFile(m_DownloadFilePath, "wb"));
if (!m_CallbackData->fp)
{
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING;
m_DownloadProgressData.error = fmt::sprintf(
g_L10n.Translate("Could not open temporary file for mod download: %s."), m_DownloadFilePath.string8());
return;
}
CURLMcode err = SetupRequest(m_ModData[idx].properties["binary_url"], true);
if (err != CURLM_OK)
{
TearDownRequest();
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING;
m_DownloadProgressData.error = fmt::sprintf(
g_L10n.Translate("Failed to start the download. Error: %s; %s."), curl_multi_strerror(err), m_ErrorBuffer);
return;
}
m_DownloadModID = idx;
m_DownloadProgressData.status = DownloadProgressStatus::DOWNLOADING;
}
void ModIo::CancelRequest()
{
TearDownRequest();
switch (m_DownloadProgressData.status)
{
case DownloadProgressStatus::GAMEID:
case DownloadProgressStatus::FAILED_GAMEID:
m_DownloadProgressData.status = DownloadProgressStatus::NONE;
break;
case DownloadProgressStatus::LISTING:
case DownloadProgressStatus::FAILED_LISTING:
m_DownloadProgressData.status = DownloadProgressStatus::READY;
break;
case DownloadProgressStatus::DOWNLOADING:
case DownloadProgressStatus::FAILED_DOWNLOADING:
m_DownloadProgressData.status = DownloadProgressStatus::LISTED;
DeleteDownloadedFile();
break;
default:
break;
}
}
bool ModIo::AdvanceRequest(const ScriptInterface& scriptInterface)
{
// If the request was cancelled, stop trying to advance it
if (m_DownloadProgressData.status != DownloadProgressStatus::GAMEID &&
m_DownloadProgressData.status != DownloadProgressStatus::LISTING &&
m_DownloadProgressData.status != DownloadProgressStatus::DOWNLOADING)
return true;
int stillRunning;
CURLMcode err = curl_multi_perform(m_CurlMulti, &stillRunning);
if (err != CURLM_OK)
{
std::string error = fmt::sprintf(
g_L10n.Translate("Asynchronous download failure: %s, %s."), curl_multi_strerror(err), m_ErrorBuffer);
TearDownRequest();
if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID)
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_GAMEID;
else if (m_DownloadProgressData.status == DownloadProgressStatus::LISTING)
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_LISTING;
else if (m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING)
{
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING;
DeleteDownloadedFile();
}
m_DownloadProgressData.error = error;
return true;
}
CURLMsg* message;
do
{
int in_queue;
message = curl_multi_info_read(m_CurlMulti, &in_queue);
if (!message)
continue;
CURLcode err = message->data.result;
if (err == CURLE_OK)
continue;
std::string error = fmt::sprintf(
g_L10n.Translate("Download failure. Server response: %s; %s."), curl_easy_strerror(err), m_ErrorBuffer);
TearDownRequest();
if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID)
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_GAMEID;
else if (m_DownloadProgressData.status == DownloadProgressStatus::LISTING)
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_LISTING;
else if (m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING)
{
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING;
DeleteDownloadedFile();
}
m_DownloadProgressData.error = error;
return true;
} while (message);
if (stillRunning)
return false;
// Download finished.
TearDownRequest();
// Perform parsing and/or checks
std::string error;
switch (m_DownloadProgressData.status)
{
case DownloadProgressStatus::GAMEID:
if (!ParseGameId(scriptInterface, error))
{
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_GAMEID;
m_DownloadProgressData.error = error;
break;
}
m_DownloadProgressData.status = DownloadProgressStatus::READY;
break;
case DownloadProgressStatus::LISTING:
if (!ParseMods(scriptInterface, error))
{
m_ModData.clear(); // Failed during parsing, make sure we don't provide partial data
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_LISTING;
m_DownloadProgressData.error = error;
break;
}
m_DownloadProgressData.status = DownloadProgressStatus::LISTED;
break;
case DownloadProgressStatus::DOWNLOADING:
if (!VerifyDownloadedFile(error))
{
m_DownloadProgressData.status = DownloadProgressStatus::FAILED_FILECHECK;
m_DownloadProgressData.error = error;
DeleteDownloadedFile();
break;
}
m_DownloadProgressData.status = DownloadProgressStatus::SUCCESS;
{
Paths paths(g_args);
CModInstaller installer(paths.UserData() / "mods", paths.Cache());
installer.Install(m_DownloadFilePath, g_ScriptRuntime, false);
}
break;
default:
break;
}
return true;
}
bool ModIo::ParseGameId(const ScriptInterface& scriptInterface, std::string& err)
{
int id = -1;
bool ret = ParseGameIdResponse(scriptInterface, m_ResponseData, id, err);
m_ResponseData.clear();
if (!ret)
return false;
m_GameId = "/" + std::to_string(id);
return true;
}
bool ModIo::ParseMods(const ScriptInterface& scriptInterface, std::string& err)
{
bool ret = ParseModsResponse(scriptInterface, m_ResponseData, m_ModData, m_pk, err);
m_ResponseData.clear();
return ret;
}
void ModIo::DeleteDownloadedFile()
{
if (wunlink(m_DownloadFilePath) != 0)
LOGERROR("Failed to delete temporary file.");
m_DownloadFilePath = OsPath();
}
bool ModIo::VerifyDownloadedFile(std::string& err)
{
// Verify filesize, as a first basic download check.
{
u64 filesize = std::stoull(m_ModData[m_DownloadModID].properties.at("filesize"));
if (filesize != FileSize(m_DownloadFilePath))
{
err = g_L10n.Translate("Mismatched filesize.");
return false;
}
}
ENSURE(m_CallbackData);
// MD5 (because upstream provides it)
// Just used to make sure there was no obvious corruption during transfer.
{
u8 digest[MD5::DIGESTSIZE];
m_CallbackData->md5.Final(digest);
std::string md5digest = Hexify(digest, MD5::DIGESTSIZE);
if (m_ModData[m_DownloadModID].properties.at("filehash_md5") != md5digest)
{
err = fmt::sprintf(
g_L10n.Translate("Invalid file. Expected md5 %s, got %s."),
m_ModData[m_DownloadModID].properties.at("filehash_md5").c_str(),
md5digest);
return false;
}
}
// Verify file signature.
// Used to make sure that the downloaded file was actually checked and signed
// by Wildfire Games. And has not been tampered with by the API provider, or the CDN.
unsigned char hash_fin[crypto_generichash_BYTES_MAX] = {};
ENSURE(m_CallbackData->hash_state);
if (crypto_generichash_final(m_CallbackData->hash_state, hash_fin, sizeof hash_fin) != 0)
{
err = g_L10n.Translate("Failed to compute final hash.");
return false;
}
if (crypto_sign_verify_detached(m_ModData[m_DownloadModID].sig.sig, hash_fin, sizeof hash_fin, m_pk.pk) != 0)
{
err = g_L10n.Translate("Failed to verify signature.");
return false;
}
return true;
}
#define FAIL(...) STMT(err = fmt::sprintf(__VA_ARGS__); CLEANUP(); return false;)
/**
* Parses the current content of m_ResponseData to extract m_GameId.
*
* The JSON data is expected to look like
* { "data": [{"id": 42, ...}, ...], ... }
* where we are only interested in the value of the id property.
*
* @returns true iff it successfully parsed the id.
*/
bool ModIo::ParseGameIdResponse(const ScriptInterface& scriptInterface, const std::string& responseData, int& id, std::string& err)
{
#define CLEANUP() id = -1;
- JSContext* cx = scriptInterface.GetContext();
- JSAutoRequest rq(cx);
+ ScriptInterface::Request rq(scriptInterface);
- JS::RootedValue gameResponse(cx);
+ JS::RootedValue gameResponse(rq.cx);
if (!scriptInterface.ParseJSON(responseData, &gameResponse))
FAIL("Failed to parse response as JSON.");
if (!gameResponse.isObject())
FAIL("response not an object.");
- JS::RootedObject gameResponseObj(cx, gameResponse.toObjectOrNull());
- JS::RootedValue dataVal(cx);
- if (!JS_GetProperty(cx, gameResponseObj, "data", &dataVal))
+ JS::RootedObject gameResponseObj(rq.cx, gameResponse.toObjectOrNull());
+ JS::RootedValue dataVal(rq.cx);
+ if (!JS_GetProperty(rq.cx, gameResponseObj, "data", &dataVal))
FAIL("data property not in response.");
// [{"id": 42, ...}, ...]
if (!dataVal.isObject())
FAIL("data property not an object.");
- JS::RootedObject data(cx, dataVal.toObjectOrNull());
+ JS::RootedObject data(rq.cx, dataVal.toObjectOrNull());
u32 length;
bool isArray;
- if (!JS_IsArrayObject(cx, data, &isArray) || !isArray || !JS_GetArrayLength(cx, data, &length) || !length)
+ if (!JS_IsArrayObject(rq.cx, data, &isArray) || !isArray || !JS_GetArrayLength(rq.cx, data, &length) || !length)
FAIL("data property not an array with at least one element.");
// {"id": 42, ...}
- JS::RootedValue first(cx);
- if (!JS_GetElement(cx, data, 0, &first))
+ JS::RootedValue first(rq.cx);
+ if (!JS_GetElement(rq.cx, data, 0, &first))
FAIL("Couldn't get first element.");
if (!first.isObject())
FAIL("First element not an object.");
- JS::RootedObject firstObj(cx, &first.toObject());
+ JS::RootedObject firstObj(rq.cx, &first.toObject());
bool hasIdProperty;
- if (!JS_HasProperty(cx, firstObj, "id", &hasIdProperty) || !hasIdProperty)
+ if (!JS_HasProperty(rq.cx, firstObj, "id", &hasIdProperty) || !hasIdProperty)
FAIL("No id property in first element.");
- JS::RootedValue idProperty(cx);
- ENSURE(JS_GetProperty(cx, firstObj, "id", &idProperty));
+ JS::RootedValue idProperty(rq.cx);
+ ENSURE(JS_GetProperty(rq.cx, firstObj, "id", &idProperty));
// Make sure the property is not set to something that could be converted to a bogus value
// TODO: We should be able to convert JS::Values to C++ variables in a way that actually
// fails when types do not match (see https://trac.wildfiregames.com/ticket/5128).
if (!idProperty.isNumber())
FAIL("id property not a number.");
id = -1;
- if (!ScriptInterface::FromJSVal(cx, idProperty, id) || id <= 0)
+ if (!ScriptInterface::FromJSVal(rq, idProperty, id) || id <= 0)
FAIL("Invalid id.");
return true;
#undef CLEANUP
}
/**
* Parses the current content of m_ResponseData into m_ModData.
*
* The JSON data is expected to look like
* { data: [modobj1, modobj2, ...], ... (including result_count) }
* where modobjN has the following structure
* { homepage_url: "url", name: "displayname", nameid: "short-non-whitespace-name",
* summary: "short desc.", modfile: { version: "1.2.4", filename: "asdf.zip",
* filehash: { md5: "deadbeef" }, filesize: 1234, download: { binary_url: "someurl", ... } }, ... }.
* Only the listed properties are of interest to consumers, and we flatten
* the modfile structure as that simplifies handling and there are no conflicts.
*/
bool ModIo::ParseModsResponse(const ScriptInterface& scriptInterface, const std::string& responseData, std::vector& modData, const PKStruct& pk, std::string& err)
{
// Make sure we don't end up passing partial results back
#define CLEANUP() modData.clear();
- JSContext* cx = scriptInterface.GetContext();
- JSAutoRequest rq(cx);
+ ScriptInterface::Request rq(scriptInterface);
- JS::RootedValue modResponse(cx);
+ JS::RootedValue modResponse(rq.cx);
if (!scriptInterface.ParseJSON(responseData, &modResponse))
FAIL("Failed to parse response as JSON.");
if (!modResponse.isObject())
FAIL("response not an object.");
- JS::RootedObject modResponseObj(cx, modResponse.toObjectOrNull());
- JS::RootedValue dataVal(cx);
- if (!JS_GetProperty(cx, modResponseObj, "data", &dataVal))
+ JS::RootedObject modResponseObj(rq.cx, modResponse.toObjectOrNull());
+ JS::RootedValue dataVal(rq.cx);
+ if (!JS_GetProperty(rq.cx, modResponseObj, "data", &dataVal))
FAIL("data property not in response.");
// [modobj1, modobj2, ... ]
if (!dataVal.isObject())
FAIL("data property not an object.");
- JS::RootedObject rData(cx, dataVal.toObjectOrNull());
+ JS::RootedObject rData(rq.cx, dataVal.toObjectOrNull());
u32 length;
bool isArray;
- if (!JS_IsArrayObject(cx, rData, &isArray) || !isArray || !JS_GetArrayLength(cx, rData, &length) || !length)
+ if (!JS_IsArrayObject(rq.cx, rData, &isArray) || !isArray || !JS_GetArrayLength(rq.cx, rData, &length) || !length)
FAIL("data property not an array with at least one element.");
modData.clear();
modData.reserve(length);
#define INVALIDATE_DATA_AND_CONTINUE(...) \
{\
data.properties.emplace("invalid", "true");\
data.properties.emplace("error", __VA_ARGS__);\
continue;\
}
for (u32 i = 0; i < length; ++i)
{
modData.emplace_back();
ModIoModData& data = modData.back();
- JS::RootedValue el(cx);
- if (!JS_GetElement(cx, rData, i, &el) || !el.isObject())
+ JS::RootedValue el(rq.cx);
+ if (!JS_GetElement(rq.cx, rData, i, &el) || !el.isObject())
INVALIDATE_DATA_AND_CONTINUE("Failed to get array element object.")
bool ok = true;
std::string copyStringError;
#define COPY_STRINGS_ELSE_CONTINUE(prefix, obj, ...) \
for (const std::string& prop : { __VA_ARGS__ }) \
{ \
std::string val; \
- if (!ScriptInterface::FromJSProperty(cx, obj, prop.c_str(), val, true)) \
+ if (!ScriptInterface::FromJSProperty(rq, obj, prop.c_str(), val, true)) \
{ \
ok = false; \
copyStringError = "Failed to get " + prop + " from " + #obj + "."; \
break; \
}\
data.properties.emplace(prefix+prop, val); \
} \
if (!ok) \
INVALIDATE_DATA_AND_CONTINUE(copyStringError);
// TODO: Currently the homepage_url field does not contain a non-null value for any entry.
COPY_STRINGS_ELSE_CONTINUE("", el, "name", "name_id", "summary")
// Now copy over the modfile part, but without the pointless substructure
- JS::RootedObject elObj(cx, el.toObjectOrNull());
- JS::RootedValue modFile(cx);
- if (!JS_GetProperty(cx, elObj, "modfile", &modFile))
+ JS::RootedObject elObj(rq.cx, el.toObjectOrNull());
+ JS::RootedValue modFile(rq.cx);
+ if (!JS_GetProperty(rq.cx, elObj, "modfile", &modFile))
INVALIDATE_DATA_AND_CONTINUE("Failed to get modfile data.");
if (!modFile.isObject())
INVALIDATE_DATA_AND_CONTINUE("modfile not an object.");
COPY_STRINGS_ELSE_CONTINUE("", modFile, "version", "filesize");
- JS::RootedObject modFileObj(cx, modFile.toObjectOrNull());
- JS::RootedValue filehash(cx);
- if (!JS_GetProperty(cx, modFileObj, "filehash", &filehash))
+ JS::RootedObject modFileObj(rq.cx, modFile.toObjectOrNull());
+ JS::RootedValue filehash(rq.cx);
+ if (!JS_GetProperty(rq.cx, modFileObj, "filehash", &filehash))
INVALIDATE_DATA_AND_CONTINUE("Failed to get filehash data.");
COPY_STRINGS_ELSE_CONTINUE("filehash_", filehash, "md5");
- JS::RootedValue download(cx);
- if (!JS_GetProperty(cx, modFileObj, "download", &download))
+ JS::RootedValue download(rq.cx);
+ if (!JS_GetProperty(rq.cx, modFileObj, "download", &download))
INVALIDATE_DATA_AND_CONTINUE("Failed to get download data.");
COPY_STRINGS_ELSE_CONTINUE("", download, "binary_url");
// Parse metadata_blob (sig+deps)
std::string metadata_blob;
- if (!ScriptInterface::FromJSProperty(cx, modFile, "metadata_blob", metadata_blob, true))
+ if (!ScriptInterface::FromJSProperty(rq, modFile, "metadata_blob", metadata_blob, true))
INVALIDATE_DATA_AND_CONTINUE("Failed to get metadata_blob from modFile.");
- JS::RootedValue metadata(cx);
+ JS::RootedValue metadata(rq.cx);
if (!scriptInterface.ParseJSON(metadata_blob, &metadata))
INVALIDATE_DATA_AND_CONTINUE("Failed to parse metadata_blob as JSON.");
if (!metadata.isObject())
INVALIDATE_DATA_AND_CONTINUE("metadata_blob is not decoded as an object.");
- if (!ScriptInterface::FromJSProperty(cx, metadata, "dependencies", data.dependencies, true))
+ if (!ScriptInterface::FromJSProperty(rq, metadata, "dependencies", data.dependencies, true))
INVALIDATE_DATA_AND_CONTINUE("Failed to get dependencies from metadata_blob.");
std::vector minisigs;
- if (!ScriptInterface::FromJSProperty(cx, metadata, "minisigs", minisigs, true))
+ if (!ScriptInterface::FromJSProperty(rq, metadata, "minisigs", minisigs, true))
INVALIDATE_DATA_AND_CONTINUE("Failed to get minisigs from metadata_blob.");
// Check we did find a valid matching signature.
std::string signatureParsingErr;
if (!ParseSignature(minisigs, data.sig, pk, signatureParsingErr))
INVALIDATE_DATA_AND_CONTINUE(signatureParsingErr);
#undef COPY_STRINGS_ELSE_CONTINUE
#undef INVALIDATE_DATA_AND_CONTINUE
}
return true;
#undef CLEANUP
}
/**
* Parse signatures to find one that matches the public key, and has a valid global signature.
* Returns true and sets @param sig to the valid matching signature.
*/
bool ModIo::ParseSignature(const std::vector& minisigs, SigStruct& sig, const PKStruct& pk, std::string& err)
{
#define CLEANUP() sig = {};
for (const std::string& file_sig : minisigs)
{
// Format of a .minisig file (created using minisign(1) with -SHm file.zip)
// untrusted comment: .*\nb64sign_of_file\ntrusted comment: .*\nb64sign_of_sign_of_file_and_trusted_comment
std::vector sig_lines;
boost::split(sig_lines, file_sig, boost::is_any_of("\n"));
if (sig_lines.size() < 4)
FAIL("Invalid (too short) sig.");
// Verify that both the untrusted comment and the trusted comment start with the correct prefix
// because that is easy.
const std::string untrusted_comment_prefix = "untrusted comment: ";
const std::string trusted_comment_prefix = "trusted comment: ";
if (!boost::algorithm::starts_with(sig_lines[0], untrusted_comment_prefix))
FAIL("Malformed untrusted comment.");
if (!boost::algorithm::starts_with(sig_lines[2], trusted_comment_prefix))
FAIL("Malformed trusted comment.");
// We only _really_ care about the second line which is the signature of the file (b64-encoded)
// Also handling the other signature is nice, but not really required.
const std::string& msg_sig = sig_lines[1];
size_t bin_len = 0;
if (sodium_base642bin((unsigned char*)&sig, sizeof sig, msg_sig.c_str(), msg_sig.size(), NULL, &bin_len, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || bin_len != sizeof sig)
FAIL("Failed to decode base64 sig.");
cassert(sizeof pk.keynum == sizeof sig.keynum);
if (memcmp(&pk.keynum, &sig.keynum, sizeof sig.keynum) != 0)
continue; // mismatched key, try another one
if (memcmp(&sig.sig_alg, "ED", 2) != 0)
FAIL("Only hashed minisign signatures are supported.");
// Signature matches our public key
// Now verify the global signature (sig || trusted_comment)
unsigned char global_sig[crypto_sign_BYTES];
if (sodium_base642bin(global_sig, sizeof global_sig, sig_lines[3].c_str(), sig_lines[3].size(), NULL, &bin_len, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || bin_len != sizeof global_sig)
FAIL("Failed to decode base64 global_sig.");
const std::string trusted_comment = sig_lines[2].substr(trusted_comment_prefix.size());
unsigned char* sig_and_trusted_comment = (unsigned char*)sodium_malloc((sizeof sig.sig) + trusted_comment.size());
if (!sig_and_trusted_comment)
FAIL("sodium_malloc failed.");
memcpy(sig_and_trusted_comment, sig.sig, sizeof sig.sig);
memcpy(sig_and_trusted_comment + sizeof sig.sig, trusted_comment.data(), trusted_comment.size());
if (crypto_sign_verify_detached(global_sig, sig_and_trusted_comment, (sizeof sig.sig) + trusted_comment.size(), pk.pk) != 0)
{
err = "Failed to verify global signature.";
sodium_free(sig_and_trusted_comment);
return false;
}
sodium_free(sig_and_trusted_comment);
// Valid global sig, and the keynum matches the real one
return true;
}
return false;
#undef CLEANUP
}
#undef FAIL
Index: ps/trunk/source/ps/SavedGame.cpp
===================================================================
--- ps/trunk/source/ps/SavedGame.cpp (revision 24175)
+++ ps/trunk/source/ps/SavedGame.cpp (revision 24176)
@@ -1,305 +1,300 @@
-/* Copyright (C) 2019 Wildfire Games.
+/* 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 "SavedGame.h"
#include "graphics/GameView.h"
#include "i18n/L10n.h"
#include "lib/allocators/shared_ptr.h"
#include "lib/file/archive/archive_zip.h"
#include "lib/utf8.h"
#include "maths/Vector3D.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/Mod.h"
#include "ps/Pyrogenesis.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
// TODO: we ought to check version numbers when loading files
Status SavedGames::SavePrefix(const CStrW& prefix, const CStrW& description, CSimulation2& simulation, const shared_ptr& guiMetadataClone)
{
// Determine the filename to save under
const VfsPath basenameFormat(L"saves/" + prefix + L"-%04d");
const VfsPath filenameFormat = basenameFormat.ChangeExtension(L".0adsave");
VfsPath filename;
// Don't make this a static global like NextNumberedFilename expects, because
// that wouldn't work when 'prefix' changes, and because it's not thread-safe
size_t nextSaveNumber = 0;
vfs::NextNumberedFilename(g_VFS, filenameFormat, nextSaveNumber, filename);
return Save(filename.Filename().string(), description, simulation, guiMetadataClone);
}
Status SavedGames::Save(const CStrW& name, const CStrW& description, CSimulation2& simulation, const shared_ptr& guiMetadataClone)
{
- JSContext* cx = simulation.GetScriptInterface().GetContext();
- JSAutoRequest rq(cx);
+ ScriptInterface::Request rq(simulation.GetScriptInterface());
// Determine the filename to save under
const VfsPath basenameFormat(L"saves/" + name);
const VfsPath filename = basenameFormat.ChangeExtension(L".0adsave");
// ArchiveWriter_Zip can only write to OsPaths, not VfsPaths,
// but we'd like to handle saved games via VFS.
// To avoid potential confusion from writing with non-VFS then
// reading the same file with VFS, we'll just write to a temporary
// non-VFS path and then load and save again via VFS,
// which is kind of a hack.
OsPath tempSaveFileRealPath;
WARN_RETURN_STATUS_IF_ERR(g_VFS->GetDirectoryRealPath("cache/", tempSaveFileRealPath));
tempSaveFileRealPath = tempSaveFileRealPath / "temp.0adsave";
time_t now = time(NULL);
// Construct the serialized state to be saved
std::stringstream simStateStream;
if (!simulation.SerializeState(simStateStream))
WARN_RETURN(ERR::FAIL);
- JS::RootedValue initAttributes(cx, simulation.GetInitAttributes());
- JS::RootedValue mods(cx, Mod::GetLoadedModsWithVersions(simulation.GetScriptInterface()));
+ JS::RootedValue initAttributes(rq.cx, simulation.GetInitAttributes());
+ JS::RootedValue mods(rq.cx, Mod::GetLoadedModsWithVersions(simulation.GetScriptInterface()));
- JS::RootedValue metadata(cx);
+ JS::RootedValue metadata(rq.cx);
ScriptInterface::CreateObject(
- cx,
+ rq,
&metadata,
"engine_version", engine_version,
"time", static_cast(now),
"playerID", g_Game->GetPlayerID(),
"mods", mods,
"initAttributes", initAttributes);
- JS::RootedValue guiMetadata(cx);
+ JS::RootedValue guiMetadata(rq.cx);
simulation.GetScriptInterface().ReadStructuredClone(guiMetadataClone, &guiMetadata);
// get some camera data
const CVector3D cameraPosition = g_Game->GetView()->GetCameraPosition();
const CVector3D cameraRotation = g_Game->GetView()->GetCameraRotation();
- JS::RootedValue cameraMetadata(cx);
+ JS::RootedValue cameraMetadata(rq.cx);
ScriptInterface::CreateObject(
- cx,
+ rq,
&cameraMetadata,
"PosX", cameraPosition.X,
"PosY", cameraPosition.Y,
"PosZ", cameraPosition.Z,
"RotX", cameraRotation.X,
"RotY", cameraRotation.Y,
"Zoom", g_Game->GetView()->GetCameraZoom());
simulation.GetScriptInterface().SetProperty(guiMetadata, "camera", cameraMetadata);
simulation.GetScriptInterface().SetProperty(metadata, "gui", guiMetadata);
simulation.GetScriptInterface().SetProperty(metadata, "description", description);
std::string metadataString = simulation.GetScriptInterface().StringifyJSON(&metadata, true);
// Write the saved game as zip file containing the various components
PIArchiveWriter archiveWriter = CreateArchiveWriter_Zip(tempSaveFileRealPath, false);
if (!archiveWriter)
WARN_RETURN(ERR::FAIL);
WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)metadataString.c_str(), metadataString.length(), now, "metadata.json"));
WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)simStateStream.str().c_str(), simStateStream.str().length(), now, "simulation.dat"));
archiveWriter.reset(); // close the file
WriteBuffer buffer;
CFileInfo tempSaveFile;
WARN_RETURN_STATUS_IF_ERR(GetFileInfo(tempSaveFileRealPath, &tempSaveFile));
buffer.Reserve(tempSaveFile.Size());
WARN_RETURN_STATUS_IF_ERR(io::Load(tempSaveFileRealPath, buffer.Data().get(), buffer.Size()));
WARN_RETURN_STATUS_IF_ERR(g_VFS->CreateFile(filename, buffer.Data(), buffer.Size()));
OsPath realPath;
WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath));
LOGMESSAGERENDER(g_L10n.Translate("Saved game to '%s'"), realPath.string8());
debug_printf("Saved game to '%s'\n", realPath.string8().c_str());
return INFO::OK;
}
/**
* Helper class for retrieving data from saved game archives
*/
class CGameLoader
{
NONCOPYABLE(CGameLoader);
public:
/**
* @param scriptInterface the ScriptInterface used for loading metadata.
* @param[out] savedState serialized simulation state stored as string of bytes,
* loaded from simulation.dat inside the archive.
*
* Note: We use a different approach for returning the string and the metadata JS::Value.
* We use a pointer for the string to avoid copies (efficiency). We don't use this approach
* for the metadata because it would be error prone with rooting and the stack-based rooting
* types and confusing (a chain of pointers pointing to other pointers).
*/
CGameLoader(const ScriptInterface& scriptInterface, std::string* savedState) :
m_ScriptInterface(scriptInterface),
m_Metadata(scriptInterface.GetJSRuntime()),
m_SavedState(savedState)
{
}
static void ReadEntryCallback(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile, uintptr_t cbData)
{
((CGameLoader*)cbData)->ReadEntry(pathname, fileInfo, archiveFile);
}
void ReadEntry(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile)
{
- JSContext* cx = m_ScriptInterface.GetContext();
- JSAutoRequest rq(cx);
-
if (pathname == L"metadata.json")
{
std::string buffer;
buffer.resize(fileInfo.Size());
WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)buffer.data()), buffer.size()));
m_ScriptInterface.ParseJSON(buffer, &m_Metadata);
}
else if (pathname == L"simulation.dat" && m_SavedState)
{
m_SavedState->resize(fileInfo.Size());
WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)m_SavedState->data()), m_SavedState->size()));
}
}
JS::Value GetMetadata()
{
return m_Metadata.get();
}
private:
const ScriptInterface& m_ScriptInterface;
JS::PersistentRooted m_Metadata;
std::string* m_SavedState;
};
Status SavedGames::Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState)
{
// Determine the filename to load
const VfsPath basename(L"saves/" + name);
const VfsPath filename = basename.ChangeExtension(L".0adsave");
// Don't crash just because file isn't found, this can happen if the file is deleted from the OS
if (!VfsFileExists(filename))
return ERR::FILE_NOT_FOUND;
OsPath realPath;
WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath));
PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath);
if (!archiveReader)
WARN_RETURN(ERR::FAIL);
CGameLoader loader(scriptInterface, &savedState);
WARN_RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader));
metadata.set(loader.GetMetadata());
return INFO::OK;
}
JS::Value SavedGames::GetSavedGames(const ScriptInterface& scriptInterface)
{
TIMER(L"GetSavedGames");
- JSContext* cx = scriptInterface.GetContext();
- JSAutoRequest rq(cx);
+ ScriptInterface::Request rq(scriptInterface);
- JS::RootedValue games(cx);
- ScriptInterface::CreateArray(cx, &games);
+ JS::RootedValue games(rq.cx);
+ ScriptInterface::CreateArray(rq, &games);
Status err;
VfsPaths pathnames;
err = vfs::GetPathnames(g_VFS, "saves/", L"*.0adsave", pathnames);
WARN_IF_ERR(err);
for (size_t i = 0; i < pathnames.size(); ++i)
{
OsPath realPath;
err = g_VFS->GetRealPath(pathnames[i], realPath);
if (err < 0)
{
DEBUG_WARN_ERR(err);
continue; // skip this file
}
PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath);
if (!archiveReader)
{
// Triggered by e.g. the file being open in another program
LOGWARNING("Failed to read saved game '%s'", realPath.string8());
continue; // skip this file
}
CGameLoader loader(scriptInterface, NULL);
err = archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader);
if (err < 0)
{
DEBUG_WARN_ERR(err);
continue; // skip this file
}
- JS::RootedValue metadata(cx, loader.GetMetadata());
+ JS::RootedValue metadata(rq.cx, loader.GetMetadata());
- JS::RootedValue game(cx);
+ JS::RootedValue game(rq.cx);
ScriptInterface::CreateObject(
- cx,
+ rq,
&game,
"id", pathnames[i].Basename(),
"metadata", metadata);
scriptInterface.SetPropertyInt(games, i, game);
}
return games;
}
bool SavedGames::DeleteSavedGame(const std::wstring& name)
{
const VfsPath basename(L"saves/" + name);
const VfsPath filename = basename.ChangeExtension(L".0adsave");
OsPath realpath;
// Make sure it exists in VFS and find its real path
if (!VfsFileExists(filename) || g_VFS->GetRealPath(filename, realpath) != INFO::OK)
return false; // Error
// Remove from VFS
if (g_VFS->RemoveFile(filename) != INFO::OK)
return false; // Error
// Delete actual file
if (wunlink(realpath) != 0)
return false; // Error
// Successfully deleted file
return true;
}
Index: ps/trunk/source/ps/scripting/JSInterface_Main.cpp
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_Main.cpp (revision 24175)
+++ ps/trunk/source/ps/scripting/JSInterface_Main.cpp (revision 24176)
@@ -1,139 +1,138 @@
-/* Copyright (C) 2018 Wildfire Games.
+/* 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 "JSInterface_Main.h"
#include "graphics/FontMetrics.h"
#include "graphics/MapReader.h"
#include "lib/sysdep/sysdep.h"
#include "lib/utf8.h"
#include "maths/MD5.h"
#include "ps/CStrIntern.h"
#include "ps/GUID.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Util.h"
#include "scriptinterface/ScriptInterface.h"
#include "tools/atlas/GameInterface/GameLoop.h"
extern void QuitEngine();
extern void StartAtlas();
void JSI_Main::QuitEngine(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
::QuitEngine();
}
void JSI_Main::StartAtlas(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
::StartAtlas();
}
bool JSI_Main::AtlasIsAvailable(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return ATLAS_IsAvailable();
}
bool JSI_Main::IsAtlasRunning(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_AtlasGameLoop && g_AtlasGameLoop->running;
}
void JSI_Main::OpenURL(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& url)
{
sys_open_url(url);
}
std::wstring JSI_Main::GetSystemUsername(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return sys_get_user_name();
}
std::wstring JSI_Main::GetMatchID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return ps_generate_guid().FromUTF8();
}
JS::Value JSI_Main::LoadMapSettings(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& pathname)
{
- JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
- JSAutoRequest rq(cx);
+ ScriptInterface::Request rq(pCxPrivate);
CMapSummaryReader reader;
if (reader.LoadMap(pathname) != PSRETURN_OK)
return JS::UndefinedValue();
- JS::RootedValue settings(cx);
+ JS::RootedValue settings(rq.cx);
reader.GetMapSettings(*(pCxPrivate->pScriptInterface), &settings);
return settings;
}
bool JSI_Main::HotkeyIsPressed_(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& hotkeyName)
{
return HotkeyIsPressed(hotkeyName);
}
// This value is recalculated once a frame. We take special care to
// filter it, so it is both accurate and free of jitter.
int JSI_Main::GetFps(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (!g_frequencyFilter)
return 0;
return g_frequencyFilter->StableFrequency();
}
int JSI_Main::GetTextWidth(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& fontName, const std::wstring& text)
{
int width = 0;
int height = 0;
CStrIntern _fontName(fontName);
CFontMetrics fontMetrics(_fontName);
fontMetrics.CalculateStringSize(text.c_str(), width, height);
return width;
}
std::string JSI_Main::CalculateMD5(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& input)
{
u8 digest[MD5::DIGESTSIZE];
MD5 m;
m.Update((const u8*)input.c_str(), input.length());
m.Final(digest);
return Hexify(digest, MD5::DIGESTSIZE);
}
void JSI_Main::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("Exit");
scriptInterface.RegisterFunction("RestartInAtlas");
scriptInterface.RegisterFunction("AtlasIsAvailable");
scriptInterface.RegisterFunction("IsAtlasRunning");
scriptInterface.RegisterFunction("OpenURL");
scriptInterface.RegisterFunction("GetSystemUsername");
scriptInterface.RegisterFunction("GetMatchID");
scriptInterface.RegisterFunction("LoadMapSettings");
scriptInterface.RegisterFunction("HotkeyIsPressed");
scriptInterface.RegisterFunction("GetFPS");
scriptInterface.RegisterFunction("GetTextWidth");
scriptInterface.RegisterFunction("CalculateMD5");
}
Index: ps/trunk/source/ps/scripting/JSInterface_VFS.cpp
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_VFS.cpp (revision 24175)
+++ ps/trunk/source/ps/scripting/JSInterface_VFS.cpp (revision 24176)
@@ -1,280 +1,275 @@
/* 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 "JSInterface_VFS.h"
#include "lib/file/vfs/vfs_util.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/Filesystem.h"
#include "scriptinterface/ScriptInterface.h"
#include
// Only allow engine compartments to read files they may be concerned about.
#define PathRestriction_GUI {L""}
#define PathRestriction_Simulation {L"simulation/"}
#define PathRestriction_Maps {L"simulation/", L"maps/"}
// shared error handling code
#define JS_CHECK_FILE_ERR(err)\
/* this is liable to happen often, so don't complain */\
if (err == ERR::VFS_FILE_NOT_FOUND)\
{\
return 0; \
}\
/* unknown failure. We output an error message. */\
else if (err < 0)\
LOGERROR("Unknown failure in VFS %i", err );
/* else: success */
// state held across multiple BuildDirEntListCB calls; init by BuildDirEntList.
struct BuildDirEntListState
{
- JSContext* cx;
+ ScriptInterface* pScriptInterface;
JS::PersistentRootedObject filename_array;
int cur_idx;
- BuildDirEntListState(JSContext* cx_)
- : cx(cx_),
- filename_array(cx, JS_NewArrayObject(cx, JS::HandleValueArray::empty())),
+ BuildDirEntListState(ScriptInterface* scriptInterface)
+ : pScriptInterface(scriptInterface),
+ filename_array(scriptInterface->GetJSRuntime()),
cur_idx(0)
{
+ ScriptInterface::Request rq(pScriptInterface);
+ filename_array = JS_NewArrayObject(rq.cx, JS::HandleValueArray::empty());
}
};
// called for each matching directory entry; add its full pathname to array.
static Status BuildDirEntListCB(const VfsPath& pathname, const CFileInfo& UNUSED(fileINfo), uintptr_t cbData)
{
BuildDirEntListState* s = (BuildDirEntListState*)cbData;
- JSAutoRequest rq(s->cx);
+ ScriptInterface::Request rq(s->pScriptInterface);
- JS::RootedObject filenameArrayObj(s->cx, s->filename_array);
- JS::RootedValue val(s->cx);
- ScriptInterface::ToJSVal( s->cx, &val, CStrW(pathname.string()) );
- JS_SetElement(s->cx, filenameArrayObj, s->cur_idx++, val);
+ JS::RootedObject filenameArrayObj(rq.cx, s->filename_array);
+ JS::RootedValue val(rq.cx);
+ ScriptInterface::ToJSVal(rq, &val, CStrW(pathname.string()) );
+ JS_SetElement(rq.cx, filenameArrayObj, s->cur_idx++, val);
return INFO::OK;
}
// Return an array of pathname strings, one for each matching entry in the
// specified directory.
// filter_string: default "" matches everything; otherwise, see vfs_next_dirent.
// recurse: should subdirectories be included in the search? default false.
JS::Value JSI_VFS::BuildDirEntList(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const std::wstring& path, const std::wstring& filterStr, bool recurse)
{
if (!PathRestrictionMet(pCxPrivate, validPaths, path))
return JS::NullValue();
// convert to const wchar_t*; if there's no filter, pass 0 for speed
// (interpreted as: "accept all files without comparing").
const wchar_t* filter = 0;
if (!filterStr.empty())
filter = filterStr.c_str();
int flags = recurse ? vfs::DIR_RECURSIVE : 0;
- JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
- JSAutoRequest rq(cx);
-
// build array in the callback function
- BuildDirEntListState state(cx);
+ BuildDirEntListState state(pCxPrivate->pScriptInterface);
vfs::ForEachFile(g_VFS, path, BuildDirEntListCB, (uintptr_t)&state, filter, flags);
return JS::ObjectValue(*state.filename_array);
}
// Return true iff the file exits
bool JSI_VFS::FileExists(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filename)
{
return PathRestrictionMet(pCxPrivate, validPaths, filename) && g_VFS->GetFileInfo(filename, 0) == INFO::OK;
}
// Return time [seconds since 1970] of the last modification to the specified file.
double JSI_VFS::GetFileMTime(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& filename)
{
CFileInfo fileInfo;
Status err = g_VFS->GetFileInfo(filename, &fileInfo);
JS_CHECK_FILE_ERR(err);
return (double)fileInfo.MTime();
}
// Return current size of file.
unsigned int JSI_VFS::GetFileSize(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& filename)
{
CFileInfo fileInfo;
Status err = g_VFS->GetFileInfo(filename, &fileInfo);
JS_CHECK_FILE_ERR(err);
return (unsigned int)fileInfo.Size();
}
// Return file contents in a string. Assume file is UTF-8 encoded text.
JS::Value JSI_VFS::ReadFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename)
{
- JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
- JSAutoRequest rq(cx);
-
CVFSFile file;
if (file.Load(g_VFS, filename) != PSRETURN_OK)
return JS::NullValue();
CStr contents = file.DecodeUTF8(); // assume it's UTF-8
// Fix CRLF line endings. (This function will only ever be used on text files.)
contents.Replace("\r\n", "\n");
// Decode as UTF-8
- JS::RootedValue ret(cx);
- ScriptInterface::ToJSVal(cx, &ret, contents.FromUTF8());
+ ScriptInterface::Request rq(pCxPrivate);
+ JS::RootedValue ret(rq.cx);
+ ScriptInterface::ToJSVal(rq, &ret, contents.FromUTF8());
return ret;
}
// Return file contents as an array of lines. Assume file is UTF-8 encoded text.
JS::Value JSI_VFS::ReadFileLines(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename)
{
- const ScriptInterface& scriptInterface = *pCxPrivate->pScriptInterface;
- JSContext* cx = scriptInterface.GetContext();
- JSAutoRequest rq(cx);
-
CVFSFile file;
if (file.Load(g_VFS, filename) != PSRETURN_OK)
return JS::NullValue();
CStr contents = file.DecodeUTF8(); // assume it's UTF-8
// Fix CRLF line endings. (This function will only ever be used on text files.)
contents.Replace("\r\n", "\n");
// split into array of strings (one per line)
std::stringstream ss(contents);
- JS::RootedValue line_array(cx);
- ScriptInterface::CreateArray(cx, &line_array);
+ const ScriptInterface& scriptInterface = *pCxPrivate->pScriptInterface;
+ ScriptInterface::Request rq(scriptInterface);
+
+ JS::RootedValue line_array(rq.cx);
+ ScriptInterface::CreateArray(rq, &line_array);
std::string line;
int cur_line = 0;
while (std::getline(ss, line))
{
// Decode each line as UTF-8
- JS::RootedValue val(cx);
- ScriptInterface::ToJSVal(cx, &val, CStr(line).FromUTF8());
+ JS::RootedValue val(rq.cx);
+ ScriptInterface::ToJSVal(rq, &val, CStr(line).FromUTF8());
scriptInterface.SetPropertyInt(line_array, cur_line++, val);
}
return line_array;
}
JS::Value JSI_VFS::ReadJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filePath)
{
if (!PathRestrictionMet(pCxPrivate, validPaths, filePath))
return JS::NullValue();
- JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
- JSAutoRequest rq(cx);
- JS::RootedValue out(cx);
- pCxPrivate->pScriptInterface->ReadJSONFile(filePath, &out);
+ const ScriptInterface& scriptInterface = *pCxPrivate->pScriptInterface;
+ ScriptInterface::Request rq(scriptInterface);
+ JS::RootedValue out(rq.cx);
+ scriptInterface.ReadJSONFile(filePath, &out);
return out;
}
void JSI_VFS::WriteJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath, JS::HandleValue val1)
{
- JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
- JSAutoRequest rq(cx);
+ const ScriptInterface& scriptInterface = *pCxPrivate->pScriptInterface;
+ ScriptInterface::Request rq(scriptInterface);
// TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON.
- JS::RootedValue val(cx, val1);
+ JS::RootedValue val(rq.cx, val1);
- std::string str(pCxPrivate->pScriptInterface->StringifyJSON(&val, false));
+ std::string str(scriptInterface.StringifyJSON(&val, false));
VfsPath path(filePath);
WriteBuffer buf;
buf.Append(str.c_str(), str.length());
g_VFS->CreateFile(path, buf.Data(), buf.Size());
}
bool JSI_VFS::PathRestrictionMet(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filePath)
{
for (const CStrW& validPath : validPaths)
if (filePath.find(validPath) == 0)
return true;
CStrW allowedPaths;
for (std::size_t i = 0; i < validPaths.size(); ++i)
{
if (i != 0)
allowedPaths += L", ";
allowedPaths += L"\"" + validPaths[i] + L"\"";
}
- JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
- JSAutoRequest rq(cx);
- JS_ReportError(cx, "This part of the engine may only read from %s!", utf8_from_wstring(allowedPaths).c_str());
+ ScriptInterface::Request rq(pCxPrivate);
+ JS_ReportError(rq.cx, "This part of the engine may only read from %s!", utf8_from_wstring(allowedPaths).c_str());
return false;
}
#define VFS_ScriptFunctions(context)\
JS::Value Script_ReadJSONFile_##context(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath)\
{\
return JSI_VFS::ReadJSONFile(pCxPrivate, PathRestriction_##context, filePath);\
}\
JS::Value Script_ListDirectoryFiles_##context(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& path, const std::wstring& filterStr, bool recurse)\
{\
return JSI_VFS::BuildDirEntList(pCxPrivate, PathRestriction_##context, path, filterStr, recurse);\
}\
bool Script_FileExists_##context(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath)\
{\
return JSI_VFS::FileExists(pCxPrivate, PathRestriction_##context, filePath);\
}\
VFS_ScriptFunctions(GUI);
VFS_ScriptFunctions(Simulation);
VFS_ScriptFunctions(Maps);
#undef VFS_ScriptFunctions
void JSI_VFS::RegisterScriptFunctions_GUI(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("ListDirectoryFiles");
scriptInterface.RegisterFunction("FileExists");
scriptInterface.RegisterFunction("GetFileMTime");
scriptInterface.RegisterFunction("GetFileSize");
scriptInterface.RegisterFunction("ReadFile");
scriptInterface.RegisterFunction("ReadFileLines");
scriptInterface.RegisterFunction("ReadJSONFile");
scriptInterface.RegisterFunction("WriteJSONFile");
}
void JSI_VFS::RegisterScriptFunctions_Simulation(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("ListDirectoryFiles");
scriptInterface.RegisterFunction("FileExists");
scriptInterface.RegisterFunction("ReadJSONFile");
}
void JSI_VFS::RegisterScriptFunctions_Maps(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("ListDirectoryFiles");
scriptInterface.RegisterFunction("FileExists");
scriptInterface.RegisterFunction("ReadJSONFile");
}
Index: ps/trunk/source/scriptinterface/NativeWrapperDefns.h
===================================================================
--- ps/trunk/source/scriptinterface/NativeWrapperDefns.h (revision 24175)
+++ ps/trunk/source/scriptinterface/NativeWrapperDefns.h (revision 24176)
@@ -1,239 +1,235 @@
/* 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...),
+// ScriptInterface_NativeWrapper::call(rq, 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)
+ static void call(const ScriptInterface::Request& rq, JS::MutableHandleValue rval, F fptr, Ts... params)
{
- ScriptInterface::AssignOrToJSValUnrooted(cx, rval, fptr(ScriptInterface::GetScriptInterfaceAndCBData(cx), params...));
+ ScriptInterface::AssignOrToJSValUnrooted(rq, rval, fptr(ScriptInterface::GetScriptInterfaceAndCBData(rq.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)
+ static void call(const ScriptInterface::Request& rq, JS::MutableHandleValue UNUSED(rval), F fptr, Ts... params)
{
- fptr(ScriptInterface::GetScriptInterfaceAndCBData(cx), params...);
+ fptr(ScriptInterface::GetScriptInterfaceAndCBData(rq.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)
+ static void call(const ScriptInterface::Request& rq, JS::MutableHandleValue rval, TC* c, F fptr, Ts... params)
{
- ScriptInterface::AssignOrToJSValUnrooted(cx, rval, (c->*fptr)(params...));
+ ScriptInterface::AssignOrToJSValUnrooted(rq, 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)
+ static void call(const ScriptInterface::Request& UNUSED(rq), 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); \
+ ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \
BOOST_PP_REPEAT_##z (i, CONVERT_ARG, ~) \
- JS::RootedValue rval(cx); \
- ScriptInterface_NativeWrapper::template call(cx, &rval, fptr A0_TAIL(z,i)); \
+ JS::RootedValue rval(rq.cx); \
+ ScriptInterface_NativeWrapper::template call(rq, &rval, fptr A0_TAIL(z,i)); \
args.rval().set(rval); \
- return !ScriptInterface::IsExceptionPending(cx); \
+ return !ScriptInterface::IsExceptionPending(rq); \
}
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); \
+ ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \
+ TC* c = ScriptInterface::GetPrivate(rq, 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)); \
+ JS::RootedValue rval(rq.cx); \
+ ScriptInterface_NativeMethodWrapper::template call(rq, &rval, c, fptr A0_TAIL(z,i)); \
args.rval().set(rval); \
- return !ScriptInterface::IsExceptionPending(cx); \
+ return !ScriptInterface::IsExceptionPending(rq); \
}
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); \
+ ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \
+ TC* c = ScriptInterface::GetPrivate(rq, 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)); \
+ JS::RootedValue rval(rq.cx); \
+ ScriptInterface_NativeMethodWrapper::template call(rq, &rval, c, fptr A0_TAIL(z,i)); \
args.rval().set(rval); \
- return !ScriptInterface::IsExceptionPending(cx); \
+ return !ScriptInterface::IsExceptionPending(rq); \
}
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)
+static void AssignOrToJSValHelper(const ScriptInterface::Request& rq, JS::AutoValueVector& argv, const T& a, const Ts&... params)
{
- ScriptInterface::AssignOrToJSVal(cx, argv[i], a);
- AssignOrToJSValHelper(cx, argv, params...);
+ ScriptInterface::AssignOrToJSVal(rq, argv[i], a);
+ AssignOrToJSValHelper(rq, argv, params...);
}
template
-static void AssignOrToJSValHelper(JSContext* UNUSED(cx), JS::AutoValueVector& UNUSED(argv))
+static void AssignOrToJSValHelper(const ScriptInterface::Request& UNUSED(rq), 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);
+ ScriptInterface::Request rq(this);
+ JS::RootedValue jsRet(rq.cx);
+ JS::AutoValueVector argv(rq.cx);
argv.resize(sizeof...(Ts));
- AssignOrToJSValHelper<0>(cx, argv, params...);
+ AssignOrToJSValHelper<0>(rq, argv, params...);
if (!CallFunction_(val, name, argv, &jsRet))
return false;
- return FromJSVal(cx, jsRet, ret);
+ return FromJSVal(rq, 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);
+ ScriptInterface::Request rq(this);
JS::MutableHandle jsRet(ret);
- JS::AutoValueVector argv(cx);
+ JS::AutoValueVector argv(rq.cx);
argv.resize(sizeof...(Ts));
- AssignOrToJSValHelper<0>(cx, argv, params...);
+ AssignOrToJSValHelper<0>(rq, 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);
+ ScriptInterface::Request rq(this);
+ JS::AutoValueVector argv(rq.cx);
argv.resize(sizeof...(Ts));
- AssignOrToJSValHelper<0>(cx, argv, params...);
+ AssignOrToJSValHelper<0>(rq, 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);
+ ScriptInterface::Request rq(this);
+ JS::RootedValue jsRet(rq.cx);
+ JS::AutoValueVector argv(rq.cx);
argv.resize(sizeof...(Ts));
- AssignOrToJSValHelper<0>(cx, argv, params...);
+ AssignOrToJSValHelper<0>(rq, 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/graphics/MapGenerator.cpp
===================================================================
--- ps/trunk/source/graphics/MapGenerator.cpp (revision 24175)
+++ ps/trunk/source/graphics/MapGenerator.cpp (revision 24176)
@@ -1,440 +1,437 @@
/* 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: 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 = 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);
+ ScriptInterface::Request rq(m_ScriptInterface);
// Parse settings
- JS::RootedValue settingsVal(cx);
+ JS::RootedValue settingsVal(rq.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());
+ JS::RootedValue global(rq.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