Index: ps/trunk/binaries/data/mods/public/gui/savegame/save.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/savegame/save.js (revision 22921)
+++ ps/trunk/binaries/data/mods/public/gui/savegame/save.js (revision 22922)
@@ -1,91 +1,84 @@
var g_Descriptions;
var g_SavedGameData;
function selectDescription()
{
let gameSelection = Engine.GetGUIObjectByName("gameSelection");
let gameID = gameSelection.list_data[gameSelection.selected];
Engine.GetGUIObjectByName("deleteGameButton").enabled = !!gameID;
if (!gameID)
return;
Engine.GetGUIObjectByName("saveGameDesc").caption = g_Descriptions[gameID];
}
function init(data)
{
g_SavedGameData = data && data.savedGameData || {};
let simulationState = Engine.GuiInterfaceCall("GetSimulationState");
g_SavedGameData.timeElapsed = simulationState.timeElapsed;
g_SavedGameData.states = simulationState.players.map(pState => pState.state);
let savedGames = Engine.GetSavedGames().sort(sortDecreasingDate);
let gameSelection = Engine.GetGUIObjectByName("gameSelection");
gameSelection.enabled = savedGames.length != 0;
if (!savedGames.length)
{
gameSelection.list = [translate("No saved games found")];
gameSelection.selected = -1;
return;
}
g_Descriptions = {};
for (let game of savedGames)
g_Descriptions[game.id] = game.metadata.description || "";
let engineInfo = Engine.GetEngineInfo();
gameSelection.list = savedGames.map(game => generateSavegameLabel(game.metadata, engineInfo));
gameSelection.list_data = savedGames.map(game => game.id);
gameSelection.selected = Math.min(gameSelection.selected, gameSelection.list.length - 1);
Engine.GetGUIObjectByName("deleteGameButton").tooltip = deleteTooltip();
}
function saveGame()
{
let gameSelection = Engine.GetGUIObjectByName("gameSelection");
let gameLabel = gameSelection.list[gameSelection.selected];
let gameID = gameSelection.list_data[gameSelection.selected];
let desc = Engine.GetGUIObjectByName("saveGameDesc").caption;
let name = gameID || "savegame";
if (!gameID)
{
reallySaveGame(name, desc, true);
return;
}
messageBox(
500, 200,
sprintf(translate("\"%(label)s\""), { "label": gameLabel }) + "\n" +
translate("Saved game will be permanently overwritten, are you sure?"),
translate("OVERWRITE SAVE"),
[translate("No"), translate("Yes")],
[null, function(){ reallySaveGame(name, desc, false); }]
);
}
function reallySaveGame(name, desc, nameIsPrefix)
{
if (nameIsPrefix)
Engine.SaveGamePrefix(name, desc, g_SavedGameData);
else
Engine.SaveGame(name, desc, g_SavedGameData);
closeSave();
}
function closeSave()
{
Engine.PopGuiPage();
}
-
-// HACK: Engine.SaveGame* expects this function to be defined on the current page.
-// That's why we have to pass the data to this page even if we don't need it.
-function getSavedGameData()
-{
- return g_SavedGameData;
-}
Index: ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml (revision 22921)
+++ ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml (revision 22922)
@@ -1,130 +1,130 @@
Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 22921)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 22922)
@@ -1,158 +1,162 @@
onTick();
onWindowResized();
+
+ restoreSavedGameData(arguments[0]);
+
+
onSimulationUpdate();
onReplayFinished();
onReplayOutOfSync(arguments[0], arguments[1], arguments[2]);
Engine.ConfigDB_CreateValue("user", "gui.session.timeelapsedcounter", String(Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") != "true"));
Engine.ConfigDB_CreateValue("user", "gui.session.ceasefirecounter", String(Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") != "true"));
ExitleaveGame();Game PausedClick to Resume GametogglePause();
Index: ps/trunk/source/gui/GUIManager.cpp
===================================================================
--- ps/trunk/source/gui/GUIManager.cpp (revision 22921)
+++ ps/trunk/source/gui/GUIManager.cpp (revision 22922)
@@ -1,426 +1,402 @@
/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "GUIManager.h"
#include "gui/CGUI.h"
#include "lib/timer.h"
#include "ps/Filesystem.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
#include "ps/XML/Xeromyces.h"
#include "ps/GameSetup/Config.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptRuntime.h"
CGUIManager* g_GUI = NULL;
// General TODOs:
//
// A lot of the CGUI data could (and should) be shared between
// multiple pages, instead of treating them as completely independent, to save
// memory and loading time.
// called from main loop when (input) events are received.
// event is passed to other handlers if false is returned.
// trampoline: we don't want to make the HandleEvent implementation static
InReaction gui_handler(const SDL_Event_* ev)
{
if (!g_GUI)
return IN_PASS;
PROFILE("GUI event handler");
return g_GUI->HandleEvent(ev);
}
static Status ReloadChangedFileCB(void* param, const VfsPath& path)
{
return static_cast(param)->ReloadChangedFile(path);
}
CGUIManager::CGUIManager()
{
m_ScriptRuntime = g_ScriptRuntime;
m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIManager", m_ScriptRuntime));
m_ScriptInterface->SetCallbackData(this);
m_ScriptInterface->LoadGlobalScripts();
if (!CXeromyces::AddValidator(g_VFS, "gui_page", "gui/gui_page.rng"))
LOGERROR("CGUIManager: failed to load GUI page grammar file 'gui/gui_page.rng'");
if (!CXeromyces::AddValidator(g_VFS, "gui", "gui/gui.rng"))
LOGERROR("CGUIManager: failed to load GUI XML grammar file 'gui/gui.rng'");
RegisterFileReloadFunc(ReloadChangedFileCB, this);
}
CGUIManager::~CGUIManager()
{
UnregisterFileReloadFunc(ReloadChangedFileCB, this);
}
size_t CGUIManager::GetPageCount() const
{
return m_PageStack.size();
}
void CGUIManager::SwitchPage(const CStrW& pageName, ScriptInterface* srcScriptInterface, JS::HandleValue initData)
{
// The page stack is cleared (including the script context where initData came from),
// therefore we have to clone initData.
shared_ptr initDataClone;
if (!initData.isUndefined())
initDataClone = srcScriptInterface->WriteStructuredClone(initData);
m_PageStack.clear();
PushPage(pageName, initDataClone, JS::UndefinedHandleValue);
}
void CGUIManager::PushPage(const CStrW& pageName, shared_ptr initData, JS::HandleValue callbackFunction)
{
// Store the callback handler in the current GUI page before opening the new one
if (!m_PageStack.empty() && !callbackFunction.isUndefined())
m_PageStack.back().SetCallbackFunction(*m_ScriptInterface, callbackFunction);
// Push the page prior to loading its contents, because that may push
// another GUI page on init which should be pushed on top of this new page.
m_PageStack.emplace_back(pageName, initData);
m_PageStack.back().LoadPage(m_ScriptRuntime);
ResetCursor();
}
void CGUIManager::PopPage(shared_ptr args)
{
if (m_PageStack.size() < 2)
{
debug_warn(L"Tried to pop GUI page when there's < 2 in the stack");
return;
}
m_PageStack.pop_back();
m_PageStack.back().PerformCallbackFunction(args);
}
CGUIManager::SGUIPage::SGUIPage(const CStrW& pageName, const shared_ptr initData)
: name(pageName), initData(initData), inputs(), gui(), callbackFunction()
{
}
void CGUIManager::SGUIPage::LoadPage(shared_ptr scriptRuntime)
{
// If we're hotloading then try to grab some data from the previous page
shared_ptr hotloadData;
if (gui)
{
shared_ptr scriptInterface = gui->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue global(cx, scriptInterface->GetGlobalObject());
JS::RootedValue hotloadDataVal(cx);
scriptInterface->CallFunction(global, "getHotloadData", &hotloadDataVal);
hotloadData = scriptInterface->WriteStructuredClone(hotloadDataVal);
}
inputs.clear();
gui.reset(new CGUI(scriptRuntime));
gui->Initialize();
VfsPath path = VfsPath("gui") / name;
inputs.insert(path);
CXeromyces xero;
if (xero.Load(g_VFS, path, "gui_page") != PSRETURN_OK)
// Fail silently (Xeromyces reported the error)
return;
int elmt_page = xero.GetElementID("page");
int elmt_include = xero.GetElementID("include");
XMBElement root = xero.GetRoot();
if (root.GetNodeName() != elmt_page)
{
LOGERROR("GUI page '%s' must have root element ", utf8_from_wstring(name));
return;
}
XERO_ITER_EL(root, node)
{
if (node.GetNodeName() != elmt_include)
{
LOGERROR("GUI page '%s' must only have elements inside ", utf8_from_wstring(name));
continue;
}
std::string name = node.GetText();
CStrW nameW (node.GetText().FromUTF8());
PROFILE2("load gui xml");
PROFILE2_ATTR("name: %s", name.c_str());
TIMER(nameW.c_str());
if (name.back() == '/')
{
VfsPath directory = VfsPath("gui") / nameW;
VfsPaths pathnames;
vfs::GetPathnames(g_VFS, directory, L"*.xml", pathnames);
for (const VfsPath& path : pathnames)
gui->LoadXmlFile(path, inputs);
}
else
{
VfsPath path = VfsPath("gui") / nameW;
gui->LoadXmlFile(path, inputs);
}
}
gui->SendEventToAll("load");
shared_ptr scriptInterface = gui->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue initDataVal(cx);
JS::RootedValue hotloadDataVal(cx);
JS::RootedValue global(cx, scriptInterface->GetGlobalObject());
if (initData)
scriptInterface->ReadStructuredClone(initData, &initDataVal);
if (hotloadData)
scriptInterface->ReadStructuredClone(hotloadData, &hotloadDataVal);
if (scriptInterface->HasProperty(global, "init") &&
!scriptInterface->CallFunctionVoid(global, "init", initDataVal, hotloadDataVal))
LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(name));
}
void CGUIManager::SGUIPage::SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc)
{
if (!callbackFunc.isObject())
{
LOGERROR("Given callback handler is not an object!");
return;
}
// Does not require JSAutoRequest
if (!JS_ObjectIsFunction(scriptInterface.GetContext(), &callbackFunc.toObject()))
{
LOGERROR("Given callback handler is not a function!");
return;
}
callbackFunction = std::make_shared(scriptInterface.GetJSRuntime(), callbackFunc);
}
void CGUIManager::SGUIPage::PerformCallbackFunction(shared_ptr args)
{
if (!callbackFunction)
return;
shared_ptr scriptInterface = gui->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedObject globalObj(cx, &scriptInterface->GetGlobalObject().toObject());
JS::RootedValue funcVal(cx, *callbackFunction);
// Delete the callback function, so that it is not called again
callbackFunction.reset();
JS::RootedValue argVal(cx);
if (args)
scriptInterface->ReadStructuredClone(args, &argVal);
JS::AutoValueVector paramData(cx);
paramData.append(argVal);
JS::RootedValue result(cx);
JS_CallFunctionValue(cx, globalObj, funcVal, paramData, &result);
}
Status CGUIManager::ReloadChangedFile(const VfsPath& path)
{
for (SGUIPage& p : m_PageStack)
if (p.inputs.count(path))
{
LOGMESSAGE("GUI file '%s' changed - reloading page '%s'", path.string8(), utf8_from_wstring(p.name));
p.LoadPage(m_ScriptRuntime);
// TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators
}
return INFO::OK;
}
Status CGUIManager::ReloadAllPages()
{
// TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators
for (SGUIPage& p : m_PageStack)
p.LoadPage(m_ScriptRuntime);
return INFO::OK;
}
void CGUIManager::ResetCursor()
{
g_CursorName = g_DefaultCursor;
}
-std::string CGUIManager::GetSavedGameData()
-{
- shared_ptr scriptInterface = top()->GetScriptInterface();
- JSContext* cx = scriptInterface->GetContext();
- JSAutoRequest rq(cx);
-
- JS::RootedValue data(cx);
- JS::RootedValue global(cx, top()->GetGlobalObject());
- scriptInterface->CallFunction(global, "getSavedGameData", &data);
- return scriptInterface->StringifyJSON(&data, false);
-}
-
-void CGUIManager::RestoreSavedGameData(const std::string& jsonData)
-{
- shared_ptr scriptInterface = top()->GetScriptInterface();
- JSContext* cx = scriptInterface->GetContext();
- JSAutoRequest rq(cx);
-
- JS::RootedValue global(cx, top()->GetGlobalObject());
- JS::RootedValue dataVal(cx);
- scriptInterface->ParseJSON(jsonData, &dataVal);
- scriptInterface->CallFunctionVoid(global, "restoreSavedGameData", dataVal);
-}
-
InReaction CGUIManager::HandleEvent(const SDL_Event_* ev)
{
// We want scripts to have access to the raw input events, so they can do complex
// processing when necessary (e.g. for unit selection and camera movement).
// Sometimes they'll want to be layered behind the GUI widgets (e.g. to detect mousedowns on the
// visible game area), sometimes they'll want to intercepts events before the GUI (e.g.
// to capture all mouse events until a mouseup after dragging).
// So we call two separate handler functions:
bool handled = false;
{
PROFILE("handleInputBeforeGui");
JSContext* cx = top()->GetScriptInterface()->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue global(cx, top()->GetGlobalObject());
if (top()->GetScriptInterface()->CallFunction(global, "handleInputBeforeGui", handled, *ev, top()->FindObjectUnderMouse()))
if (handled)
return IN_HANDLED;
}
{
PROFILE("handle event in native GUI");
InReaction r = top()->HandleEvent(ev);
if (r != IN_PASS)
return r;
}
{
// We can't take the following lines out of this scope because top() may be another gui page than it was when calling handleInputBeforeGui!
JSContext* cx = top()->GetScriptInterface()->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue global(cx, top()->GetGlobalObject());
PROFILE("handleInputAfterGui");
if (top()->GetScriptInterface()->CallFunction(global, "handleInputAfterGui", handled, *ev))
if (handled)
return IN_HANDLED;
}
return IN_PASS;
}
void CGUIManager::SendEventToAll(const CStr& eventName) const
{
top()->SendEventToAll(eventName);
}
void CGUIManager::SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const
{
top()->SendEventToAll(eventName, paramData);
}
void CGUIManager::TickObjects()
{
PROFILE3("gui tick");
// We share the script runtime with everything else that runs in the same thread.
// This call makes sure we trigger GC regularly even if the simulation is not running.
m_ScriptInterface->GetRuntime()->MaybeIncrementalGC(1.0f);
// Save an immutable copy so iterators aren't invalidated by tick handlers
PageStackType pageStack = m_PageStack;
for (const SGUIPage& p : pageStack)
p.gui->TickObjects();
}
void CGUIManager::Draw() const
{
PROFILE3_GPU("gui");
for (const SGUIPage& p : m_PageStack)
p.gui->Draw();
}
void CGUIManager::UpdateResolution()
{
// Save an immutable copy so iterators aren't invalidated by event handlers
PageStackType pageStack = m_PageStack;
for (const SGUIPage& p : pageStack)
{
p.gui->UpdateResolution();
p.gui->SendEventToAll("WindowResized");
}
}
bool CGUIManager::TemplateExists(const std::string& templateName) const
{
return m_TemplateLoader.TemplateExists(templateName);
}
const CParamNode& CGUIManager::GetTemplate(const std::string& templateName)
{
const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity");
if (!templateRoot.IsOk())
LOGERROR("Invalid template found for '%s'", templateName.c_str());
return templateRoot;
}
// This returns a shared_ptr to make sure the CGUI doesn't get deallocated
// while we're in the middle of calling a function on it (e.g. if a GUI script
// calls SwitchPage)
shared_ptr CGUIManager::top() const
{
ENSURE(m_PageStack.size());
return m_PageStack.back().gui;
}
Index: ps/trunk/source/gui/GUIManager.h
===================================================================
--- ps/trunk/source/gui/GUIManager.h (revision 22921)
+++ ps/trunk/source/gui/GUIManager.h (revision 22922)
@@ -1,195 +1,188 @@
/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_GUIMANAGER
#define INCLUDED_GUIMANAGER
#include
#include
#include "lib/input.h"
#include "lib/file/vfs/vfs_path.h"
#include "ps/CStr.h"
#include "ps/TemplateLoader.h"
#include "scriptinterface/ScriptVal.h"
#include "scriptinterface/ScriptInterface.h"
class CGUI;
class JSObject;
class IGUIObject;
struct CGUIColor;
struct SGUIIcon;
/**
* External interface to the GUI system.
*
* The GUI consists of a set of pages. Each page is constructed from a
* series of XML files, and is independent from any other page.
* Only one page is active at a time. All events and render requests etc
* will go to the active page. This lets the GUI switch between pre-game menu
* and in-game UI.
*/
class CGUIManager
{
NONCOPYABLE(CGUIManager);
public:
CGUIManager();
~CGUIManager();
shared_ptr GetScriptInterface()
{
return m_ScriptInterface;
}
shared_ptr GetRuntime() { return m_ScriptRuntime; }
shared_ptr GetActiveGUI() { return top(); }
/**
* Returns the number of currently open GUI pages.
*/
size_t GetPageCount() const;
/**
* Load a new GUI page and make it active. All current pages will be destroyed.
*/
void SwitchPage(const CStrW& name, ScriptInterface* srcScriptInterface, JS::HandleValue initData);
/**
* Load a new GUI page and make it active. All current pages will be retained,
* and will still be drawn and receive tick events, but will not receive
* user inputs.
* If given, the callbackHandler function will be executed once this page is closed.
*/
void PushPage(const CStrW& pageName, shared_ptr initData, JS::HandleValue callbackFunc);
/**
* Unload the currently active GUI page, and make the previous page active.
* (There must be at least two pages when you call this.)
*/
void PopPage(shared_ptr args);
/**
* Called when a file has been modified, to hotload changes.
*/
Status ReloadChangedFile(const VfsPath& path);
/**
* Sets the default mouse pointer.
*/
void ResetCursor();
/**
* Called when we should reload all pages (e.g. translation hotloading update).
*/
Status ReloadAllPages();
/**
* Pass input events to the currently active GUI page.
*/
InReaction HandleEvent(const SDL_Event_* ev);
/**
* See CGUI::SendEventToAll; applies to the currently active page.
*/
void SendEventToAll(const CStr& eventName) const;
void SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const;
/**
* See CGUI::TickObjects; applies to @em all loaded pages.
*/
void TickObjects();
/**
* See CGUI::Draw; applies to @em all loaded pages.
*/
void Draw() const;
/**
* See CGUI::UpdateResolution; applies to @em all loaded pages.
*/
void UpdateResolution();
- /**
- * Calls the current page's script function getSavedGameData() and returns the result.
- */
- std::string GetSavedGameData();
-
- void RestoreSavedGameData(const std::string& jsonData);
-
/**
* Check if a template with this name exists
*/
bool TemplateExists(const std::string& templateName) const;
/**
* Retrieve the requested template, used for displaying faction specificities.
*/
const CParamNode& GetTemplate(const std::string& templateName);
private:
struct SGUIPage
{
// COPYABLE, because event handlers may invalidate page stack iterators by open or close pages,
// and event handlers need to be called for the entire stack.
/**
* Initializes the data that will be used to create the CGUI page one or multiple times (hotloading).
*/
SGUIPage(const CStrW& pageName, const shared_ptr initData);
/**
* Create the CGUI with it's own ScriptInterface. Deletes the previous CGUI if it existed.
*/
void LoadPage(shared_ptr scriptRuntime);
/**
* Sets the callback handler when a new page is opened that will be performed when the page is closed.
*/
void SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc);
/**
* Execute the stored callback function with the given arguments.
*/
void PerformCallbackFunction(shared_ptr args);
CStrW name;
boost::unordered_set inputs; // for hotloading
shared_ptr initData; // data to be passed to the init() function
shared_ptr gui; // the actual GUI page
/**
* Function executed by this parent GUI page when the child GUI page it pushed is popped.
* Notice that storing it in the SGUIPage instead of CGUI means that it will survive the hotloading CGUI reset.
*/
shared_ptr callbackFunction;
};
shared_ptr top() const;
shared_ptr m_ScriptRuntime;
shared_ptr m_ScriptInterface;
typedef std::vector PageStackType;
PageStackType m_PageStack;
CTemplateLoader m_TemplateLoader;
};
extern CGUIManager* g_GUI;
extern InReaction gui_handler(const SDL_Event_* ev);
#endif // INCLUDED_GUIMANAGER
Index: ps/trunk/source/ps/scripting/JSInterface_SavedGame.cpp
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_SavedGame.cpp (revision 22921)
+++ ps/trunk/source/ps/scripting/JSInterface_SavedGame.cpp (revision 22922)
@@ -1,117 +1,123 @@
/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "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))
+void JSI_SavedGame::QuickSave(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), JS::HandleValue GUIMetadata)
{
- g_Game->GetTurnManager()->QuickSave();
+ 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))
{
- g_Game->GetTurnManager()->QuickLoad();
+ 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);
ENSURE(!g_NetServer);
ENSURE(!g_NetClient);
ENSURE(!g_Game);
// Load the saved game data from disk
JS::RootedValue guiContextMetadata(cxGui);
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);
JS::RootedValue gameContextMetadata(cxGame,
sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), guiContextMetadata));
JS::RootedValue gameInitAttributes(cxGame);
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("QuickSave");
scriptInterface.RegisterFunction("QuickLoad");
scriptInterface.RegisterFunction("StartSavedGame");
}
Index: ps/trunk/source/ps/scripting/JSInterface_SavedGame.h
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_SavedGame.h (revision 22921)
+++ ps/trunk/source/ps/scripting/JSInterface_SavedGame.h (revision 22922)
@@ -1,36 +1,36 @@
-/* Copyright (C) 2018 Wildfire Games.
+/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_JSI_SAVEDGAME
#define INCLUDED_JSI_SAVEDGAME
#include "scriptinterface/ScriptInterface.h"
namespace JSI_SavedGame
{
JS::Value GetSavedGames(ScriptInterface::CxPrivate* pCxPrivate);
bool DeleteSavedGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name);
void SaveGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename, const std::wstring& description, JS::HandleValue GUIMetadata);
void SaveGamePrefix(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& prefix, const std::wstring& description, JS::HandleValue GUIMetadata);
- void QuickSave(ScriptInterface::CxPrivate* pCxPrivate);
+ void QuickSave(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue GUIMetadata);
void QuickLoad(ScriptInterface::CxPrivate* pCxPrivate);
JS::Value StartSavedGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name);
void RegisterScriptFunctions(const ScriptInterface& scriptInterface);
}
#endif // INCLUDED_JSI_SAVEDGAME
Index: ps/trunk/source/simulation2/system/TurnManager.cpp
===================================================================
--- ps/trunk/source/simulation2/system/TurnManager.cpp (revision 22921)
+++ ps/trunk/source/simulation2/system/TurnManager.cpp (revision 22922)
@@ -1,338 +1,354 @@
-/* Copyright (C) 2017 Wildfire Games.
+/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "TurnManager.h"
#include "gui/GUIManager.h"
#include "maths/MathUtil.h"
#include "ps/Pyrogenesis.h"
#include "ps/Profile.h"
#include "ps/CLogger.h"
#include "ps/Replay.h"
#include "ps/Util.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
const u32 DEFAULT_TURN_LENGTH_MP = 500;
const u32 DEFAULT_TURN_LENGTH_SP = 200;
const int COMMAND_DELAY = 2;
#if 0
#define NETTURN_LOG(...) debug_printf(__VA_ARGS__)
#else
#define NETTURN_LOG(...)
#endif
CTurnManager::CTurnManager(CSimulation2& simulation, u32 defaultTurnLength, int clientId, IReplayLogger& replay)
: m_Simulation2(simulation), m_CurrentTurn(0), m_ReadyTurn(1), m_TurnLength(defaultTurnLength),
m_PlayerId(-1), m_ClientId(clientId), m_DeltaSimTime(0), m_HasSyncError(false), m_Replay(replay),
- m_FinalTurn(std::numeric_limits::max()), m_TimeWarpNumTurns(0)
+ m_FinalTurn(std::numeric_limits::max()), m_TimeWarpNumTurns(0),
+ m_QuickSaveMetadata(m_Simulation2.GetScriptInterface().GetContext())
{
// When we are on turn n, we schedule new commands for n+2.
// We know that all other clients have finished scheduling commands for n (else we couldn't have got here).
// We know we have not yet finished scheduling commands for n+2.
// Hence other clients can be on turn n-1, n, n+1, and no other.
// So they can be sending us commands scheduled for n+1, n+2, n+3.
// So we need a 3-element buffer:
m_QueuedCommands.resize(COMMAND_DELAY + 1);
}
void CTurnManager::ResetState(u32 newCurrentTurn, u32 newReadyTurn)
{
m_CurrentTurn = newCurrentTurn;
m_ReadyTurn = newReadyTurn;
m_DeltaSimTime = 0;
size_t queuedCommandsSize = m_QueuedCommands.size();
m_QueuedCommands.clear();
m_QueuedCommands.resize(queuedCommandsSize);
}
void CTurnManager::SetPlayerID(int playerId)
{
m_PlayerId = playerId;
}
bool CTurnManager::WillUpdate(float simFrameLength) const
{
// Keep this in sync with the return value of Update()
if (m_CurrentTurn > m_FinalTurn)
return false;
if (m_DeltaSimTime + simFrameLength < 0)
return false;
if (m_ReadyTurn <= m_CurrentTurn)
return false;
return true;
}
bool CTurnManager::Update(float simFrameLength, size_t maxTurns)
{
if (m_CurrentTurn > m_FinalTurn)
return false;
m_DeltaSimTime += simFrameLength;
// If the game becomes laggy, m_DeltaSimTime increases progressively.
// The engine will fast forward accordingly to catch up.
// To keep the game playable, stop fast forwarding after 2 turn lengths.
m_DeltaSimTime = std::min(m_DeltaSimTime, 2.0f * m_TurnLength / 1000.0f);
// If we haven't reached the next turn yet, do nothing
if (m_DeltaSimTime < 0)
return false;
NETTURN_LOG("Update current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn);
// Check that the next turn is ready for execution
if (m_ReadyTurn <= m_CurrentTurn)
{
// Oops, we wanted to start the next turn but it's not ready yet -
// there must be too much network lag.
// TODO: complain to the user.
// TODO: send feedback to the server to increase the turn length.
// Reset the next-turn timer to 0 so we try again next update but
// so we don't rush to catch up in subsequent turns.
// TODO: we should do clever rate adjustment instead of just pausing like this.
m_DeltaSimTime = 0;
return false;
}
maxTurns = std::max((size_t)1, maxTurns); // always do at least one turn
for (size_t i = 0; i < maxTurns; ++i)
{
// Check that we've reached the i'th next turn
if (m_DeltaSimTime < 0)
break;
// Check that the i'th next turn is still ready
if (m_ReadyTurn <= m_CurrentTurn)
break;
NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);
// Increase now, so Update can send new commands for a subsequent turn
++m_CurrentTurn;
// Clean up any destroyed entities since the last turn (e.g. placement previews
// or rally point flags generated by the GUI). (Must do this before the time warp
// serialization.)
m_Simulation2.FlushDestroyedEntities();
// Save the current state for rewinding, if enabled
if (m_TimeWarpNumTurns && (m_CurrentTurn % m_TimeWarpNumTurns) == 0)
{
PROFILE3("time warp serialization");
std::stringstream stream;
m_Simulation2.SerializeState(stream);
m_TimeWarpStates.push_back(stream.str());
}
// Put all the client commands into a single list, in a globally consistent order
std::vector commands;
for (std::pair>& p : m_QueuedCommands[0])
commands.insert(commands.end(), std::make_move_iterator(p.second.begin()), std::make_move_iterator(p.second.end()));
m_QueuedCommands.pop_front();
m_QueuedCommands.resize(m_QueuedCommands.size() + 1);
m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);
NETTURN_LOG("Running %d cmds\n", commands.size());
m_Simulation2.Update(m_TurnLength, commands);
NotifyFinishedUpdate(m_CurrentTurn);
// Set the time for the next turn update
m_DeltaSimTime -= m_TurnLength / 1000.f;
}
return true;
}
bool CTurnManager::UpdateFastForward()
{
m_DeltaSimTime = 0;
NETTURN_LOG("UpdateFastForward current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn);
// Check that the next turn is ready for execution
if (m_ReadyTurn <= m_CurrentTurn)
return false;
while (m_ReadyTurn > m_CurrentTurn)
{
// TODO: It would be nice to remove some of the duplication with Update()
// (This is similar but doesn't call any Notify functions or update DeltaTime,
// it just updates the simulation state)
++m_CurrentTurn;
m_Simulation2.FlushDestroyedEntities();
// Put all the client commands into a single list, in a globally consistent order
std::vector commands;
for (std::pair>& p : m_QueuedCommands[0])
commands.insert(commands.end(), std::make_move_iterator(p.second.begin()), std::make_move_iterator(p.second.end()));
m_QueuedCommands.pop_front();
m_QueuedCommands.resize(m_QueuedCommands.size() + 1);
m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);
NETTURN_LOG("Running %d cmds\n", commands.size());
m_Simulation2.Update(m_TurnLength, commands);
}
return true;
}
void CTurnManager::Interpolate(float simFrameLength, float realFrameLength)
{
// TODO: using m_TurnLength might be a bit dodgy when length changes - maybe
// we need to save the previous turn length?
float offset = clamp(m_DeltaSimTime / (m_TurnLength / 1000.f) + 1.0, 0.0, 1.0);
// Stop animations while still updating the selection highlight
if (m_CurrentTurn > m_FinalTurn)
simFrameLength = 0;
m_Simulation2.Interpolate(simFrameLength, offset, realFrameLength);
}
void CTurnManager::AddCommand(int client, int player, JS::HandleValue data, u32 turn)
{
NETTURN_LOG("AddCommand(client=%d player=%d turn=%d)\n", client, player, turn);
if (!(m_CurrentTurn < turn && turn <= m_CurrentTurn + COMMAND_DELAY + 1))
{
debug_warn(L"Received command for invalid turn");
return;
}
m_Simulation2.GetScriptInterface().FreezeObject(data, true);
JSContext* cx = m_Simulation2.GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
m_QueuedCommands[turn - (m_CurrentTurn+1)][client].emplace_back(player, cx, data);
}
void CTurnManager::FinishedAllCommands(u32 turn, u32 turnLength)
{
NETTURN_LOG("FinishedAllCommands(%d, %d)\n", turn, turnLength);
ENSURE(turn == m_ReadyTurn + 1);
m_ReadyTurn = turn;
m_TurnLength = turnLength;
}
bool CTurnManager::TurnNeedsFullHash(u32 turn) const
{
// Check immediately for errors caused by e.g. inconsistent game versions
// (The hash is computed after the first sim update, so we start at turn == 1)
if (turn == 1)
return true;
// Otherwise check the full state every ~10 seconds in multiplayer games
// (TODO: should probably remove this when we're reasonably sure the game
// isn't too buggy, since the full hash is still pretty slow)
if (turn % 20 == 0)
return true;
return false;
}
void CTurnManager::EnableTimeWarpRecording(size_t numTurns)
{
m_TimeWarpStates.clear();
m_TimeWarpNumTurns = numTurns;
}
void CTurnManager::RewindTimeWarp()
{
if (m_TimeWarpStates.empty())
return;
std::stringstream stream(m_TimeWarpStates.back());
m_Simulation2.DeserializeState(stream);
m_TimeWarpStates.pop_back();
// Reset the turn manager state, so we won't execute stray commands and
// won't do the next snapshot until the appropriate time.
// (Ideally we ought to serialise the turn manager state and restore it
// here, but this is simpler for now.)
ResetState(0, 1);
}
-void CTurnManager::QuickSave()
+void CTurnManager::QuickSave(JS::HandleValue GUIMetadata)
{
TIMER(L"QuickSave");
std::stringstream stream;
if (!m_Simulation2.SerializeState(stream))
{
LOGERROR("Failed to quicksave game");
return;
}
m_QuickSaveState = stream.str();
- if (g_GUI)
- m_QuickSaveMetadata = g_GUI->GetSavedGameData();
+
+ JSContext* cx = m_Simulation2.GetScriptInterface().GetContext();
+ JSAutoRequest rq(cx);
+
+ if (JS_StructuredClone(cx, GUIMetadata, &m_QuickSaveMetadata, nullptr, nullptr))
+ {
+ m_Simulation2.GetScriptInterface().FreezeObject(m_QuickSaveMetadata, true);
+ }
else
- m_QuickSaveMetadata = std::string();
+ {
+ LOGERROR("Could not copy savegame GUI metadata");
+ m_QuickSaveMetadata = JS::UndefinedValue();
+ }
LOGMESSAGERENDER("Quicksaved game");
-
}
void CTurnManager::QuickLoad()
{
TIMER(L"QuickLoad");
if (m_QuickSaveState.empty())
{
LOGERROR("Cannot quickload game - no game was quicksaved");
return;
}
std::stringstream stream(m_QuickSaveState);
if (!m_Simulation2.DeserializeState(stream))
{
LOGERROR("Failed to quickload game");
return;
}
- if (g_GUI && !m_QuickSaveMetadata.empty())
- g_GUI->RestoreSavedGameData(m_QuickSaveMetadata);
-
- LOGMESSAGERENDER("Quickloaded game");
-
// See RewindTimeWarp
ResetState(0, 1);
+
+ if (!g_GUI)
+ return;
+
+ JSContext* cx = m_Simulation2.GetScriptInterface().GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::AutoValueArray<1> paramData(cx);
+ paramData[0].set(m_QuickSaveMetadata);
+ g_GUI->SendEventToAll("SavegameLoaded", paramData);
+
+ LOGMESSAGERENDER("Quickloaded game");
}
Index: ps/trunk/source/simulation2/system/TurnManager.h
===================================================================
--- ps/trunk/source/simulation2/system/TurnManager.h (revision 22921)
+++ ps/trunk/source/simulation2/system/TurnManager.h (revision 22922)
@@ -1,195 +1,195 @@
-/* Copyright (C) 2017 Wildfire Games.
+/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_TURNMANAGER
#define INCLUDED_TURNMANAGER
#include "simulation2/helpers/SimulationCommand.h"
#include
#include