Index: ps/trunk/source/scriptinterface/NativeWrapperDecls.h =================================================================== --- ps/trunk/source/scriptinterface/NativeWrapperDecls.h (revision 25353) +++ ps/trunk/source/scriptinterface/NativeWrapperDecls.h (nonexistent) @@ -1,102 +0,0 @@ -/* 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( \ - 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" - -// 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; Property changes on: ps/trunk/source/scriptinterface/NativeWrapperDecls.h ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/scriptinterface/NativeWrapperDefns.h =================================================================== --- ps/trunk/source/scriptinterface/NativeWrapperDefns.h (revision 25353) +++ ps/trunk/source/scriptinterface/NativeWrapperDefns.h (nonexistent) @@ -1,235 +0,0 @@ -/* 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(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(const ScriptRequest& rq, JS::MutableHandleValue rval, F fptr, Ts... 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(const ScriptRequest& rq, JS::MutableHandleValue UNUSED(rval), F fptr, Ts... params) - { - fptr(ScriptInterface::GetScriptInterfaceAndCBData(rq.cx), params...); - } -}; - -// Same idea but for method calls: - -template -struct ScriptInterface_NativeMethodWrapper -{ - template - static void call(const ScriptRequest& rq, JS::MutableHandleValue rval, TC* c, F fptr, Ts... params) - { - ScriptInterface::AssignOrToJSValUnrooted(rq, rval, (c->*fptr)(params...)); - } -}; - -template -struct ScriptInterface_NativeMethodWrapper -{ - template - static void call(const ScriptRequest& 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); \ - ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ - BOOST_PP_REPEAT_##z (i, CONVERT_ARG, ~) \ - JS::RootedValue rval(rq.cx); \ - ScriptInterface_NativeWrapper::template call(rq, &rval, fptr A0_TAIL(z,i)); \ - args.rval().set(rval); \ - return !ScriptException::IsPending(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); \ - ScriptRequest 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(rq.cx); \ - ScriptInterface_NativeMethodWrapper::template call(rq, &rval, c, fptr A0_TAIL(z,i)); \ - args.rval().set(rval); \ - return !ScriptException::IsPending(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); \ - ScriptRequest 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(rq.cx); \ - ScriptInterface_NativeMethodWrapper::template call(rq, &rval, c, fptr A0_TAIL(z,i)); \ - args.rval().set(rval); \ - return !ScriptException::IsPending(rq); \ - } -BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~) -#undef OVERLOADS - -template -static void AssignOrToJSValHelper(const ScriptRequest& rq, JS::MutableHandleValueVector argv, const T& a, const Ts&... params) -{ - ScriptInterface::AssignOrToJSVal(rq, argv[i], a); - AssignOrToJSValHelper(rq, argv, params...); -} - -template -static void AssignOrToJSValHelper(const ScriptRequest& UNUSED(rq), JS::MutableHandleValueVector 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 -{ - ScriptRequest rq(this); - JS::RootedValue jsRet(rq.cx); - JS::RootedValueVector argv(rq.cx); - ignore_result(argv.resize(sizeof...(Ts))); - AssignOrToJSValHelper<0>(rq, &argv, params...); - if (!CallFunction_(val, name, argv, &jsRet)) - return false; - return FromJSVal(rq, jsRet, ret); -} - -template -bool ScriptInterface::CallFunction(JS::HandleValue val, const char* name, JS::Rooted* ret, const Ts&... params) const -{ - ScriptRequest rq(this); - JS::MutableHandle jsRet(ret); - JS::RootedValueVector argv(rq.cx); - ignore_result(argv.resize(sizeof...(Ts))); - 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 -{ - ScriptRequest rq(this); - JS::RootedValueVector argv(rq.cx); - ignore_result(argv.resize(sizeof...(Ts))); - 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 -{ - ScriptRequest rq(this); - JS::RootedValue jsRet(rq.cx); - JS::RootedValueVector argv(rq.cx); - ignore_result(argv.resize(sizeof...(Ts))); - 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 Property changes on: ps/trunk/source/scriptinterface/NativeWrapperDefns.h ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/gui/GUIManager.cpp =================================================================== --- ps/trunk/source/gui/GUIManager.cpp (revision 25353) +++ ps/trunk/source/gui/GUIManager.cpp (revision 25354) @@ -1,405 +1,406 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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/CLogger.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/XML/Xeromyces.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" CGUIManager* g_GUI = nullptr; const CStr CGUIManager::EventNameWindowResized = "WindowResized"; // 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_ScriptContext = g_ScriptContext; m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIManager", m_ScriptContext)); 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. ScriptInterface::StructuredClone initDataClone; if (!initData.isUndefined()) initDataClone = srcScriptInterface->WriteStructuredClone(initData); m_PageStack.clear(); PushPage(pageName, initDataClone, JS::UndefinedHandleValue); } void CGUIManager::PushPage(const CStrW& pageName, ScriptInterface::StructuredClone 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_ScriptContext); } void CGUIManager::PopPage(ScriptInterface::StructuredClone 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 ScriptInterface::StructuredClone initData) : m_Name(pageName), initData(initData), inputs(), gui(), callbackFunction() { } void CGUIManager::SGUIPage::LoadPage(shared_ptr scriptContext) { // If we're hotloading then try to grab some data from the previous page ScriptInterface::StructuredClone hotloadData; if (gui) { shared_ptr scriptInterface = gui->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue hotloadDataVal(rq.cx); - scriptInterface->CallFunction(global, "getHotloadData", &hotloadDataVal); + ScriptFunction::Call(rq, global, "getHotloadData", &hotloadDataVal); hotloadData = scriptInterface->WriteStructuredClone(hotloadDataVal); } g_CursorName = g_DefaultCursor; inputs.clear(); gui.reset(new CGUI(scriptContext)); gui->AddObjectTypes(); VfsPath path = VfsPath("gui") / m_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(m_Name)); return; } XERO_ITER_EL(root, node) { if (node.GetNodeName() != elmt_include) { LOGERROR("GUI page '%s' must only have elements inside ", utf8_from_wstring(m_Name)); continue; } CStr8 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 currentDirectory = VfsPath("gui") / nameW; VfsPaths directories; vfs::GetPathnames(g_VFS, currentDirectory, L"*.xml", directories); for (const VfsPath& directory : directories) gui->LoadXmlFile(directory, inputs); } else { VfsPath directory = VfsPath("gui") / nameW; gui->LoadXmlFile(directory, inputs); } } gui->LoadedXmlFiles(); shared_ptr scriptInterface = gui->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue initDataVal(rq.cx); JS::RootedValue hotloadDataVal(rq.cx); JS::RootedValue global(rq.cx, rq.globalValue()); if (initData) scriptInterface->ReadStructuredClone(initData, &initDataVal); if (hotloadData) scriptInterface->ReadStructuredClone(hotloadData, &hotloadDataVal); if (scriptInterface->HasProperty(global, "init") && - !scriptInterface->CallFunctionVoid(global, "init", initDataVal, hotloadDataVal)) + !ScriptFunction::CallVoid(rq, global, "init", initDataVal, hotloadDataVal)) LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(m_Name)); } void CGUIManager::SGUIPage::SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc) { if (!callbackFunc.isObject()) { LOGERROR("Given callback handler is not an object!"); return; } ScriptRequest rq(scriptInterface); if (!JS_ObjectIsFunction(&callbackFunc.toObject())) { LOGERROR("Given callback handler is not a function!"); return; } callbackFunction = std::make_shared(scriptInterface.GetGeneralJSContext(), callbackFunc); } void CGUIManager::SGUIPage::PerformCallbackFunction(ScriptInterface::StructuredClone args) { if (!callbackFunction) return; shared_ptr scriptInterface = gui->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedObject globalObj(rq.cx, rq.glob); JS::RootedValue funcVal(rq.cx, *callbackFunction); // Delete the callback function, so that it is not called again callbackFunction.reset(); JS::RootedValue argVal(rq.cx); if (args) scriptInterface->ReadStructuredClone(args, &argVal); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(argVal)); JS::RootedValue result(rq.cx); if(!JS_CallFunctionValue(rq.cx, globalObj, funcVal, paramData, &result)) ScriptException::CatchPending(rq); } Status CGUIManager::ReloadChangedFile(const VfsPath& path) { for (SGUIPage& p : m_PageStack) if (p.inputs.find(path) != p.inputs.end()) { LOGMESSAGE("GUI file '%s' changed - reloading page '%s'", path.string8(), utf8_from_wstring(p.m_Name)); p.LoadPage(m_ScriptContext); // 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_ScriptContext); return INFO::OK; } 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"); ScriptRequest rq(*top()->GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); - if (top()->GetScriptInterface()->CallFunction(global, "handleInputBeforeGui", handled, *ev, top()->FindObjectUnderMouse())) + if (ScriptFunction::Call(rq, 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! ScriptRequest rq(*top()->GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); PROFILE("handleInputAfterGui"); - if (top()->GetScriptInterface()->CallFunction(global, "handleInputAfterGui", handled, *ev)) + if (ScriptFunction::Call(rq, global, "handleInputAfterGui", handled, *ev)) if (handled) return IN_HANDLED; } return IN_PASS; } void CGUIManager::SendEventToAll(const CStr& eventName) const { // Save an immutable copy so iterators aren't invalidated by handlers PageStackType pageStack = m_PageStack; for (const SGUIPage& p : pageStack) p.gui->SendEventToAll(eventName); } void CGUIManager::SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const { // Save an immutable copy so iterators aren't invalidated by handlers PageStackType pageStack = m_PageStack; for (const SGUIPage& p : pageStack) p.gui->SendEventToAll(eventName, paramData); } void CGUIManager::TickObjects() { PROFILE3("gui tick"); // We share the script context 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->GetContext()->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(EventNameWindowResized); } } 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/ps/Game.cpp =================================================================== --- ps/trunk/source/ps/Game.cpp (revision 25353) +++ ps/trunk/source/ps/Game.cpp (revision 25354) @@ -1,470 +1,471 @@ /* Copyright (C) 2021 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/World.h" #include "ps/GameSetup/GameSetup.h" #include "renderer/Renderer.h" #include "renderer/TimeManager.h" #include "renderer/WaterManager.h" +#include "scriptinterface/FunctionWrapper.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" #include 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_ScriptContext, 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(); ScriptRequest rq(scriptInterface); 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(); ScriptRequest 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(rq.cx); scriptInterface.GetProperty(attribs, "script", scriptFile); scriptInterface.GetProperty(attribs, "settings", &settings); m_World->RegisterInitRMS(scriptFile, *scriptInterface.GetContext(), settings, m_PlayerID); } else { std::wstring mapFile; JS::RootedValue settings(rq.cx); scriptInterface.GetProperty(attribs, "map", mapFile); scriptInterface.GetProperty(attribs, "settings", &settings); m_World->RegisterInit(mapFile, *scriptInterface.GetContext(), 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(); ScriptRequest rq(scriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); if (scriptInterface->HasProperty(global, "reallyStartGame")) - scriptInterface->CallFunctionVoid(global, "reallyStartGame"); + ScriptFunction::CallVoid(rq, 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/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 25353) +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 25354) @@ -1,1645 +1,1646 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "lib/app_hooks.h" #include "lib/config2.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "lib/file/common/file_stats.h" #include "lib/res/h_mgr.h" #include "lib/res/graphics/cursor.h" #include "graphics/CinemaManager.h" #include "graphics/Color.h" #include "graphics/FontMetrics.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/MapReader.h" #include "graphics/ModelDef.h" #include "graphics/MaterialManager.h" #include "graphics/TerrainTextureManager.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "i18n/L10n.h" #include "maths/MathUtil.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetMessages.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/HWDetect.h" #include "ps/Globals.h" #include "ps/GUID.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/ModIo.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" // psSetLogDir #include "ps/scripting/JSInterface_Console.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/VisualReplay.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/VertexBufferManager.h" #include "renderer/ModelRenderer.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" #include "simulation2/Simulation2.h" #include "lobby/IXmppClient.h" #include "soundmanager/scripting/JSInterface_Sound.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include "tools/atlas/GameInterface/View.h" #if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets #define MUST_INIT_X11 1 #include #else #define MUST_INIT_X11 0 #endif extern void RestartEngine(); #include #include #include #include ERROR_GROUP(System); ERROR_TYPE(System, SDLInitFailed); ERROR_TYPE(System, VmodeFailed); ERROR_TYPE(System, RequiredExtensionsMissing); bool g_DoRenderGui = true; bool g_DoRenderLogger = true; bool g_DoRenderCursor = true; thread_local shared_ptr g_ScriptContext; static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code static const CStr g_EventNameGameLoadProgress = "GameLoadProgress"; bool g_InDevelopmentCopy; bool g_CheckedIfInDevelopmentCopy = false; static void SetTextureQuality(int quality) { int q_flags; GLint filter; retry: // keep this in sync with SANE_TEX_QUALITY_DEFAULT switch(quality) { // worst quality case 0: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_NEAREST; break; // [perf] add bilinear filtering case 1: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] no longer reduce resolution case 2: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] add mipmaps case 3: q_flags = OGL_TEX_HALF_BPP; filter = GL_NEAREST_MIPMAP_LINEAR; break; // [perf] better filtering case 4: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [vmem] no longer reduce bpp case SANE_TEX_QUALITY_DEFAULT: q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [perf] add anisotropy case 6: // TODO: add anisotropic filtering q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // invalid default: debug_warn(L"SetTextureQuality: invalid quality"); quality = SANE_TEX_QUALITY_DEFAULT; // careful: recursion doesn't work and we don't want to duplicate // the "sane" default values. goto retry; } ogl_tex_set_defaults(q_flags, filter); } //---------------------------------------------------------------------------- // GUI integration //---------------------------------------------------------------------------- // display progress / description in loading screen void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task) { const ScriptInterface& scriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface()); ScriptRequest rq(scriptInterface); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(JS::NumberValue(percent))); JS::RootedValue valPendingTask(rq.cx); scriptInterface.ToJSVal(rq, &valPendingTask, pending_task); ignore_result(paramData.append(valPendingTask)); g_GUI->SendEventToAll(g_EventNameGameLoadProgress, paramData); } bool ShouldRender() { return !g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen()); } void Render() { // Do not render if not focused while in fullscreen or minimised, // as that triggers a difficult-to-reproduce crash on some graphic cards. if (!ShouldRender()) return; PROFILE3("render"); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameStart(); ogl_WarnIfError(); // prepare before starting the renderer frame if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->BeginFrame(); if (g_Game) g_Renderer.SetSimulation(g_Game->GetSimulation2()); // start new frame g_Renderer.BeginFrame(); ogl_WarnIfError(); if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->Render(); ogl_WarnIfError(); g_Renderer.RenderTextOverlays(); // If we're in Atlas game view, render special tools if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawCinemaPathTool(); ogl_WarnIfError(); } if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->GetCinema()->Render(); ogl_WarnIfError(); if (g_DoRenderGui) { // All GUI elements are drawn in Z order to render semi-transparent // objects correctly. glDisable(GL_DEPTH_TEST); g_GUI->Draw(); glEnable(GL_DEPTH_TEST); } ogl_WarnIfError(); // If we're in Atlas game view, render special overlays (e.g. editor bandbox) if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawOverlays(); ogl_WarnIfError(); } // Text: glDisable(GL_DEPTH_TEST); g_Console->Render(); ogl_WarnIfError(); if (g_DoRenderLogger) g_Logger->Render(); ogl_WarnIfError(); // Profile information g_ProfileViewer.RenderProfile(); ogl_WarnIfError(); // Draw the cursor (or set the Windows cursor, on Windows) if (g_DoRenderCursor) { PROFILE3_GPU("cursor"); CStrW cursorName = g_CursorName; if (cursorName.empty()) { cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y, g_GuiScale, false); } else { bool forceGL = false; CFG_GET_VAL("nohwcursor", forceGL); #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // set up transform for GL cursor glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); CMatrix3D transform; transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f); glLoadMatrixf(&transform._11); #endif #if OS_ANDROID #warning TODO: cursors for Android #else if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, g_GuiScale, forceGL) < 0) LOGWARNING("Failed to draw cursor '%s'", utf8_from_wstring(cursorName)); #endif #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // restore transform glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); #endif } } glEnable(GL_DEPTH_TEST); g_Renderer.EndFrame(); PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls); PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris); PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris); PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris); PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris); PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats); PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameEnd(); ogl_WarnIfError(); } ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags)) { // If we're fullscreen, then sometimes (at least on some particular drivers on Linux) // displaying the error dialog hangs the desktop since the dialog box is behind the // fullscreen window. So we just force the game to windowed mode before displaying the dialog. // (But only if we're in the main thread, and not if we're being reentrant.) if (Threading::IsMainThread()) { static bool reentering = false; if (!reentering) { reentering = true; g_VideoMode.SetFullscreen(false); reentering = false; } } // We don't actually implement the error display here, so return appropriately return ERI_NOT_IMPLEMENTED; } const std::vector& GetMods(const CmdLineArgs& args, int flags) { const bool init_mods = (flags & INIT_MODS) == INIT_MODS; const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC; if (!init_mods) return g_modsLoaded; g_modsLoaded = args.GetMultiple("mod"); if (add_public) g_modsLoaded.insert(g_modsLoaded.begin(), "public"); g_modsLoaded.insert(g_modsLoaded.begin(), "mod"); return g_modsLoaded; } void MountMods(const Paths& paths, const std::vector& mods) { OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE; size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; size_t priority = 0; for (size_t i = 0; i < mods.size(); ++i) { priority = i + 1; // Mods are higher priority than regular mountings, which default to priority 0 OsPath modName(mods[i]); // Only mount mods from the user path if they don't exist in the 'rdata' path. if (DirectoryExists(modPath / modName / "")) g_VFS->Mount(L"", modPath / modName / "", baseFlags, priority); else g_VFS->Mount(L"", modUserPath / modName / "", userFlags, priority); } // Mount the user mod last. In dev copy, mount it with a low priority. Otherwise, make it writable. g_VFS->Mount(L"", modUserPath / "user" / "", userFlags, InDevelopmentCopy() ? 0 : priority + 1); } static void InitVfs(const CmdLineArgs& args, int flags) { TIMER(L"InitVfs"); const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0; const Paths paths(args); OsPath logs(paths.Logs()); CreateDirectories(logs, 0700); psSetLogDir(logs); // desired location for crashlog is now known. update AppHooks ASAP // (particularly before the following error-prone operations): AppHooks hooks = {0}; hooks.bundle_logs = psBundleLogs; hooks.get_log_dir = psLogDir; if (setup_error) hooks.display_error = psDisplayError; app_hooks_update(&hooks); g_VFS = CreateVfs(); const OsPath readonlyConfig = paths.RData()/"config"/""; // Mount these dirs with highest priority so that mods can't overwrite them. g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY); // (adding XMBs to archive speeds up subsequent reads) if (readonlyConfig != paths.Config()) g_VFS->Mount(L"config/", readonlyConfig, 0, VFS_MAX_PRIORITY-1); g_VFS->Mount(L"config/", paths.Config(), 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"", 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH, VFS_MAX_PRIORITY); // Engine localization files (regular priority, these can be overwritten). g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); MountMods(paths, GetMods(args, flags)); // note: don't bother with g_VFS->TextRepresentation - directories // haven't yet been populated and are empty. } static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData) { { // console TIMER(L"ps_console"); g_Console->UpdateScreenSize(g_xres, g_yres); // Calculate and store the line spacing CFontMetrics font(CStrIntern(CONSOLE_FONT)); g_Console->m_iFontHeight = font.GetLineSpacing(); g_Console->m_iFontWidth = font.GetCharacterWidth(L'C'); g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth); // Offset by an arbitrary amount, to make it fit more nicely g_Console->m_iFontOffset = 7; double blinkRate = 0.5; CFG_GET_VAL("gui.cursorblinkrate", blinkRate); g_Console->SetCursorBlinkRate(blinkRate); } // hotkeys { TIMER(L"ps_lang_hotkeys"); LoadHotkeys(g_ConfigDB); } if (!setup_gui) { // We do actually need *some* kind of GUI loaded, so use the // (currently empty) Atlas one g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData); return; } // GUI uses VFS, so this must come after VFS init. g_GUI->SwitchPage(gui_page, srcScriptInterface, initData); } void InitPsAutostart(bool networked, JS::HandleValue attrs) { // The GUI has not been initialized yet, so use the simulation scriptinterface for this variable ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue playerAssignments(rq.cx); ScriptInterface::CreateObject(rq, &playerAssignments); if (!networked) { JS::RootedValue localPlayer(rq.cx); ScriptInterface::CreateObject(rq, &localPlayer, "player", g_Game->GetPlayerID()); scriptInterface.SetProperty(playerAssignments, "local", localPlayer); } JS::RootedValue sessionInitData(rq.cx); ScriptInterface::CreateObject( rq, &sessionInitData, "attribs", attrs, "playerAssignments", playerAssignments); InitPs(true, L"page_loading.xml", &scriptInterface, sessionInitData); } void InitInput() { g_Joystick.Initialise(); // register input handlers // This stack is constructed so the first added, will be the last // one called. This is important, because each of the handlers // has the potential to block events to go further down // in the chain. I.e. the last one in the list added, is the // only handler that can block all messages before they are // processed. in_add_handler(game_view_handler); in_add_handler(CProfileViewer::InputThunk); in_add_handler(HotkeyInputActualHandler); // gui_handler needs to be registered after (i.e. called before!) the // hotkey handler so that input boxes can be typed in without // setting off hotkeys. in_add_handler(gui_handler); // Likewise for the console. in_add_handler(conInputHandler); in_add_handler(touch_input_handler); // Should be called after scancode map update (i.e. after the global input, but before UI). // This never blocks the event, but it does some processing necessary for hotkeys, // which are triggered later down the input chain. // (by calling this before the UI, we can use 'EventWouldTriggerHotkey' in the UI). in_add_handler(HotkeyInputPrepHandler); // These two must be called first (i.e. pushed last) // GlobalsInputHandler deals with some important global state, // such as which scancodes are being pressed, mouse buttons pressed, etc. // while HotkeyStateChange updates the map of active hotkeys. in_add_handler(GlobalsInputHandler); in_add_handler(HotkeyStateChange); } static void ShutdownPs() { SAFE_DELETE(g_GUI); UnloadHotkeys(); // disable the special Windows cursor, or free textures for OGL cursors cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, 1.0, false); } static void InitRenderer() { TIMER(L"InitRenderer"); // create renderer new CRenderer; // create terrain related stuff new CTerrainTextureManager; g_Renderer.Open(g_xres, g_yres); // Setup lighting environment. Since the Renderer accesses the // lighting environment through a pointer, this has to be done before // the first Frame. g_Renderer.SetLightEnv(&g_LightEnv); // I haven't seen the camera affecting GUI rendering and such, but the // viewport has to be updated according to the video mode SViewPort vp; vp.m_X = 0; vp.m_Y = 0; vp.m_Width = g_xres; vp.m_Height = g_yres; g_Renderer.SetViewport(vp); ModelDefActivateFastImpl(); ColorActivateFastImpl(); ModelRenderer::Init(); } static void InitSDL() { #if OS_LINUX // In fullscreen mode when SDL is compiled with DGA support, the mouse // sensitivity often appears to be unusably wrong (typically too low). // (This seems to be reported almost exclusively on Ubuntu, but can be // reproduced on Gentoo after explicitly enabling DGA.) // Disabling the DGA mouse appears to fix that problem, and doesn't // have any obvious negative effects. setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0); #endif if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0) { LOGERROR("SDL library initialization failed: %s", SDL_GetError()); throw PSERROR_System_SDLInitFailed(); } atexit(SDL_Quit); // Text input is active by default, disable it until it is actually needed. SDL_StopTextInput(); #if SDL_VERSION_ATLEAST(2, 0, 9) // SDL2 >= 2.0.9 defaults to 32 pixels (to support touch screens) but that can break our double-clicking. SDL_SetHint(SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS, "1"); #endif #if OS_MACOSX // Some Mac mice only have one button, so they can't right-click // but SDL2 can emulate that with Ctrl+Click bool macMouse = false; CFG_GET_VAL("macmouse", macMouse); SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0"); #endif } static void ShutdownSDL() { SDL_Quit(); } void EndGame() { SAFE_DELETE(g_NetClient); SAFE_DELETE(g_NetServer); SAFE_DELETE(g_Game); if (CRenderer::IsInitialised()) { ISoundManager::CloseGame(); g_Renderer.ResetState(); } } void Shutdown(int flags) { const bool hasRenderer = CRenderer::IsInitialised(); if ((flags & SHUTDOWN_FROM_CONFIG)) goto from_config; EndGame(); SAFE_DELETE(g_XmppClient); SAFE_DELETE(g_ModIo); ShutdownPs(); TIMER_BEGIN(L"shutdown TexMan"); delete &g_TexMan; TIMER_END(L"shutdown TexMan"); if (hasRenderer) { TIMER_BEGIN(L"shutdown Renderer"); g_Renderer.~CRenderer(); g_VBMan.Shutdown(); TIMER_END(L"shutdown Renderer"); } g_RenderingOptions.ClearHooks(); g_Profiler2.ShutdownGPU(); // Free cursors before shutting down SDL, as they may depend on SDL. cursor_shutdown(); TIMER_BEGIN(L"shutdown SDL"); ShutdownSDL(); TIMER_END(L"shutdown SDL"); if (hasRenderer) g_VideoMode.Shutdown(); TIMER_BEGIN(L"shutdown UserReporter"); g_UserReporter.Deinitialize(); TIMER_END(L"shutdown UserReporter"); // Cleanup curl now that g_ModIo and g_UserReporter have been shutdown. curl_global_cleanup(); delete &g_L10n; from_config: TIMER_BEGIN(L"shutdown ConfigDB"); CConfigDB::Shutdown(); TIMER_END(L"shutdown ConfigDB"); SAFE_DELETE(g_Console); // This is needed to ensure that no callbacks from the JSAPI try to use // the profiler when it's already destructed g_ScriptContext.reset(); // resource // first shut down all resource owners, and then the handle manager. TIMER_BEGIN(L"resource modules"); ISoundManager::SetEnabled(false); g_VFS.reset(); // this forcibly frees all open handles (thus preventing real leaks), // and makes further access to h_mgr impossible. h_mgr_shutdown(); file_stats_dump(); TIMER_END(L"resource modules"); TIMER_BEGIN(L"shutdown misc"); timer_DisplayClientTotals(); CNetHost::Deinitialize(); // should be last, since the above use them SAFE_DELETE(g_Logger); delete &g_Profiler; delete &g_ProfileViewer; SAFE_DELETE(g_ScriptStatsTable); TIMER_END(L"shutdown misc"); } #if OS_UNIX static void FixLocales() { #if OS_MACOSX || OS_BSD // OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle // wide characters. Peculiarly the string "UTF-8" seems to be acceptable // despite not being a real locale, and it's conveniently language-agnostic, // so use that. setlocale(LC_CTYPE, "UTF-8"); #endif // On misconfigured systems with incorrect locale settings, we'll die // with a C++ exception when some code (e.g. Boost) tries to use locales. // To avoid death, we'll detect the problem here and warn the user and // reset to the default C locale. // For informing the user of the problem, use the list of env vars that // glibc setlocale looks at. (LC_ALL is checked first, and LANG last.) const char* const LocaleEnvVars[] = { "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LANG" }; try { // this constructor is similar to setlocale(LC_ALL, ""), // but instead of returning NULL, it throws runtime_error // when the first locale env variable found contains an invalid value std::locale(""); } catch (std::runtime_error&) { LOGWARNING("Invalid locale settings"); for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++) { if (char* envval = getenv(LocaleEnvVars[i])) LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval); else LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]); } // We should set LC_ALL since it overrides LANG if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1)) debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable."); else LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL")); } } #else static void FixLocales() { // Do nothing on Windows } #endif void EarlyInit() { // If you ever want to catch a particular allocation: //_CrtSetBreakAlloc(232647); Threading::SetMainThread(); debug_SetThreadName("main"); // add all debug_printf "tags" that we are interested in: debug_filter_add("TIMER"); timer_Init(); // initialise profiler early so it can profile startup, // but only after LatchStartTime g_Profiler2.Initialise(); FixLocales(); // Because we do GL calls from a secondary thread, Xlib needs to // be told to support multiple threads safely. // This is needed for Atlas, but we have to call it before any other // Xlib functions (e.g. the ones used when drawing the main menu // before launching Atlas) #if MUST_INIT_X11 int status = XInitThreads(); if (status == 0) debug_printf("Error enabling thread-safety via XInitThreads\n"); #endif // Initialise the low-quality rand function srand(time(NULL)); // NOTE: this rand should *not* be used for simulation! } bool Autostart(const CmdLineArgs& args); /** * Returns true if the user has intended to start a visual replay from command line. */ bool AutostartVisualReplay(const std::string& replayFile); bool Init(const CmdLineArgs& args, int flags) { h_mgr_init(); // Do this as soon as possible, because it chdirs // and will mess up the error reporting if anything // crashes before the working directory is set. InitVfs(args, flags); // This must come after VFS init, which sets the current directory // (required for finding our output log files). g_Logger = new CLogger; new CProfileViewer; new CProfileManager; // before any script code g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); // Set up the console early, so that debugging // messages can be logged to it. (The console's size // and fonts are set later in InitPs()) g_Console = new CConsole(); // g_ConfigDB, command line args, globals CONFIG_Init(args); // Using a global object for the context is a workaround until Simulation and AI use // their own threads and also their own contexts. const int contextSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger); Mod::CacheEnabledModVersions(g_ScriptContext); // Special command-line mode to dump the entity schemas instead of running the game. // (This must be done after loading VFS etc, but should be done before wasting time // on anything else.) if (args.Has("dumpSchema")) { CSimulation2 sim(NULL, g_ScriptContext, NULL); sim.LoadDefaultScripts(); std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc); f << sim.GenerateSchema(); std::cout << "Generated entity.rng\n"; exit(0); } CNetHost::Initialize(); #if CONFIG2_AUDIO if (!args.Has("autostart-nonvisual") && !g_DisableAudio) ISoundManager::CreateSoundManager(); #endif // Check if there are mods specified on the command line, // or if we already set the mods (~INIT_MODS), // else check if there are mods that should be loaded specified // in the config and load those (by aborting init and restarting // the engine). if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS) { CStr modstring; CFG_GET_VAL("mod.enabledmods", modstring); if (!modstring.empty()) { std::vector mods; boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on); std::swap(g_modsLoaded, mods); // Abort init and restart RestartEngine(); return false; } } new L10n; // Optionally start profiler HTTP output automatically // (By default it's only enabled by a hotkey, for security/performance) bool profilerHTTPEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable); if (profilerHTTPEnable) g_Profiler2.EnableHTTP(); // Initialise everything except Win32 sockets (because our networking // system already inits those) curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32); if (!g_Quickstart) g_UserReporter.Initialize(); // after config PROFILE2_EVENT("Init finished"); return true; } void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; if(setup_vmode) { InitSDL(); if (!g_VideoMode.InitSDL()) throw PSERROR_System_VmodeFailed(); // abort startup } RunHardwareDetection(); const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file SetTextureQuality(quality); ogl_WarnIfError(); // Optionally start profiler GPU timings automatically // (By default it's only enabled by a hotkey, for performance/compatibility) bool profilerGPUEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable); if (profilerGPUEnable) g_Profiler2.EnableGPU(); if(!g_Quickstart) { WriteSystemInfo(); // note: no longer vfs_display here. it's dog-slow due to unbuffered // file output and very rarely needed. } if(g_DisableAudio) ISoundManager::SetEnabled(false); g_GUI = new CGUIManager(); // (must come after SetVideoMode, since it calls ogl_Init) CStr8 renderPath = "default"; CFG_GET_VAL("renderpath", renderPath); if ((ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB && ogl_HaveExtensions(0, "GL_ARB_vertex_shader", "GL_ARB_fragment_shader", NULL) != 0) // GLSL || RenderPathEnum::FromString(renderPath) == FIXED) { // It doesn't make sense to continue working here, because we're not // able to display anything. DEBUG_DISPLAY_FATAL_ERROR( L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders." L" The game does not support pre-shader graphics cards." L" You are advised to try installing newer drivers and/or upgrade your graphics card." L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734" ); } const char* missing = ogl_HaveExtensions(0, "GL_ARB_multitexture", "GL_EXT_draw_range_elements", "GL_ARB_texture_env_combine", "GL_ARB_texture_env_dot3", NULL); if(missing) { wchar_t buf[500]; swprintf_s(buf, ARRAY_SIZE(buf), L"The %hs extension doesn't appear to be available on your computer." L" The game may still work, though - you are welcome to try at your own risk." L" If not or it doesn't look right, upgrade your graphics card.", missing ); DEBUG_DISPLAY_ERROR(buf); // TODO: i18n } if (!ogl_HaveExtension("GL_ARB_texture_env_crossbar")) { DEBUG_DISPLAY_ERROR( L"The GL_ARB_texture_env_crossbar extension doesn't appear to be available on your computer." L" Shadows are not available and overall graphics quality might suffer." L" You are advised to try installing newer drivers and/or upgrade your graphics card."); g_ConfigDB.SetValueBool(CFG_HWDETECT, "shadows", false); } ogl_WarnIfError(); g_RenderingOptions.ReadConfigAndSetupHooks(); InitRenderer(); InitInput(); ogl_WarnIfError(); // TODO: Is this the best place for this? if (VfsDirectoryExists(L"maps/")) CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng"); try { if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args)) { const bool setup_gui = ((flags & INIT_NO_GUI) == 0); // We only want to display the splash screen at startup shared_ptr scriptInterface = g_GUI->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue data(rq.cx); if (g_GUI) { ScriptInterface::CreateObject(rq, &data, "isStartup", true); if (!installedMods.empty()) scriptInterface->SetProperty(data, "installedMods", installedMods); } InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data); } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map Loading failed // Start the engine so we have a GUI InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue); // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } } void InitNonVisual(const CmdLineArgs& args) { // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); Autostart(args); } void RenderGui(bool RenderingState) { g_DoRenderGui = RenderingState; } void RenderLogger(bool RenderingState) { g_DoRenderLogger = RenderingState; } void RenderCursor(bool RenderingState) { g_DoRenderCursor = RenderingState; } /** * Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON * data from it. * The scenario map format is used for scenario and skirmish map types (random * games do not use a "map" (format) but a small JavaScript program which * creates a map on the fly). It contains a section to initialize the game * setup screen. * @param mapPath Absolute path (from VFS root) to the map file to peek in. * @return ScriptSettings in JSON format extracted from the map. */ CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath) { CXeromyces mapFile; const char *pathToSettings[] = { "Scenario", "ScriptSettings", "" // Path to JSON data in map }; Status loadResult = mapFile.Load(g_VFS, mapPath); if (INFO::OK != loadResult) { LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8()); throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos."); } XMBElement mapElement = mapFile.GetRoot(); // Select the ScriptSettings node in the map file... for (int i = 0; pathToSettings[i][0]; ++i) { int childId = mapFile.GetElementID(pathToSettings[i]); XMBElementList nodes = mapElement.GetChildNodes(); auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) { return child.GetNodeName() == childId; }); if (it != nodes.end()) mapElement = *it; } // ... they contain a JSON document to initialize the game setup // screen return mapElement.GetText(); } /* * Command line options for autostart * (keep synchronized with binaries/system/readme.txt): * * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; * TYPEDIR is skirmishes, scenarios, or random * -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random) * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI * (0: sandbox, 5: very hard) * -autostart-aiseed=AISEED sets the seed used for the AI random * generator (default 0, use -1 for random) * -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer) * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV * (skirmish and random maps only) * -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2). * -autostart-ceasefire=NUM sets a ceasefire duration NUM * (default 0 minutes) * -autostart-nonvisual disable any graphics and sounds * -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME * located in simulation/data/settings/victory_conditions/ * (default conquest). When the first given SCRIPTNAME is * "endless", no victory conditions will apply. * -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition * (default 10 minutes) * -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition * (default 10 minutes) * -autostart-reliccount=NUM sets the number of relics for relic victory condition * (default 2 relics) * -autostart-disable-replay disable saving of replays * * Multiplayer: * -autostart-playername=NAME sets local player NAME (default 'anonymous') * -autostart-host sets multiplayer host mode * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer * game (default 2) * -autostart-client=IP sets multiplayer client to join host at * given IP address * Random maps only: * -autostart-size=TILES sets random map size in TILES (default 192) * -autostart-players=NUMBER sets NUMBER of players on random map * (default 2) * * Examples: * 1) "Bob" will host a 2 player game on the Arcadia map: * -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" * "Alice" joins the match as player 2: * -autostart="scenarios/Arcadia" -autostart-client=127.0.0.1 -autostart-playername="Alice" * The players use the developer overlay to control players. * * 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: * -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra * * 3) Observe the PetraBot on a triggerscript map: * -autostart="random/jebel_barkal" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=1:petra -autostart-ai=2:petra -autostart-player=-1 */ bool Autostart(const CmdLineArgs& args) { CStr autoStartName = args.Get("autostart"); if (autoStartName.empty()) return false; g_Game = new CGame(!args.Has("autostart-disable-replay")); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx); JS::RootedValue settings(rq.cx); JS::RootedValue playerData(rq.cx); ScriptInterface::CreateObject(rq, &attrs); ScriptInterface::CreateObject(rq, &settings); ScriptInterface::CreateArray(rq, &playerData); // The directory in front of the actual map name indicates which type // of map is being loaded. Drawback of this approach is the association // of map types and folders is hard-coded, but benefits are: // - No need to pass the map type via command line separately // - Prevents mixing up of scenarios and skirmish maps to some degree Path mapPath = Path(autoStartName); std::wstring mapDirectory = mapPath.Parent().Filename().string(); std::string mapType; if (mapDirectory == L"random") { // Random map definition will be loaded from JSON file, so we need to parse it std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json"; JS::RootedValue scriptData(rq.cx); scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings)) { // JSON loaded ok - copy script name over to game attributes std::wstring scriptFile; scriptInterface.GetProperty(settings, "Script", scriptFile); scriptInterface.SetProperty(attrs, "script", scriptFile); // RMS filename } else { // Problem with JSON file LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details."); } // Get optional map size argument (default 192) uint mapSize = 192; if (args.Has("autostart-size")) { CStr size = args.Get("autostart-size"); mapSize = size.ToUInt(); } scriptInterface.SetProperty(settings, "Size", mapSize); // Random map size (in patches) // Get optional number of players (default 2) size_t numPlayers = 2; if (args.Has("autostart-players")) { CStr num = args.Get("autostart-players"); numPlayers = num.ToUInt(); } // Set up player data for (size_t i = 0; i < numPlayers; ++i) { JS::RootedValue player(rq.cx); // We could load player_defaults.json here, but that would complicate the logic // even more and autostart is only intended for developers anyway ScriptInterface::CreateObject(rq, &player, "Civ", "athen"); scriptInterface.SetPropertyInt(playerData, i, player); } mapType = "random"; } else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // Initialize general settings from the map data so some values // (e.g. name of map) are always present, even when autostart is // partially configured CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml"); scriptInterface.ParseJSON(mapSettingsJSON, &settings); // Initialize the playerData array being modified by autostart // with the real map data, so sensible values are present: scriptInterface.GetProperty(settings, "PlayerData", &playerData); if (mapDirectory == L"scenarios") mapType = "scenario"; else mapType = "skirmish"; } else { LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory)); throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types."); } scriptInterface.SetProperty(attrs, "mapType", mapType); scriptInterface.SetProperty(attrs, "map", "maps/" + autoStartName); scriptInterface.SetProperty(settings, "mapType", mapType); scriptInterface.SetProperty(settings, "CheatsEnabled", true); // The seed is used for both random map generation and simulation u32 seed = 0; if (args.Has("autostart-seed")) { CStr seedArg = args.Get("autostart-seed"); if (seedArg == "-1") seed = rand(); else seed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "Seed", seed); // Set seed for AIs u32 aiseed = 0; if (args.Has("autostart-aiseed")) { CStr seedArg = args.Get("autostart-aiseed"); if (seedArg == "-1") aiseed = rand(); else aiseed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "AISeed", aiseed); // Set player data for AIs // attrs.settings = { PlayerData: [ { AI: ... }, ... ] } // or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set int offset = 1; JS::RootedValue player(rq.cx); if (scriptInterface.GetPropertyInt(playerData, 0, &player) && player.isNull()) offset = 0; // Set teams if (args.Has("autostart-team")) { std::vector civArgs = args.GetMultiple("autostart-team"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } int teamID = civArgs[i].AfterFirst(":").ToInt() - 1; scriptInterface.SetProperty(currentPlayer, "Team", teamID); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } int ceasefire = 0; if (args.Has("autostart-ceasefire")) ceasefire = args.Get("autostart-ceasefire").ToInt(); scriptInterface.SetProperty(settings, "Ceasefire", ceasefire); if (args.Has("autostart-ai")) { std::vector aiArgs = args.GetMultiple("autostart-ai"); for (size_t i = 0; i < aiArgs.size(); ++i) { int playerID = aiArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } scriptInterface.SetProperty(currentPlayer, "AI", aiArgs[i].AfterFirst(":")); scriptInterface.SetProperty(currentPlayer, "AIDiff", 3); scriptInterface.SetProperty(currentPlayer, "AIBehavior", "balanced"); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } // Set AI difficulty if (args.Has("autostart-aidiff")) { std::vector civArgs = args.GetMultiple("autostart-aidiff"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } scriptInterface.SetProperty(currentPlayer, "AIDiff", civArgs[i].AfterFirst(":").ToInt()); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } // Set player data for Civs if (args.Has("autostart-civ")) { if (mapDirectory != L"scenarios") { std::vector civArgs = args.GetMultiple("autostart-civ"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } scriptInterface.SetProperty(currentPlayer, "Civ", civArgs[i].AfterFirst(":")); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } else LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios"); } // Add player data to map settings scriptInterface.SetProperty(settings, "PlayerData", playerData); // Add map settings to game attributes scriptInterface.SetProperty(attrs, "settings", settings); // Get optional playername CStrW userName = L"anonymous"; if (args.Has("autostart-playername")) userName = args.Get("autostart-playername").FromUTF8(); // Add additional scripts to the TriggerScripts property std::vector triggerScriptsVector; JS::RootedValue triggerScripts(rq.cx); if (scriptInterface.HasProperty(settings, "TriggerScripts")) { scriptInterface.GetProperty(settings, "TriggerScripts", &triggerScripts); FromJSVal_vector(rq, triggerScripts, triggerScriptsVector); } if (!CRenderer::IsInitialised()) { CStr nonVisualScript = "scripts/NonVisualTrigger.js"; triggerScriptsVector.push_back(nonVisualScript.FromUTF8()); } std::vector victoryConditions(1, "conquest"); if (args.Has("autostart-victory")) victoryConditions = args.GetMultiple("autostart-victory"); if (victoryConditions.size() == 1 && victoryConditions[0] == "endless") victoryConditions.clear(); scriptInterface.SetProperty(settings, "VictoryConditions", victoryConditions); for (const CStr& victory : victoryConditions) { JS::RootedValue scriptData(rq.cx); JS::RootedValue data(rq.cx); JS::RootedValue victoryScripts(rq.cx); CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + victory.FromUTF8() + L".json"; scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "Data", &data) && !data.isUndefined() && scriptInterface.GetProperty(data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined()) { std::vector victoryScriptsVector; FromJSVal_vector(rq, victoryScripts, victoryScriptsVector); triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end()); } else { LOGERROR("Autostart: Error reading victory script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading victory script.\nCheck application log for details."); } } ToJSVal_vector(rq, &triggerScripts, triggerScriptsVector); scriptInterface.SetProperty(settings, "TriggerScripts", triggerScripts); int wonderDuration = 10; if (args.Has("autostart-wonderduration")) wonderDuration = args.Get("autostart-wonderduration").ToInt(); scriptInterface.SetProperty(settings, "WonderDuration", wonderDuration); int relicDuration = 10; if (args.Has("autostart-relicduration")) relicDuration = args.Get("autostart-relicduration").ToInt(); scriptInterface.SetProperty(settings, "RelicDuration", relicDuration); int relicCount = 2; if (args.Has("autostart-reliccount")) relicCount = args.Get("autostart-reliccount").ToInt(); scriptInterface.SetProperty(settings, "RelicCount", relicCount); if (args.Has("autostart-host")) { InitPsAutostart(true, attrs); size_t maxPlayers = 2; if (args.Has("autostart-host-players")) maxPlayers = args.Get("autostart-host-players").ToUInt(); // Generate a secret to identify the host client. std::string secret = ps_generate_guid(); g_NetServer = new CNetServer(false, maxPlayers); g_NetServer->SetControllerSecret(secret); g_NetServer->UpdateInitAttributes(&attrs, scriptInterface); bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT); ENSURE(ok); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); g_NetClient->SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false); g_NetClient->SetControllerSecret(secret); g_NetClient->SetupConnection(nullptr); } else if (args.Has("autostart-client")) { InitPsAutostart(true, attrs); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); CStr ip = args.Get("autostart-client"); if (ip.empty()) ip = "127.0.0.1"; g_NetClient->SetupServerData(ip, PS_DEFAULT_PORT, false); ENSURE(g_NetClient->SetupConnection(nullptr)); } else { g_Game->SetPlayerID(args.Has("autostart-player") ? args.Get("autostart-player").ToInt() : 1); g_Game->StartGame(&attrs, ""); if (CRenderer::IsInitialised()) { InitPsAutostart(false, attrs); } else { // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK); } } return true; } bool AutostartVisualReplay(const std::string& replayFile) { if (!FileExists(OsPath(replayFile))) return false; g_Game = new CGame(false); g_Game->SetPlayerID(-1); g_Game->StartVisualReplay(replayFile); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx, g_Game->GetSimulation2()->GetInitAttributes()); InitPsAutostart(false, attrs); return true; } void CancelLoad(const CStrW& message) { shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(pScriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); LDR_Cancel(); if (g_GUI && g_GUI->GetPageCount() && pScriptInterface->HasProperty(global, "cancelOnLoadGameError")) - pScriptInterface->CallFunctionVoid(global, "cancelOnLoadGameError", message); + ScriptFunction::CallVoid(rq, global, "cancelOnLoadGameError", message); } bool InDevelopmentCopy() { if (!g_CheckedIfInDevelopmentCopy) { g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); g_CheckedIfInDevelopmentCopy = true; } return g_InDevelopmentCopy; } Index: ps/trunk/source/ps/GameSetup/HWDetect.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/HWDetect.cpp (revision 25353) +++ ps/trunk/source/ps/GameSetup/HWDetect.cpp (revision 25354) @@ -1,660 +1,660 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "lib/ogl.h" #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/topology.h" #endif #if CONFIG2_AUDIO #include "soundmanager/SoundManager.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" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #if OS_LINUX #include #endif // 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); void SetDisableAudio(bool disabled) { g_DisableAudio = disabled; } void RunHardwareDetection() { TIMER(L"RunHardwareDetection"); ScriptInterface scriptInterface("Engine", "HWDetect", g_ScriptContext); ScriptRequest rq(scriptInterface); JSI_Debug::RegisterScriptFunctions(scriptInterface); // Engine.DisplayErrorDialog JSI_ConfigDB::RegisterScriptFunctions(scriptInterface); ScriptFunction::Register(rq, "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(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); scriptInterface.SetProperty(settings, "arch_e2k", ARCH_E2K); scriptInterface.SetProperty(settings, "arch_ppc64", ARCH_PPC64); #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 if (g_SoundManager) { scriptInterface.SetProperty(settings, "snd_card", g_SoundManager->GetSoundCardNames()); scriptInterface.SetProperty(settings, "snd_drv_ver", g_SoundManager->GetOpenALVersion()); } #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()); #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); #endif scriptInterface.SetProperty(settings, "timer_resolution", timer_Resolution()); // The version should be increased for every meaningful change. const int reportVersion = 14; // 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(rq.cx, rq.globalValue()); - scriptInterface.CallFunctionVoid(global, "RunHardwareDetection", settings); + ScriptFunction::CallVoid(rq, 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/scriptinterface/FunctionWrapper.h =================================================================== --- ps/trunk/source/scriptinterface/FunctionWrapper.h (revision 25353) +++ ps/trunk/source/scriptinterface/FunctionWrapper.h (revision 25354) @@ -1,330 +1,423 @@ /* Copyright (C) 2021 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_FUNCTIONWRAPPER #define INCLUDED_FUNCTIONWRAPPER #include "ScriptInterface.h" #include "ScriptExceptions.h" /** * This class introduces templates to conveniently wrap C++ functions in JSNative functions. * This _is_ rather template heavy, so compilation times beware. * The C++ code can have arbitrary arguments and arbitrary return types, so long * as they can be converted to/from JS using ScriptInterface::ToJSVal (FromJSVal respectively), * and they are default-constructible (TODO: that can probably changed). * (This could be a namespace, but I like being able to specify public/private). */ class ScriptFunction { private: ScriptFunction() = delete; ScriptFunction(const ScriptFunction&) = delete; ScriptFunction(ScriptFunction&&) = delete; /** * In JS->C++ calls, types are converted using FromJSVal, * and this requires them to be default-constructible (as that function takes an out parameter) * thus constref needs to be removed when defining the tuple. * Exceptions are: * - const ScriptRequest& (as the first argument only, for implementation simplicity). * - const ScriptInterface& (as the first argument only, for implementation simplicity). * - JS::HandleValue */ template using type_transform = std::conditional_t< std::is_same_v || std::is_same_v, T, std::remove_const_t> >; /** * Convenient struct to get info on a [class] [const] function pointer. * TODO VS19: I ran into a really weird bug with an auto specialisation on this taking function pointers. * It'd be good to add it back once we upgrade. */ template struct args_info; template struct args_info { static constexpr const size_t nb_args = sizeof...(Types); using return_type = R; using object_type = void; using arg_types = std::tuple...>; }; template struct args_info : public args_info { using object_type = C; }; template struct args_info : public args_info {}; /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// /** * DoConvertFromJS takes a type, a JS argument, and converts. * The type T must be default constructible (except for HandleValue, which is handled specially). * (possible) TODO: this could probably be changed if FromJSVal had a different signature. * @param went_ok - true if the conversion succeeded and went_ok was true before, false otherwise. */ template static std::tuple DoConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok) { // No need to convert JS values. if constexpr (std::is_same_v) { // Default-construct values that aren't passed by JS. // TODO: this should perhaps be removed, as it's distinct from C++ default values and kind of tricky. if (idx >= args.length()) return std::forward_as_tuple(JS::UndefinedHandleValue); else { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(rq); UNUSED2(args); UNUSED2(went_ok); return std::forward_as_tuple(args[idx]); // This passes the null handle value if idx is beyond the length of args. } } else { // Default-construct values that aren't passed by JS. // TODO: this should perhaps be removed, as it's distinct from C++ default values and kind of tricky. if (idx >= args.length()) return std::forward_as_tuple(T{}); else { T ret; went_ok &= ScriptInterface::FromJSVal(rq, args[idx], ret); return std::forward_as_tuple(ret); } } } /** * Recursive wrapper: calls DoConvertFromJS for type T and recurses. */ template static std::tuple DoConvertFromJS(const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok) { return std::tuple_cat(DoConvertFromJS(rq, args, went_ok), DoConvertFromJS(rq, args, went_ok)); } /** * ConvertFromJS is a wrapper around DoConvertFromJS, and serves to: * - unwrap the tuple types as a parameter pack * - handle specific cases for the first argument (cmptPrivate, ScriptRequest). * * Trick: to unpack the types of the tuple as a parameter pack, we deduce them from the function signature. * To do that, we want the tuple in the arguments, but we don't want to actually have to default-instantiate, * so we'll pass a nullptr that's static_cast to what we want. */ template static std::tuple ConvertFromJS(ScriptInterface::CmptPrivate*, const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple*) { if constexpr (sizeof...(Types) == 0) { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(rq); UNUSED2(args); UNUSED2(went_ok); return {}; } else return DoConvertFromJS<0, Types...>(rq, args, went_ok); } // Overloads for CmptPrivate* first argument. template static std::tuple ConvertFromJS(ScriptInterface::CmptPrivate* cmptPrivate, const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple*) { if constexpr (sizeof...(Types) == 0) { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(rq); UNUSED2(args); UNUSED2(went_ok); return std::forward_as_tuple(cmptPrivate); } else return std::tuple_cat(std::forward_as_tuple(cmptPrivate), DoConvertFromJS<0, Types...>(rq, args, went_ok)); } // Overloads for ScriptRequest& first argument. template static std::tuple ConvertFromJS(ScriptInterface::CmptPrivate*, const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple*) { if constexpr (sizeof...(Types) == 0) { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(args); UNUSED2(went_ok); return std::forward_as_tuple(rq); } else return std::tuple_cat(std::forward_as_tuple(rq), DoConvertFromJS<0, Types...>(rq, args, went_ok)); } // Overloads for ScriptInterface& first argument. template static std::tuple ConvertFromJS(ScriptInterface::CmptPrivate* cmptPrivate, const ScriptRequest& rq, JS::CallArgs& args, bool& went_ok, std::tuple*) { if constexpr (sizeof...(Types) == 0) { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(rq); UNUSED2(args); UNUSED2(went_ok); return std::forward_as_tuple(*cmptPrivate->pScriptInterface); } else return std::tuple_cat(std::forward_as_tuple(*cmptPrivate->pScriptInterface), DoConvertFromJS<0, Types...>(rq, args, went_ok)); } /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// /** * Wrap std::apply for the case where we have an object method or a regular function. */ template static typename args_info::return_type call(T* object, tuple& args) { if constexpr(std::is_same_v) { // GCC (at least < 9) & VS17 prints warnings if arguments are not used in some constexpr branch. UNUSED2(object); return std::apply(callable, args); } else return std::apply(callable, std::tuple_cat(std::forward_as_tuple(*object), args)); } /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// + + struct IgnoreResult_t {}; + static inline IgnoreResult_t IgnoreResult; + + /** + * Recursive helper to call AssignOrToJSVal + */ + template + static void AssignOrToJSValHelper(const ScriptRequest& rq, JS::MutableHandleValueVector argv, const T& a, const Ts&... params) + { + ScriptInterface::AssignOrToJSVal(rq, argv[i], a); + AssignOrToJSValHelper(rq, argv, params...); + } + + template + static void AssignOrToJSValHelper(const ScriptRequest& UNUSED(rq), JS::MutableHandleValueVector UNUSED(argv)) + { + static_assert(sizeof...(Ts) == 0); + // Nop, for terminating the template recursion. + } + + /** + * Wrapper around calling a JS function from C++. + * Arguments are const& to avoid lvalue/rvalue issues, and so can't be used as out-parameters. + * In particular, the problem is that Rooted are deduced as Rooted, not Handle, and so can't be copied. + * This could be worked around with more templates, but it doesn't seem particularly worth doing. + */ + template + static bool Call_(const ScriptRequest& rq, JS::HandleValue val, const char* name, R& ret, const Args&... args) + { + JS::RootedObject obj(rq.cx); + if (!JS_ValueToObject(rq.cx, val, &obj) || !obj) + return false; + + // Check that the named function actually exists, to avoid ugly JS error reports + // when calling an undefined value + bool found; + if (!JS_HasProperty(rq.cx, obj, name, &found) || !found) + return false; + + JS::RootedValueVector argv(rq.cx); + ignore_result(argv.resize(sizeof...(Args))); + AssignOrToJSValHelper<0>(rq, &argv, args...); + + bool success; + if constexpr (std::is_same_v) + success = JS_CallFunctionName(rq.cx, obj, name, argv, ret); + else + { + JS::RootedValue jsRet(rq.cx); + success = JS_CallFunctionName(rq.cx, obj, name, argv, &jsRet); + if constexpr (!std::is_same_v) + { + if (success) + ScriptInterface::FromJSVal(rq, jsRet, ret); + } + else + UNUSED2(ret); // VS2017 complains. + } + // Even if everything succeeded, there could be pending exceptions + return !ScriptException::CatchPending(rq) && success; + } + + /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// public: template using ObjectGetter = T*(*)(const ScriptRequest&, JS::CallArgs&); // TODO: the fact that this takes class and not auto is to work around an odd VS17 bug. // It can be removed with VS19. template using GetterFor = ObjectGetter::object_type>; /** * The meat of this file. This wraps a C++ function into a JSNative, * so that it can be called from JS and manipulated in Spidermonkey. * Most C++ functions can be directly wrapped, so long as their arguments are * convertible from JS::Value and their return value is convertible to JS::Value (or void) * The C++ function may optionally take const ScriptRequest& or CmptPrivate* as its first argument. * The function may be an object method, in which case you need to pass an appropriate getter * * Optimisation note: the ScriptRequest object is created even without arguments, * as it's necessary for IsExceptionPending. * * @param thisGetter to get the object, if necessary. */ template thisGetter = nullptr> static bool ToJSNative(JSContext* cx, unsigned argc, JS::Value* vp) { using ObjType = typename args_info::object_type; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptInterface* scriptInterface = ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface; ScriptRequest rq(*scriptInterface); // If the callable is an object method, we must specify how to fetch the object. static_assert(std::is_same_v::object_type, void> || thisGetter != nullptr, "ScriptFunction::Register - No getter specified for object method"); // GCC 7 triggers spurious warnings #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Waddress" #endif ObjType* obj = nullptr; if constexpr (thisGetter != nullptr) { obj = thisGetter(rq, args); if (!obj) return false; } #ifdef __GNUC__ #pragma GCC diagnostic pop #endif bool went_ok = true; typename args_info::arg_types outs = ConvertFromJS(ScriptInterface::GetScriptInterfaceAndCBData(cx), rq, args, went_ok, static_cast::arg_types*>(nullptr)); if (!went_ok) return false; /** * TODO: error handling isn't standard, and since this can call any C++ function, * there's no simple obvious way to deal with it. * For now we check for pending JS exceptions, but it would probably be nicer * to standardise on something, or perhaps provide an "errorHandler" here. */ if constexpr (std::is_same_v::return_type>) call(obj, outs); else if constexpr (std::is_same_v::return_type>) args.rval().set(call(obj, outs)); else ScriptInterface::ToJSVal(rq, args.rval(), call(obj, outs)); return !ScriptException::IsPending(rq); } /** + * Call a JS function @a name, property of object @a val, with the arguments @a args. + * @a ret will be updated with the return value, if any. + * @return the success (or failure) thereof. + */ + template + static bool Call(const ScriptRequest& rq, JS::HandleValue val, const char* name, R& ret, const Args&... args) + { + return Call_(rq, val, name, ret, std::forward(args)...); + } + + // Specialisation for MutableHandleValue return. + template + static bool Call(const ScriptRequest& rq, JS::HandleValue val, const char* name, JS::MutableHandleValue ret, const Args&... args) + { + return Call_(rq, val, name, ret, std::forward(args)...); + } + + /** + * Call a JS function @a name, property of object @a val, with the arguments @a args. + * @return the success (or failure) thereof. + */ + template + static bool CallVoid(const ScriptRequest& rq, JS::HandleValue val, const char* name, const Args&... args) + { + return Call(rq, val, name, IgnoreResult, std::forward(args)...); + } + + /** * Return a function spec from a C++ function. */ template thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT> static JSFunctionSpec Wrap(const char* name) { return JS_FN(name, (&ToJSNative), args_info::nb_args, flags); } /** * Return a JSFunction from a C++ function. */ template thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT> static JSFunction* Create(const ScriptRequest& rq, const char* name) { return JS_NewFunction(rq.cx, &ToJSNative, args_info::nb_args, flags, name); } /** * Register a function on the native scope (usually 'Engine'). */ template thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT> static void Register(const ScriptRequest& rq, const char* name) { JS_DefineFunction(rq.cx, rq.nativeScope, name, &ToJSNative, args_info::nb_args, flags); } /** * Register a function on @param scope. * Prefer the version taking ScriptRequest unless you have a good reason not to. * @see Register */ template thisGetter = nullptr, u16 flags = JSPROP_ENUMERATE|JSPROP_READONLY|JSPROP_PERMANENT> static void Register(JSContext* cx, JS::HandleObject scope, const char* name) { JS_DefineFunction(cx, scope, name, &ToJSNative, args_info::nb_args, flags); } /** * Convert the CmptPrivate callback data to T* */ template static T* ObjectFromCBData(const ScriptRequest& rq, JS::CallArgs&) { return static_cast(ScriptInterface::GetScriptInterfaceAndCBData(rq.cx)->pCBData); } }; #endif // INCLUDED_FUNCTIONWRAPPER Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 25353) +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 25354) @@ -1,1015 +1,995 @@ /* Copyright (C) 2021 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 "FunctionWrapper.h" #include "ScriptContext.h" #include "ScriptExtraHeaders.h" #include "ScriptInterface.h" #include "ScriptStats.h" #include "lib/debug.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/utf16string.h" #include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION #include #include #include #include #include #include #include #include "valgrind.h" /** * @file * Abstractions of various SpiderMonkey features. * Engine code should be using functions of these interfaces rather than * directly accessing the underlying JS api. */ struct ScriptInterface_impl { ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context); ~ScriptInterface_impl(); // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. shared_ptr m_context; friend ScriptRequest; private: JSContext* m_cx; JS::PersistentRootedObject m_glob; // global scope object public: boost::rand48* m_rng; JS::PersistentRootedObject m_nativeScope; // native function scope object }; ScriptRequest::ScriptRequest(const ScriptInterface& scriptInterface) : cx(scriptInterface.m->m_cx), nativeScope(scriptInterface.m->m_nativeScope) { m_formerRealm = JS::EnterRealm(cx, scriptInterface.m->m_glob); glob = JS::CurrentGlobalOrNull(cx); } JS::Value ScriptRequest::globalValue() const { return JS::ObjectValue(*glob); } ScriptRequest::~ScriptRequest() { JS::LeaveRealm(cx, m_formerRealm); } namespace { JSClassOps global_classops = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook }; JSClass global_class = { "global", JSCLASS_GLOBAL_FLAGS, &global_classops }; // Functions in the global namespace: bool print(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ for (uint i = 0; i < args.length(); ++i) { std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[i], str)) return false; debug_printf("%s", utf8_from_wstring(str).c_str()); } fflush(stdout); args.rval().setUndefined(); return true; } bool logmsg(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGMESSAGE("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool warn(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGWARNING("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool error(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGERROR("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } JS::Value deepcopy(const ScriptRequest& rq, JS::HandleValue val) { if (val.isNullOrUndefined()) { ScriptException::Raise(rq, "deepcopy requires one argument."); return JS::UndefinedValue(); } JS::RootedValue ret(rq.cx); if (!JS_StructuredClone(rq.cx, val, &ret, NULL, NULL)) { ScriptException::Raise(rq, "deepcopy StructureClone copy failed."); return JS::UndefinedValue(); } return ret; } JS::Value deepfreeze(const ScriptInterface& scriptInterface, JS::HandleValue val) { ScriptRequest rq(scriptInterface); if (!val.isObject()) { ScriptException::Raise(rq, "deepfreeze requires exactly one object as an argument."); return JS::UndefinedValue(); } scriptInterface.FreezeObject(val, true); return val; } void ProfileStart(const std::string& regionName) { const char* name = "(ProfileStart)"; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; if (!regionName.empty()) name = StringFlyweight(regionName).get().c_str(); if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.StartScript(name); g_Profiler2.RecordRegionEnter(name); } void ProfileStop() { if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave(); } void ProfileAttribute(const std::string& attr) { const char* name = "(ProfileAttribute)"; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; if (!attr.empty()) name = StringFlyweight(attr).get().c_str(); g_Profiler2.RecordAttribute("%s", name); } // Math override functions: // boost::uniform_real is apparently buggy in Boost pre-1.47 - for integer generators // it returns [min,max], not [min,max). The bug was fixed in 1.47. // We need consistent behaviour, so manually implement the correct version: static double generate_uniform_real(boost::rand48& rng, double min, double max) { while (true) { double n = (double)(rng() - rng.min()); double d = (double)(rng.max() - rng.min()) + 1.0; ENSURE(d > 0 && n >= 0 && n <= d); double r = n / d * (max - min) + min; if (r < max) return r; } } bool Math_random(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); double r; if (!ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->MathRandom(r)) return false; args.rval().setNumber(r); return true; } } // anonymous namespace bool ScriptInterface::MathRandom(double& nbr) { if (m->m_rng == NULL) return false; nbr = generate_uniform_real(*(m->m_rng), 0.0, 1.0); return true; } ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context) : m_context(context), m_cx(context->GetGeneralJSContext()), m_glob(context->GetGeneralJSContext()), m_nativeScope(context->GetGeneralJSContext()) { JS::RealmCreationOptions creationOpt; // Keep JIT code during non-shrinking GCs. This brings a quite big performance improvement. creationOpt.setPreserveJitCode(true); // Enable uneval creationOpt.setToSourceEnabled(true); JS::RealmOptions opt(creationOpt, JS::RealmBehaviors{}); m_glob = JS_NewGlobalObject(m_cx, &global_class, nullptr, JS::OnNewGlobalHookOption::FireOnNewGlobalHook, opt); JSAutoRealm autoRealm(m_cx, m_glob); ENSURE(JS::InitRealmStandardClasses(m_cx)); JS_DefineProperty(m_cx, m_glob, "global", m_glob, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); // These first 4 actually use CallArgs & thus don't use ScriptFunction JS_DefineFunction(m_cx, m_glob, "print", ::print, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "log", ::logmsg, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "warn", ::warn, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "error", ::error, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); ScriptFunction::Register(m_cx, m_glob, "clone"); ScriptFunction::Register(m_cx, m_glob, "deepfreeze"); m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, nullptr, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); ScriptFunction::Register<&ProfileStart>(m_cx, m_nativeScope, "ProfileStart"); ScriptFunction::Register<&ProfileStop>(m_cx, m_nativeScope, "ProfileStop"); ScriptFunction::Register<&ProfileAttribute>(m_cx, m_nativeScope, "ProfileAttribute"); m_context->RegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } ScriptInterface_impl::~ScriptInterface_impl() { m_context->UnRegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& context) : m(std::make_unique(nativeScopeName, context)) { // Profiler stats table isn't thread-safe, so only enable this on the main thread if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Add(this, debugName); } ScriptRequest rq(this); m_CmptPrivate.pScriptInterface = this; JS_SetCompartmentPrivate(js::GetObjectCompartment(rq.glob), (void*)&m_CmptPrivate); } ScriptInterface::~ScriptInterface() { if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Remove(this); } } void ScriptInterface::SetCallbackData(void* pCBData) { m_CmptPrivate.pCBData = pCBData; } ScriptInterface::CmptPrivate* ScriptInterface::GetScriptInterfaceAndCBData(JSContext* cx) { CmptPrivate* pCmptPrivate = (CmptPrivate*)JS_GetCompartmentPrivate(js::GetContextCompartment(cx)); return pCmptPrivate; } bool ScriptInterface::LoadGlobalScripts() { // Ignore this failure in tests if (!g_VFS) return false; // Load and execute *.js in the global scripts directory VfsPaths pathnames; vfs::GetPathnames(g_VFS, L"globalscripts/", L"*.js", pathnames); for (const VfsPath& path : pathnames) if (!LoadGlobalScriptFile(path)) { LOGERROR("LoadGlobalScripts: Failed to load script %s", path.string8()); return false; } return true; } bool ScriptInterface::ReplaceNondeterministicRNG(boost::rand48& rng) { ScriptRequest rq(this); JS::RootedValue math(rq.cx); JS::RootedObject global(rq.cx, rq.glob); if (JS_GetProperty(rq.cx, global, "Math", &math) && math.isObject()) { JS::RootedObject mathObj(rq.cx, &math.toObject()); JS::RootedFunction random(rq.cx, JS_DefineFunction(rq.cx, mathObj, "random", Math_random, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); if (random) { m->m_rng = &rng; return true; } } ScriptException::CatchPending(rq); LOGERROR("ReplaceNondeterministicRNG: failed to replace Math.random"); return false; } JSContext* ScriptInterface::GetGeneralJSContext() const { return m->m_context->GetGeneralJSContext(); } shared_ptr ScriptInterface::GetContext() const { return m->m_context; } void ScriptInterface::CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const { ScriptRequest rq(this); if (!ctor.isObject()) { LOGERROR("CallConstructor: ctor is not an object"); out.setNull(); return; } JS::RootedObject ctorObj(rq.cx, &ctor.toObject()); out.setObjectOrNull(JS_New(rq.cx, ctorObj, argv)); } void ScriptInterface::DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs) { ScriptRequest rq(this); std::string typeName = clasp->name; if (m_CustomObjectTypes.find(typeName) != m_CustomObjectTypes.end()) { // This type already exists throw PSERROR_Scripting_DefineType_AlreadyExists(); } JS::RootedObject global(rq.cx, rq.glob); JS::RootedObject obj(rq.cx, JS_InitClass(rq.cx, global, nullptr, clasp, constructor, minArgs, // Constructor, min args ps, fs, // Properties, methods static_ps, static_fs)); // Constructor properties, methods if (obj == nullptr) { ScriptException::CatchPending(rq); throw PSERROR_Scripting_DefineType_CreationFailed(); } CustomType& type = m_CustomObjectTypes[typeName]; type.m_Prototype.init(rq.cx, obj); type.m_Class = clasp; type.m_Constructor = constructor; } JSObject* ScriptInterface::CreateCustomObject(const std::string& typeName) const { std::map::const_iterator it = m_CustomObjectTypes.find(typeName); if (it == m_CustomObjectTypes.end()) throw PSERROR_Scripting_TypeDoesNotExist(); ScriptRequest rq(this); JS::RootedObject prototype(rq.cx, it->second.m_Prototype.get()); return JS_NewObjectWithGivenProto(rq.cx, it->second.m_Class, prototype); } -bool ScriptInterface::CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) const -{ - ScriptRequest rq(this); - JS::RootedObject obj(rq.cx); - if (!JS_ValueToObject(rq.cx, val, &obj) || !obj) - return false; - - // Check that the named function actually exists, to avoid ugly JS error reports - // when calling an undefined value - bool found; - if (!JS_HasProperty(rq.cx, obj, name, &found) || !found) - return false; - - if (JS_CallFunctionName(rq.cx, obj, name, argv, ret)) - return true; - - ScriptException::CatchPending(rq); - return false; -} - bool ScriptInterface::CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject object) { object.set(JS_NewPlainObject(rq.cx)); if (!object) throw PSERROR_Scripting_CreateObjectFailed(); return true; } void ScriptInterface::CreateArray(const ScriptRequest& rq, JS::MutableHandleValue objectValue, size_t length) { objectValue.setObjectOrNull(JS::NewArrayObject(rq.cx, length)); if (!objectValue.isObject()) throw PSERROR_Scripting_CreateObjectFailed(); } bool ScriptInterface::SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); bool found; if (!JS_HasProperty(rq.cx, global, name, &found)) return false; if (found) { JS::Rooted desc(rq.cx); if (!JS_GetOwnPropertyDescriptor(rq.cx, global, name, &desc)) return false; if (!desc.writable()) { if (!replace) { ScriptException::Raise(rq, "SetGlobal \"%s\" called multiple times", name); return false; } // This is not supposed to happen, unless the user has called SetProperty with constant = true on the global object // instead of using SetGlobal. if (!desc.configurable()) { ScriptException::Raise(rq, "The global \"%s\" is permanent and cannot be hotloaded", name); return false; } LOGMESSAGE("Hotloading new value for global \"%s\".", name); ENSURE(JS_DeleteProperty(rq.cx, global, name)); } } uint attrs = 0; if (constant) attrs |= JSPROP_READONLY; if (enumerate) attrs |= JSPROP_ENUMERATE; return JS_DefineProperty(rq.cx, global, name, value, attrs); } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_DefineProperty(rq.cx, object, name, value, attrs); } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); utf16string name16(name, name + wcslen(name)); return JS_DefineUCProperty(rq.cx, object, reinterpret_cast(name16.c_str()), name16.length(), value, attrs); } bool ScriptInterface::SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); JS::RootedId id(rq.cx, INT_TO_JSID(name)); return JS_DefinePropertyById(rq.cx, object, id, value, attrs); } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const { ScriptRequest rq(this); return GetProperty(rq, obj, name, out); } bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleObject out) { JS::RootedValue val(rq.cx); if (!GetProperty(rq, obj, name, &val)) return false; if (!val.isObject()) { LOGERROR("GetProperty failed: trying to get an object, but the property is not an object!"); return false; } out.set(&val.toObject()); return true; } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const { ScriptRequest rq(this); return GetProperty(rq, obj, name, out); } bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleValue out) { if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_GetProperty(rq.cx, object, name, out); } bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const { ScriptRequest rq(this); return GetPropertyInt(rq,obj, name, out); } bool ScriptInterface::GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleValue out) { JS::RootedId nameId(rq.cx, INT_TO_JSID(name)); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_GetPropertyById(rq.cx, object, nameId, out); } bool ScriptInterface::HasProperty(JS::HandleValue obj, const char* name) const { ScriptRequest rq(this); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); bool found; if (!JS_HasProperty(rq.cx, object, name, &found)) return false; return found; } bool ScriptInterface::GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out) { // Try to get the object as a property of the global object. JS::RootedObject global(rq.cx, rq.glob); if (!JS_GetProperty(rq.cx, global, name.c_str(), out)) { out.set(JS::NullHandleValue); return false; } if (!out.isNullOrUndefined()) return true; // Some objects, such as const definitions, or Class definitions, are hidden inside closures. // We must fetch those from the correct lexical scope. //JS::RootedValue glob(cx); JS::RootedObject lexical_environment(rq.cx, JS_GlobalLexicalEnvironment(rq.glob)); if (!JS_GetProperty(rq.cx, lexical_environment, name.c_str(), out)) { out.set(JS::NullHandleValue); return false; } if (!out.isNullOrUndefined()) return true; out.set(JS::NullHandleValue); return false; } bool ScriptInterface::EnumeratePropertyNames(JS::HandleValue objVal, bool enumerableOnly, std::vector& out) const { ScriptRequest rq(this); if (!objVal.isObjectOrNull()) { LOGERROR("EnumeratePropertyNames expected object type!"); return false; } JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::RootedIdVector props(rq.cx); // This recurses up the prototype chain on its own. if (!js::GetPropertyKeys(rq.cx, obj, enumerableOnly? 0 : JSITER_HIDDEN, &props)) return false; out.reserve(out.size() + props.length()); for (size_t i = 0; i < props.length(); ++i) { JS::RootedId id(rq.cx, props[i]); JS::RootedValue val(rq.cx); if (!JS_IdToValue(rq.cx, id, &val)) return false; // Ignore integer properties for now. // TODO: is this actually a thing in ECMAScript 6? if (!val.isString()) continue; std::string propName; if (!FromJSVal(rq, val, propName)) return false; out.emplace_back(std::move(propName)); } return true; } bool ScriptInterface::SetPrototype(JS::HandleValue objVal, JS::HandleValue protoVal) { ScriptRequest rq(this); if (!objVal.isObject() || !protoVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::RootedObject proto(rq.cx, &protoVal.toObject()); return JS_SetPrototype(rq.cx, obj, proto); } bool ScriptInterface::FreezeObject(JS::HandleValue objVal, bool deep) const { ScriptRequest rq(this); if (!objVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); if (deep) return JS_DeepFreezeObject(rq.cx, obj); else return JS_FreezeObject(rq.cx, obj); } bool ScriptInterface::LoadScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::CompileOptions options(rq.cx); // Set the line to 0 because CompileFunction silently adds a `(function() {` as the first line, // and errors get misreported. // TODO: it would probably be better to not implicitly introduce JS scopes. options.setFileAndLine(filenameStr.c_str(), 0); options.setIsRunOnce(false); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); JS::RootedObjectVector emptyScopeChain(rq.cx); JS::RootedFunction func(rq.cx, JS::CompileFunction(rq.cx, emptyScopeChain, options, NULL, 0, NULL, src)); if (func == nullptr) { ScriptException::CatchPending(rq); return false; } JS::RootedValue rval(rq.cx); if (JS_CallFunction(rq.cx, nullptr, func, JS::HandleValueArray::empty(), &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), 1); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScriptFile(const VfsPath& path) const { ScriptRequest rq(this); if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return false; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return false; } CStr code = file.DecodeUTF8(); // assume it's UTF-8 uint lineNo = 1; // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = path.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), lineNo); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code, JS::MutableHandleValue rval) const { ScriptRequest rq(this); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::ParseJSON(const std::string& string_utf8, JS::MutableHandleValue out) const { ScriptRequest rq(this); std::wstring attrsW = wstring_from_utf8(string_utf8); utf16string string(attrsW.begin(), attrsW.end()); if (JS_ParseJSON(rq.cx, reinterpret_cast(string.c_str()), (u32)string.size(), out)) return true; ScriptException::CatchPending(rq); return false; } void ScriptInterface::ReadJSONFile(const VfsPath& path, JS::MutableHandleValue out) const { if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return; } std::string content(file.DecodeUTF8()); // assume it's UTF-8 if (!ParseJSON(content, out)) LOGERROR("Failed to parse '%s'", path.string8()); } struct Stringifier { static bool callback(const char16_t* buf, u32 len, void* data) { utf16string str(buf, buf+len); std::wstring strw(str.begin(), str.end()); Status err; // ignore Unicode errors static_cast(data)->stream << utf8_from_wstring(strw, &err); return true; } std::stringstream stream; }; // TODO: It's not quite clear why JS_Stringify needs JS::MutableHandleValue. |obj| should not get modified. // It probably has historical reasons and could be changed by SpiderMonkey in the future. std::string ScriptInterface::StringifyJSON(JS::MutableHandleValue obj, bool indent) const { ScriptRequest rq(this); Stringifier str; JS::RootedValue indentVal(rq.cx, indent ? JS::Int32Value(2) : JS::UndefinedValue()); if (!JS_Stringify(rq.cx, obj, nullptr, indentVal, &Stringifier::callback, &str)) { ScriptException::CatchPending(rq); return std::string(); } return str.stream.str(); } std::string ScriptInterface::ToString(JS::MutableHandleValue obj, bool pretty) const { ScriptRequest rq(this); if (obj.isUndefined()) return "(void 0)"; // Try to stringify as JSON if possible // (TODO: this is maybe a bad idea since it'll drop 'undefined' values silently) if (pretty) { Stringifier str; JS::RootedValue indentVal(rq.cx, JS::Int32Value(2)); if (JS_Stringify(rq.cx, obj, nullptr, indentVal, &Stringifier::callback, &str)) return str.stream.str(); // Drop exceptions raised by cyclic values before trying something else JS_ClearPendingException(rq.cx); } // Caller didn't want pretty output, or JSON conversion failed (e.g. due to cycles), // so fall back to obj.toSource() std::wstring source = L"(error)"; - CallFunction(obj, "toSource", source); + ScriptFunction::Call(rq, obj, "toSource", source); return utf8_from_wstring(source); } JS::Value ScriptInterface::CloneValueFromOtherCompartment(const ScriptInterface& otherCompartment, JS::HandleValue val) const { PROFILE("CloneValueFromOtherCompartment"); ScriptRequest rq(this); JS::RootedValue out(rq.cx); ScriptInterface::StructuredClone structuredClone = otherCompartment.WriteStructuredClone(val); ReadStructuredClone(structuredClone, &out); return out.get(); } ScriptInterface::StructuredClone ScriptInterface::WriteStructuredClone(JS::HandleValue v) const { ScriptRequest rq(this); ScriptInterface::StructuredClone ret(new JSStructuredCloneData(JS::StructuredCloneScope::SameProcess)); JS::CloneDataPolicy policy; if (!JS_WriteStructuredClone(rq.cx, v, ret.get(), JS::StructuredCloneScope::SameProcess, policy, nullptr, nullptr, JS::UndefinedHandleValue)) { debug_warn(L"Writing a structured clone with JS_WriteStructuredClone failed!"); ScriptException::CatchPending(rq); return ScriptInterface::StructuredClone(); } return ret; } void ScriptInterface::ReadStructuredClone(const ScriptInterface::StructuredClone& ptr, JS::MutableHandleValue ret) const { ScriptRequest rq(this); JS::CloneDataPolicy policy; if (!JS_ReadStructuredClone(rq.cx, *ptr, JS_STRUCTURED_CLONE_VERSION, ptr->scope(), ret, policy, nullptr, nullptr)) ScriptException::CatchPending(rq); } Index: ps/trunk/source/scriptinterface/ScriptInterface.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.h (revision 25353) +++ ps/trunk/source/scriptinterface/ScriptInterface.h (revision 25354) @@ -1,628 +1,595 @@ /* Copyright (C) 2021 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_SCRIPTINTERFACE #define INCLUDED_SCRIPTINTERFACE #include "lib/file/vfs/vfs_path.h" #include "maths/Fixed.h" #include "ps/Errors.h" #include "scriptinterface/ScriptExceptions.h" #include "scriptinterface/ScriptTypes.h" #include ERROR_GROUP(Scripting); ERROR_TYPE(Scripting, SetupFailed); ERROR_SUBGROUP(Scripting, LoadFile); ERROR_TYPE(Scripting_LoadFile, OpenFailed); ERROR_TYPE(Scripting_LoadFile, EvalErrors); ERROR_TYPE(Scripting, CallFunctionFailed); ERROR_TYPE(Scripting, DefineConstantFailed); ERROR_TYPE(Scripting, CreateObjectFailed); ERROR_TYPE(Scripting, TypeDoesNotExist); ERROR_SUBGROUP(Scripting, DefineType); ERROR_TYPE(Scripting_DefineType, AlreadyExists); ERROR_TYPE(Scripting_DefineType, CreationFailed); // Set the maximum number of function arguments that can be handled // (This should be as small as possible (for compiler efficiency), // but as large as necessary for all wrapped functions) #define SCRIPT_INTERFACE_MAX_ARGS 8 class JSStructuredCloneData; class ScriptInterface; struct ScriptInterface_impl; class ScriptContext; // Using a global object for the context is a workaround until Simulation, AI, etc, // use their own threads and also their own contexts. extern thread_local shared_ptr g_ScriptContext; namespace boost { namespace random { class rand48; } } /** * RAII structure which encapsulates an access to the context and compartment of a ScriptInterface. * This struct provides: * - a pointer to the context, while acting like JSAutoRequest * - a pointer to the global object of the compartment, while acting like JSAutoRealm * * This way, getting and using those pointers is safe with respect to the GC * and to the separation of compartments. */ class ScriptRequest { public: ScriptRequest() = delete; ScriptRequest(const ScriptRequest& rq) = delete; ScriptRequest& operator=(const ScriptRequest& rq) = delete; ScriptRequest(const ScriptInterface& scriptInterface); ScriptRequest(const ScriptInterface* scriptInterface) : ScriptRequest(*scriptInterface) {} ScriptRequest(shared_ptr scriptInterface) : ScriptRequest(*scriptInterface) {} ~ScriptRequest(); JS::Value globalValue() const; JSContext* cx; JSObject* glob; JS::HandleObject nativeScope; private: JS::Realm* m_formerRealm; }; /** * Abstraction around a SpiderMonkey JS::Realm. * * Thread-safety: * - May be used in non-main threads. * - Each ScriptInterface must be created, used, and destroyed, all in a single thread * (it must never be shared between threads). */ class ScriptInterface { NONCOPYABLE(ScriptInterface); friend class ScriptRequest; public: /** * Constructor. * @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will * be placed into, as a scoping mechanism; typically "Engine" * @param debugName Name of this interface for CScriptStats purposes. * @param context ScriptContext to use when initializing this interface. */ ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& context); ~ScriptInterface(); struct CmptPrivate { ScriptInterface* pScriptInterface; // the ScriptInterface object the compartment belongs to void* pCBData; // meant to be used as the "this" object for callback functions } m_CmptPrivate; void SetCallbackData(void* pCBData); static CmptPrivate* GetScriptInterfaceAndCBData(JSContext* cx); /** * GetGeneralJSContext returns the context without starting a GC request and without * entering the ScriptInterface compartment. It should only be used in specific situations, * for instance when initializing a persistent rooted. * If you need the compartmented context of the ScriptInterface, you should create a * ScriptInterface::Request and use the context from that. */ JSContext* GetGeneralJSContext() const; shared_ptr GetContext() const; /** * Load global scripts that most script interfaces need, * located in the /globalscripts directory. VFS must be initialized. */ bool LoadGlobalScripts(); /** * Replace the default JS random number geenrator with a seeded, network-sync'd one. */ bool ReplaceNondeterministicRNG(boost::random::rand48& rng); /** * Call a constructor function, equivalent to JS "new ctor(arg)". * @param ctor An object that can be used as constructor * @param argv Constructor arguments * @param out The new object; On error an error message gets logged and out is Null (out.isNull() == true). */ void CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const; JSObject* CreateCustomObject(const std::string & typeName) const; void DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs); /** * Sets the given value to a new plain JS::Object, converts the arguments to JS::Values and sets them as properties. * This is static so that callers like ToJSVal can use it with the JSContext directly instead of having to obtain the instance using GetScriptInterfaceAndCBData. * Can throw an exception. */ template static bool CreateObject(const ScriptRequest& rq, JS::MutableHandleValue objectValue, Args const&... args) { JS::RootedObject obj(rq.cx); if (!CreateObject_(rq, &obj, args...)) return false; objectValue.setObject(*obj); return true; } /** * Sets the given value to a new JS object or Null Value in case of out-of-memory. */ static void CreateArray(const ScriptRequest& rq, JS::MutableHandleValue objectValue, size_t length = 0); /** * Set the named property on the global object. * Optionally makes it {ReadOnly, DontEnum}. We do not allow to make it DontDelete, so that it can be hotloaded * by deleting it and re-creating it, which is done by setting @p replace to true. */ template bool SetGlobal(const char* name, const T& value, bool replace = false, bool constant = true, bool enumerate = true); /** * Set the named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetProperty(JS::HandleValue obj, const char* name, const T& value, bool constant = false, bool enumerate = true) const; /** * Set the named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetProperty(JS::HandleValue obj, const wchar_t* name, const T& value, bool constant = false, bool enumerate = true) const; /** * Set the integer-named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetPropertyInt(JS::HandleValue obj, int name, const T& value, bool constant = false, bool enumerate = true) const; /** * Get the named property on the given object. */ template bool GetProperty(JS::HandleValue obj, const char* name, T& out) const; bool GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const; bool GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const; template static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, T& out); static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleValue out); static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleObject out); /** * Get the integer-named property on the given object. */ template bool GetPropertyInt(JS::HandleValue obj, int name, T& out) const; bool GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const; bool GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleObject out) const; template static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, T& out); static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleValue out); static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleObject out); /** * Check the named property has been defined on the given object. */ bool HasProperty(JS::HandleValue obj, const char* name) const; /** * Get an object from the global scope or any lexical scope. * This can return globally accessible objects even if they are not properties * of the global object (e.g. ES6 class definitions). * @param name - Name of the property. * @param out The object or null. */ static bool GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out); /** * Returns all properties of the object, both own properties and inherited. * This is essentially equivalent to calling Object.getOwnPropertyNames() * and recursing up the prototype chain. * NB: this does not return properties with symbol or numeric keys, as that would * require a variant in the vector, and it's not useful for now. * @param enumerableOnly - only return enumerable properties. */ bool EnumeratePropertyNames(JS::HandleValue objVal, bool enumerableOnly, std::vector& out) const; bool SetPrototype(JS::HandleValue obj, JS::HandleValue proto); bool FreezeObject(JS::HandleValue objVal, bool deep) const; /** * Convert an object to a UTF-8 encoded string, either with JSON * (if pretty == true and there is no JSON error) or with toSource(). * * We have to use a mutable handle because JS_Stringify requires that for unknown reasons. */ std::string ToString(JS::MutableHandleValue obj, bool pretty = false) const; /** * Parse a UTF-8-encoded JSON string. Returns the unmodified value on error * and prints an error message. * @return true on success; false otherwise */ bool ParseJSON(const std::string& string_utf8, JS::MutableHandleValue out) const; /** * Read a JSON file. Returns the unmodified value on error and prints an error message. */ void ReadJSONFile(const VfsPath& path, JS::MutableHandleValue out) const; /** * Stringify to a JSON string, UTF-8 encoded. Returns an empty string on error. */ std::string StringifyJSON(JS::MutableHandleValue obj, bool indent = true) const; /** * Load and execute the given script in a new function scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadScript(const VfsPath& filename, const std::string& code) const; /** * Load and execute the given script in the global scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScript(const VfsPath& filename, const std::string& code) const; /** * Load and execute the given script in the global scope. * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScriptFile(const VfsPath& path) const; /** * Evaluate some JS code in the global scope. * @return true on successful compilation and execution; false otherwise */ bool Eval(const char* code) const; bool Eval(const char* code, JS::MutableHandleValue out) const; template bool Eval(const char* code, T& out) const; /** * Convert a JS::Value to a C++ type. (This might trigger GC.) */ template static bool FromJSVal(const ScriptRequest& rq, const JS::HandleValue val, T& ret); /** * Convert a C++ type to a JS::Value. (This might trigger GC. The return * value must be rooted if you don't want it to be collected.) * NOTE: We are passing the JS::Value by reference instead of returning it by value. * The reason is a memory corruption problem that appears to be caused by a bug in Visual Studio. * Details here: http://www.wildfiregames.com/forum/index.php?showtopic=17289&p=285921 */ template static void ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, T const& val); /** * Convert a named property of an object to a C++ type. */ template static bool FromJSProperty(const ScriptRequest& rq, const JS::HandleValue val, const char* name, T& ret, bool strict = false); /** * MathRandom (this function) calls the random number generator assigned to this ScriptInterface instance and * returns the generated number. * Math_random (with underscore, not this function) is a global function, but different random number generators can be * stored per ScriptInterface. It calls MathRandom of the current ScriptInterface instance. */ bool MathRandom(double& nbr); /** * Structured clones are a way to serialize 'simple' JS::Values into a buffer * that can safely be passed between compartments and between threads. * A StructuredClone can be stored and read multiple times if desired. * We wrap them in shared_ptr so memory management is automatic and * thread-safe. */ using StructuredClone = shared_ptr; StructuredClone WriteStructuredClone(JS::HandleValue v) const; void ReadStructuredClone(const StructuredClone& ptr, JS::MutableHandleValue ret) const; /** * Construct a new value (usable in this ScriptInterface's compartment) by cloning * a value from a different compartment. * Complex values (functions, XML, etc) won't be cloned correctly, but basic * types and cyclic references should be fine. */ JS::Value CloneValueFromOtherCompartment(const ScriptInterface& otherCompartment, JS::HandleValue val) const; /** * Retrieve the private data field of a JSObject that is an instance of the given JSClass. */ template static T* GetPrivate(const ScriptRequest& rq, JS::HandleObject thisobj, JSClass* jsClass) { T* value = static_cast(JS_GetInstancePrivate(rq.cx, thisobj, jsClass, nullptr)); if (value == nullptr) ScriptException::Raise(rq, "Private data of the given object is null!"); return value; } /** * Retrieve the private data field of a JS Object that is an instance of the given JSClass. * If an error occurs, GetPrivate will report it with the according stack. */ template static T* GetPrivate(const ScriptRequest& rq, JS::CallArgs& callArgs, JSClass* jsClass) { if (!callArgs.thisv().isObject()) { ScriptException::Raise(rq, "Cannot retrieve private JS class data because from a non-object value!"); return nullptr; } JS::RootedObject thisObj(rq.cx, &callArgs.thisv().toObject()); T* value = static_cast(JS_GetInstancePrivate(rq.cx, thisObj, jsClass, &callArgs)); if (value == nullptr) ScriptException::Raise(rq, "Private data of the given object is null!"); return value; } /** * Converts |a| if needed and assigns it to |handle|. * This is meant for use in other templates where we want to use the same code for JS::RootedValue&/JS::HandleValue and * other types. Note that functions are meant to take JS::HandleValue instead of JS::RootedValue&, but this implicit * conversion does not work for templates (exact type matches required for type deduction). * A similar functionality could also be implemented as a ToJSVal specialization. The current approach was preferred * because "conversions" from JS::HandleValue to JS::MutableHandleValue are unusual and should not happen "by accident". */ template static void AssignOrToJSVal(const ScriptRequest& rq, JS::MutableHandleValue handle, const T& a); /** * The same as AssignOrToJSVal, but also allows JS::Value for T. * In most cases it's not safe to use the plain (unrooted) JS::Value type, but this can happen quite * easily with template functions. The idea is that the linker prints an error if AssignOrToJSVal is * used with JS::Value. If the specialization for JS::Value should be allowed, you can use this * "unrooted" version of AssignOrToJSVal. */ template static void AssignOrToJSValUnrooted(const ScriptRequest& rq, JS::MutableHandleValue handle, const T& a) { AssignOrToJSVal(rq, handle, a); } /** * Converts |val| to T if needed or just returns it if it's a handle. * This is meant for use in other templates where we want to use the same code for JS::HandleValue and * other types. */ template static T AssignOrFromJSVal(const ScriptRequest& rq, const JS::HandleValue& val, bool& ret); private: static bool CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject obj); template static bool CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject obj, const char* propertyName, const T& propertyValue, Args const&... args) { JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, propertyValue); return CreateObject_(rq, obj, args...) && JS_DefineProperty(rq.cx, obj, propertyName, val, JSPROP_ENUMERATE); } - bool CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) const; bool SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate); bool SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const; bool SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const; bool SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const; struct CustomType { JS::PersistentRootedObject m_Prototype; JSClass* m_Class; JSNative m_Constructor; }; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the custom destructor of ScriptInterface_impl. std::unique_ptr m; boost::random::rand48* m_rng; std::map m_CustomObjectTypes; - -// The nasty macro/template bits are split into a separate file so you don't have to look at them -public: - #include "NativeWrapperDecls.h" - // This declares: - // - // template - // static JSNative call; - // - // template - // static JSNative callMethod; - // - // template - // static JSNative callMethodConst; - // - // template - // static size_t nargs(); - // - // template - // bool CallFunction(JS::HandleValue val, const char* name, R& ret, const T0&...) const; - // - // template - // bool CallFunction(JS::HandleValue val, const char* name, JS::Rooted* ret, const T0&...) const; - // - // template - // bool CallFunction(JS::HandleValue val, const char* name, JS::MutableHandle ret, const T0&...) const; - // - // template - // bool CallFunctionVoid(JS::HandleValue val, const char* name, const T0&...) const; }; -// Implement those declared functions -#include "NativeWrapperDefns.h" - template inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& rq, JS::MutableHandleValue handle, const T& a) { ToJSVal(rq, handle, a); } template<> inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::PersistentRootedValue& a) { handle.set(a); } template<> inline void ScriptInterface::AssignOrToJSVal >(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::Heap& a) { handle.set(a); } template<> inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::RootedValue& a) { handle.set(a); } template <> inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::HandleValue& a) { handle.set(a); } template <> inline void ScriptInterface::AssignOrToJSValUnrooted(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::Value& a) { handle.set(a); } template inline T ScriptInterface::AssignOrFromJSVal(const ScriptRequest& rq, const JS::HandleValue& val, bool& ret) { T retVal; ret = FromJSVal(rq, val, retVal); return retVal; } template<> inline JS::HandleValue ScriptInterface::AssignOrFromJSVal(const ScriptRequest& UNUSED(rq), const JS::HandleValue& val, bool& ret) { ret = true; return val; } template bool ScriptInterface::SetGlobal(const char* name, const T& value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetGlobal_(name, val, replace, constant, enumerate); } template bool ScriptInterface::SetProperty(JS::HandleValue obj, const char* name, const T& value, bool constant, bool enumerate) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetProperty_(obj, name, val, constant, enumerate); } template bool ScriptInterface::SetProperty(JS::HandleValue obj, const wchar_t* name, const T& value, bool constant, bool enumerate) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetProperty_(obj, name, val, constant, enumerate); } template bool ScriptInterface::SetPropertyInt(JS::HandleValue obj, int name, const T& value, bool constant, bool enumerate) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetPropertyInt_(obj, name, val, constant, enumerate); } template bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, T& out) const { ScriptRequest rq(this); return GetProperty(rq, obj, name, out); } template bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, T& out) { JS::RootedValue val(rq.cx); if (!GetProperty(rq, obj, name, &val)) return false; return FromJSVal(rq, val, out); } template bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, T& out) const { ScriptRequest rq(this); return GetPropertyInt(rq, obj, name, out); } template bool ScriptInterface::GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, T& out) { JS::RootedValue val(rq.cx); if (!GetPropertyInt(rq, obj, name, &val)) return false; return FromJSVal(rq, val, out); } template bool ScriptInterface::Eval(const char* code, T& ret) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); if (!Eval(code, &rval)) return false; return FromJSVal(rq, rval, ret); } #endif // INCLUDED_SCRIPTINTERFACE Index: ps/trunk/source/scriptinterface/tests/test_ScriptConversions.h =================================================================== --- ps/trunk/source/scriptinterface/tests/test_ScriptConversions.h (revision 25353) +++ ps/trunk/source/scriptinterface/tests/test_ScriptConversions.h (revision 25354) @@ -1,272 +1,273 @@ /* Copyright (C) 2021 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 "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "maths/Fixed.h" #include "maths/FixedVector2D.h" #include "maths/FixedVector3D.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "jsapi.h" class TestScriptConversions : public CxxTest::TestSuite { template void convert_to(const T& value, const std::string& expected) { ScriptInterface script("Test", "Test", g_ScriptContext); TS_ASSERT(script.LoadGlobalScripts()); ScriptRequest rq(script); JS::RootedValue v1(rq.cx); ScriptInterface::ToJSVal(rq, &v1, value); // We want to convert values to strings, but can't just call toSource() on them // since they might not be objects. So just use uneval. std::string source; JS::RootedValue global(rq.cx, rq.globalValue()); - TS_ASSERT(script.CallFunction(global, "uneval", source, v1)); + TS_ASSERT(ScriptFunction::Call(rq, global, "uneval", source, v1)); TS_ASSERT_STR_EQUALS(source, expected); } template void roundtrip(const T& value, const char* expected) { ScriptInterface script("Test", "Test", g_ScriptContext); TS_ASSERT(script.LoadGlobalScripts()); ScriptRequest rq(script); JS::RootedValue v1(rq.cx); ScriptInterface::ToJSVal(rq, &v1, value); std::string source; JS::RootedValue global(rq.cx, rq.globalValue()); - TS_ASSERT(script.CallFunction(global, "uneval", source, v1)); + TS_ASSERT(ScriptFunction::Call(rq, global, "uneval", source, v1)); if (expected) TS_ASSERT_STR_EQUALS(source, expected); T v2 = T(); TS_ASSERT(ScriptInterface::FromJSVal(rq, v1, v2)); TS_ASSERT_EQUALS(value, v2); } template void call_prototype_function(const T& u, const T& v, const std::string& func, const std::string& expected) { ScriptInterface script("Test", "Test", g_ScriptContext); TS_ASSERT(script.LoadGlobalScripts()); ScriptRequest rq(script); JS::RootedValue v1(rq.cx); ScriptInterface::ToJSVal(rq, &v1, v); JS::RootedValue u1(rq.cx); ScriptInterface::ToJSVal(rq, &u1, u); T r; JS::RootedValue r1(rq.cx); - TS_ASSERT(script.CallFunction(u1, func.c_str(), r, v1)); + TS_ASSERT(ScriptFunction::Call(rq, u1, func.c_str(), r, v1)); ScriptInterface::ToJSVal(rq, &r1, r); std::string source; JS::RootedValue global(rq.cx, rq.globalValue()); - TS_ASSERT(script.CallFunction(global, "uneval", source, r1)); + TS_ASSERT(ScriptFunction::Call(rq, global, "uneval", source, r1)); TS_ASSERT_STR_EQUALS(source, expected); } public: void setUp() { g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.sim" / "", VFS_MOUNT_MUST_EXIST)); } void tearDown() { g_VFS.reset(); } void test_roundtrip() { roundtrip(true, "true"); roundtrip(false, "false"); roundtrip(0, "0"); roundtrip(0.5, "0.5"); roundtrip(1e9f, "1000000000"); roundtrip(1e30f, "1.0000000150474662e+30"); roundtrip(0, "0"); roundtrip(123, "123"); roundtrip(-123, "-123"); roundtrip(JSVAL_INT_MAX - 1, "2147483646"); roundtrip(JSVAL_INT_MAX, "2147483647"); roundtrip(JSVAL_INT_MIN + 1, "-2147483647"); roundtrip(JSVAL_INT_MIN, "-2147483648"); roundtrip(0, "0"); roundtrip(123, "123"); roundtrip(JSVAL_INT_MAX - 1, "2147483646"); roundtrip(JSVAL_INT_MAX, "2147483647"); roundtrip(static_cast(JSVAL_INT_MAX) + 1, "2147483648"); std::string s1 = "test"; s1[1] = '\0'; std::string s2 = "тест"; s2[2] = s2[3] = '\0'; std::wstring w1 = L"test"; w1[1] = '\0'; std::wstring w2 = L"тест"; w2[1] = '\0'; roundtrip("", "\"\""); roundtrip("test", "\"test\""); roundtrip("тест", "\"\\u0442\\u0435\\u0441\\u0442\""); roundtrip(s1, "\"t\\x00st\""); roundtrip(s2, "\"\\u0442\\x00\\x00\\u0441\\u0442\""); roundtrip(L"", "\"\""); roundtrip(L"test", "\"test\""); roundtrip(L"тест", "\"\\u0442\\u0435\\u0441\\u0442\""); roundtrip(w1, "\"t\\x00st\""); roundtrip(w2, "\"\\u0442\\x00\\u0441\\u0442\""); convert_to("", "\"\""); convert_to("test", "\"test\""); convert_to(s1.c_str(), "\"t\""); convert_to(s2.c_str(), "\"\\xD1\\x82\""); roundtrip(fixed::FromInt(0), "0"); roundtrip(fixed::FromInt(123), "123"); roundtrip(fixed::FromInt(-123), "-123"); roundtrip(fixed::FromDouble(123.25), "123.25"); } void test_integers() { ScriptInterface script("Test", "Test", g_ScriptContext); ScriptRequest rq(script); // using new uninitialized variables each time to be sure the test doesn't succeeed if ToJSVal doesn't touch the value at all. JS::RootedValue val0(rq.cx), val1(rq.cx), val2(rq.cx), val3(rq.cx), val4(rq.cx), val5(rq.cx), val6(rq.cx), val7(rq.cx), val8(rq.cx); ScriptInterface::ToJSVal(rq, &val0, 0); ScriptInterface::ToJSVal(rq, &val1, JSVAL_INT_MAX - 1); ScriptInterface::ToJSVal(rq, &val2, JSVAL_INT_MAX); ScriptInterface::ToJSVal(rq, &val3, JSVAL_INT_MIN + 1); ScriptInterface::ToJSVal(rq, &val4, -(i64)2147483648u); // JSVAL_INT_MIN TS_ASSERT(val0.isInt32()); TS_ASSERT(val1.isInt32()); TS_ASSERT(val2.isInt32()); TS_ASSERT(val3.isInt32()); TS_ASSERT(val4.isInt32()); ScriptInterface::ToJSVal(rq, &val5, 0); ScriptInterface::ToJSVal(rq, &val6, 2147483646u); // JSVAL_INT_MAX-1 ScriptInterface::ToJSVal(rq, &val7, 2147483647u); // JSVAL_INT_MAX ScriptInterface::ToJSVal(rq, &val8, 2147483648u); // JSVAL_INT_MAX+1 TS_ASSERT(val5.isInt32()); TS_ASSERT(val6.isInt32()); TS_ASSERT(val7.isInt32()); TS_ASSERT(val8.isDouble()); } void test_nonfinite() { roundtrip(std::numeric_limits::infinity(), "Infinity"); roundtrip(-std::numeric_limits::infinity(), "-Infinity"); convert_to(std::numeric_limits::quiet_NaN(), "NaN"); // can't use roundtrip since nan != nan ScriptInterface script("Test", "Test", g_ScriptContext); ScriptRequest rq(script); float f = 0; JS::RootedValue testNANVal(rq.cx); ScriptInterface::ToJSVal(rq, &testNANVal, NAN); TS_ASSERT(ScriptInterface::FromJSVal(rq, testNANVal, f)); TS_ASSERT(isnan(f)); } // NOTE: fixed and vector conversions are defined in simulation2/scripting/EngineScriptConversions.cpp void test_fixed() { fixed f; f.SetInternalValue(10590283); roundtrip(f, "161.5948944091797"); f.SetInternalValue(-10590283); roundtrip(f, "-161.5948944091797"); f.SetInternalValue(2000000000); roundtrip(f, "30517.578125"); f.SetInternalValue(2000000001); roundtrip(f, "30517.57814025879"); } void test_vector2d() { CFixedVector2D v(fixed::Zero(), fixed::Pi()); roundtrip(v, "({x:0, y:3.1415863037109375})"); CFixedVector2D u(fixed::FromInt(1), fixed::Zero()); call_prototype_function(u, v, "add", "({x:1, y:3.1415863037109375})"); } void test_vector3d() { CFixedVector3D v(fixed::Zero(), fixed::Pi(), fixed::FromInt(1)); roundtrip(v, "({x:0, y:3.1415863037109375, z:1})"); CFixedVector3D u(fixed::Pi(), fixed::Zero(), fixed::FromInt(2)); call_prototype_function(u, v, "add", "({x:3.1415863037109375, y:3.1415863037109375, z:3})"); } void test_utf8utf16_conversion() { // Fancier conversion: we store UTF8 and get UTF16 and vice-versa ScriptInterface script("Test", "Test", g_ScriptContext); TS_ASSERT(script.LoadGlobalScripts()); ScriptRequest rq(script); std::string in_utf8("éè!§$-aezi134900°°©©¢¢ÇÇ'{¶«¡Ç'[å»ÛÁØ"); std::wstring in_utf16(L"éè!§$-aezi134900°°©©¢¢ÇÇ'{¶«¡Ç'[å»ÛÁØ"); JS::RootedValue v1(rq.cx); ScriptInterface::ToJSVal(rq, &v1, in_utf8); std::wstring test_out_utf16; TS_ASSERT(ScriptInterface::FromJSVal(rq, v1, test_out_utf16)); TS_ASSERT_EQUALS(test_out_utf16, in_utf16); JS::RootedValue v2(rq.cx); ScriptInterface::ToJSVal(rq, &v2, in_utf16); std::string test_out_utf8; TS_ASSERT(ScriptInterface::FromJSVal(rq, v2, test_out_utf8)); TS_ASSERT_EQUALS(test_out_utf8, in_utf8); } }; Index: ps/trunk/source/scriptinterface/tests/test_ScriptInterface.h =================================================================== --- ps/trunk/source/scriptinterface/tests/test_ScriptInterface.h (revision 25353) +++ ps/trunk/source/scriptinterface/tests/test_ScriptInterface.h (revision 25354) @@ -1,273 +1,268 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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 "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "ps/CLogger.h" #include class TestScriptInterface : public CxxTest::TestSuite { public: void test_loadscript_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); TestLogger logger; TS_ASSERT(script.LoadScript(L"test.js", "var x = 1+1;")); TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "JavaScript error"); TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "JavaScript warning"); } void test_loadscript_error() { ScriptInterface script("Test", "Test", g_ScriptContext); TestLogger logger; TS_ASSERT(!script.LoadScript(L"test.js", "1+")); TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "ERROR: JavaScript error: test.js line 2\nexpected expression, got \'}\'"); } void test_loadscript_strict_warning() { ScriptInterface script("Test", "Test", g_ScriptContext); TestLogger logger; // in strict mode, this inside a function doesn't point to the global object TS_ASSERT(script.LoadScript(L"test.js", "var isStrict = (function() { return !this; })();warn('isStrict is '+isStrict);")); TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "WARNING: isStrict is true"); } void test_loadscript_strict_error() { ScriptInterface script("Test", "Test", g_ScriptContext); TestLogger logger; TS_ASSERT(!script.LoadScript(L"test.js", "with(1){}")); TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "ERROR: JavaScript error: test.js line 1\nstrict mode code may not contain \'with\' statements"); } void test_clone_basic() { ScriptInterface script1("Test", "Test", g_ScriptContext); ScriptInterface script2("Test", "Test", g_ScriptContext); ScriptRequest rq1(script1); JS::RootedValue obj1(rq1.cx); TS_ASSERT(script1.Eval("({'x': 123, 'y': [1, 1.5, '2', 'test', undefined, null, true, false]})", &obj1)); { ScriptRequest rq2(script2); JS::RootedValue obj2(rq2.cx, script2.CloneValueFromOtherCompartment(script1, obj1)); std::string source; - TS_ASSERT(script2.CallFunction(obj2, "toSource", source)); + TS_ASSERT(ScriptFunction::Call(rq2, obj2, "toSource", source)); TS_ASSERT_STR_EQUALS(source, "({x:123, y:[1, 1.5, \"2\", \"test\", (void 0), null, true, false]})"); } } void test_clone_getters() { // The tests should be run with JS_SetGCZeal so this can try to find GC bugs ScriptInterface script1("Test", "Test", g_ScriptContext); ScriptInterface script2("Test", "Test", g_ScriptContext); ScriptRequest rq1(script1); JS::RootedValue obj1(rq1.cx); TS_ASSERT(script1.Eval("var s = '?'; var v = ({get x() { return 123 }, 'y': {'w':{get z() { delete v.y; delete v.n; v = null; s += s; return 4 }}}, 'n': 100}); v", &obj1)); { ScriptRequest rq2(script2); JS::RootedValue obj2(rq2.cx, script2.CloneValueFromOtherCompartment(script1, obj1)); std::string source; - TS_ASSERT(script2.CallFunction(obj2, "toSource", source)); + TS_ASSERT(ScriptFunction::Call(rq2, obj2, "toSource", source)); TS_ASSERT_STR_EQUALS(source, "({x:123, y:{w:{z:4}}})"); } } void test_clone_cyclic() { ScriptInterface script1("Test", "Test", g_ScriptContext); ScriptInterface script2("Test", "Test", g_ScriptContext); ScriptRequest rq1(script1); JS::RootedValue obj1(rq1.cx); TS_ASSERT(script1.Eval("var x = []; x[0] = x; ({'a': x, 'b': x})", &obj1)); { ScriptRequest rq2(script2); JS::RootedValue obj2(rq2.cx, script2.CloneValueFromOtherCompartment(script1, obj1)); // Use JSAPI function to check if the values of the properties "a", "b" are equals a.x[0] JS::RootedValue prop_a(rq2.cx); JS::RootedValue prop_b(rq2.cx); JS::RootedValue prop_x1(rq2.cx); TS_ASSERT(script2.GetProperty(obj2, "a", &prop_a)); TS_ASSERT(script2.GetProperty(obj2, "b", &prop_b)); TS_ASSERT(prop_a.isObject()); TS_ASSERT(prop_b.isObject()); TS_ASSERT(script2.GetProperty(prop_a, "0", &prop_x1)); TS_ASSERT(prop_x1.get() == prop_a.get()); TS_ASSERT(prop_x1.get() == prop_b.get()); } } /** * This test is mainly to make sure that all required template overloads get instantiated at least once so that compiler errors * in these functions are revealed instantly (but it also tests the basic functionality of these functions). */ void test_rooted_templates() { ScriptInterface script("Test", "Test", g_ScriptContext); ScriptRequest rq(script); JS::RootedValue val(rq.cx); JS::RootedValue out(rq.cx); TS_ASSERT(script.Eval("({ " "'0':0," "inc:function() { this[0]++; return this[0]; }, " "setTo:function(nbr) { this[0] = nbr; }, " "add:function(nbr) { this[0] += nbr; return this[0]; } " "})" , &val)); JS::RootedValue nbrVal(rq.cx, JS::NumberValue(3)); int nbr = 0; - // CallFunctionVoid JS::RootedValue& parameter overload - script.CallFunctionVoid(val, "setTo", nbrVal); + ScriptFunction::CallVoid(rq, val, "setTo", nbrVal); - // CallFunction JS::RootedValue* out parameter overload - script.CallFunction(val, "inc", &out); + // Test that a mutable handle value as return value works. + ScriptFunction::Call(rq, val, "inc", &out); ScriptInterface::FromJSVal(rq, out, nbr); TS_ASSERT_EQUALS(4, nbr); - // CallFunction const JS::RootedValue& parameter overload - script.CallFunction(val, "add", nbr, nbrVal); + ScriptFunction::Call(rq, val, "add", nbr, nbrVal); TS_ASSERT_EQUALS(7, nbr); // GetProperty JS::RootedValue* overload nbr = 0; script.GetProperty(val, "0", &out); ScriptInterface::FromJSVal(rq, out, nbr); TS_ASSERT_EQUALS(nbr, 7); // GetPropertyInt JS::RootedValue* overload nbr = 0; script.GetPropertyInt(val, 0, &out); ScriptInterface::FromJSVal(rq, out, nbr); TS_ASSERT_EQUALS(nbr, 7); handle_templates_test(script, val, &out, nbrVal); } void handle_templates_test(const ScriptInterface& script, JS::HandleValue val, JS::MutableHandleValue out, JS::HandleValue nbrVal) { ScriptRequest rq(script); int nbr = 0; - // CallFunctionVoid JS::HandleValue parameter overload - script.CallFunctionVoid(val, "setTo", nbrVal); - - // CallFunction JS::MutableHandleValue out parameter overload - script.CallFunction(val, "inc", out); + ScriptFunction::CallVoid(rq, val, "setTo", nbrVal); + ScriptFunction::Call(rq, val, "inc", out); ScriptInterface::FromJSVal(rq, out, nbr); TS_ASSERT_EQUALS(4, nbr); - // CallFunction const JS::HandleValue& parameter overload - script.CallFunction(val, "add", nbr, nbrVal); + ScriptFunction::Call(rq, val, "add", nbr, nbrVal); TS_ASSERT_EQUALS(7, nbr); // GetProperty JS::MutableHandleValue overload nbr = 0; script.GetProperty(val, "0", out); ScriptInterface::FromJSVal(rq, out, nbr); TS_ASSERT_EQUALS(nbr, 7); // GetPropertyInt JS::MutableHandleValue overload nbr = 0; script.GetPropertyInt(val, 0, out); ScriptInterface::FromJSVal(rq, out, nbr); TS_ASSERT_EQUALS(nbr, 7); } void test_random() { ScriptInterface script("Test", "Test", g_ScriptContext); double d1, d2; TS_ASSERT(script.Eval("Math.random()", d1)); TS_ASSERT(script.Eval("Math.random()", d2)); TS_ASSERT_DIFFERS(d1, d2); boost::rand48 rng; script.ReplaceNondeterministicRNG(rng); rng.seed((u64)0); TS_ASSERT(script.Eval("Math.random()", d1)); TS_ASSERT(script.Eval("Math.random()", d2)); TS_ASSERT_DIFFERS(d1, d2); rng.seed((u64)0); TS_ASSERT(script.Eval("Math.random()", d2)); TS_ASSERT_EQUALS(d1, d2); } void test_json() { ScriptInterface script("Test", "Test", g_ScriptContext); ScriptRequest rq(script); std::string input = "({'x':1,'z':[2,'3\\u263A\\ud800'],\"y\":true})"; JS::RootedValue val(rq.cx); TS_ASSERT(script.Eval(input.c_str(), &val)); std::string stringified = script.StringifyJSON(&val); TS_ASSERT_STR_EQUALS(stringified, "{\n \"x\": 1,\n \"z\": [\n 2,\n \"3\u263A\\ud800\"\n ],\n \"y\": true\n}"); TS_ASSERT(script.ParseJSON(stringified, &val)); TS_ASSERT_STR_EQUALS(script.ToString(&val), "({x:1, z:[2, \"3\\u263A\\uD800\"], y:true})"); } // This function tests a common way to mod functions, by crating a wrapper that // extends the functionality and is then assigned to the name of the function. void test_function_override() { ScriptInterface script("Test", "Test", g_ScriptContext); ScriptRequest rq(script); TS_ASSERT(script.Eval( "function f() { return 1; }" "f = (function (originalFunction) {" "return function () { return originalFunction() + 1; }" "})(f);" )); JS::RootedValue out(rq.cx); TS_ASSERT(script.Eval("f()", &out)); int outNbr = 0; ScriptInterface::FromJSVal(rq, out, outNbr); TS_ASSERT_EQUALS(2, outNbr); } }; Index: ps/trunk/source/simulation2/Simulation2.cpp =================================================================== --- ps/trunk/source/simulation2/Simulation2.cpp (revision 25353) +++ ps/trunk/source/simulation2/Simulation2.cpp (revision 25354) @@ -1,995 +1,996 @@ /* Copyright (C) 2021 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 "Simulation2.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ComponentManager.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" #include "simulation2/components/ICmpAIManager.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpTemplateManager.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "lib/file/vfs/vfs_util.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/Util.h" #include "ps/XML/Xeromyces.h" #include #include #include class CSimulation2Impl { public: CSimulation2Impl(CUnitManager* unitManager, shared_ptr cx, CTerrain* terrain) : m_SimContext(), m_ComponentManager(m_SimContext, cx), m_EnableOOSLog(false), m_EnableSerializationTest(false), m_RejoinTestTurn(-1), m_TestingRejoin(false), m_MapSettings(cx->GetGeneralJSContext()), m_InitAttributes(cx->GetGeneralJSContext()) { m_SimContext.m_UnitManager = unitManager; m_SimContext.m_Terrain = terrain; m_ComponentManager.LoadComponentTypes(); RegisterFileReloadFunc(ReloadChangedFileCB, this); // Tests won't have config initialised if (CConfigDB::IsInitialised()) { CFG_GET_VAL("ooslog", m_EnableOOSLog); CFG_GET_VAL("serializationtest", m_EnableSerializationTest); CFG_GET_VAL("rejointest", m_RejoinTestTurn); if (m_RejoinTestTurn < 0) // Handle bogus values of the arg m_RejoinTestTurn = -1; } if (m_EnableOOSLog) { m_OOSLogPath = createDateIndexSubdirectory(psLogDir() / "oos_logs"); debug_printf("Writing ooslogs to %s\n", m_OOSLogPath.string8().c_str()); } } ~CSimulation2Impl() { UnregisterFileReloadFunc(ReloadChangedFileCB, this); } void ResetState(bool skipScriptedComponents, bool skipAI) { m_DeltaTime = 0.0; m_LastFrameOffset = 0.0f; m_TurnNumber = 0; ResetComponentState(m_ComponentManager, skipScriptedComponents, skipAI); } static void ResetComponentState(CComponentManager& componentManager, bool skipScriptedComponents, bool skipAI) { componentManager.ResetState(); componentManager.InitSystemEntity(); componentManager.AddSystemComponents(skipScriptedComponents, skipAI); } static bool LoadDefaultScripts(CComponentManager& componentManager, std::set* loadedScripts); static bool LoadScripts(CComponentManager& componentManager, std::set* loadedScripts, const VfsPath& path); static bool LoadTriggerScripts(CComponentManager& componentManager, JS::HandleValue mapSettings, std::set* loadedScripts); Status ReloadChangedFile(const VfsPath& path); static Status ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFile(path); } int ProgressiveLoad(); void Update(int turnLength, const std::vector& commands); static void UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector& commands); void Interpolate(float simFrameLength, float frameOffset, float realFrameLength); void DumpState(); CSimContext m_SimContext; CComponentManager m_ComponentManager; double m_DeltaTime; float m_LastFrameOffset; std::string m_StartupScript; JS::PersistentRootedValue m_InitAttributes; JS::PersistentRootedValue m_MapSettings; std::set m_LoadedScripts; uint32_t m_TurnNumber; bool m_EnableOOSLog; OsPath m_OOSLogPath; // Functions and data for the serialization test mode: (see Update() for relevant comments) bool m_EnableSerializationTest; int m_RejoinTestTurn; bool m_TestingRejoin; // Secondary simulation (NB: order matters for destruction). std::unique_ptr m_SecondaryComponentManager; std::unique_ptr m_SecondaryTerrain; std::unique_ptr m_SecondaryContext; std::unique_ptr> m_SecondaryLoadedScripts; struct SerializationTestState { std::stringstream state; std::stringstream debug; std::string hash; }; void DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix); void ReportSerializationFailure( SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter, SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter); void InitRNGSeedSimulation(); void InitRNGSeedAI(); static std::vector CloneCommandsFromOtherCompartment(const ScriptInterface& oldScript, const ScriptInterface& newScript, const std::vector& commands) { std::vector newCommands; newCommands.reserve(commands.size()); ScriptRequest rqNew(newScript); for (const SimulationCommand& command : commands) { JS::RootedValue tmpCommand(rqNew.cx, newScript.CloneValueFromOtherCompartment(oldScript, command.data)); newScript.FreezeObject(tmpCommand, true); SimulationCommand cmd(command.player, rqNew.cx, tmpCommand); newCommands.emplace_back(std::move(cmd)); } return newCommands; } }; bool CSimulation2Impl::LoadDefaultScripts(CComponentManager& componentManager, std::set* loadedScripts) { return ( LoadScripts(componentManager, loadedScripts, L"simulation/components/interfaces/") && LoadScripts(componentManager, loadedScripts, L"simulation/helpers/") && LoadScripts(componentManager, loadedScripts, L"simulation/components/") ); } bool CSimulation2Impl::LoadScripts(CComponentManager& componentManager, std::set* loadedScripts, const VfsPath& path) { VfsPaths pathnames; if (vfs::GetPathnames(g_VFS, path, L"*.js", pathnames) < 0) return false; bool ok = true; for (const VfsPath& scriptPath : pathnames) { if (loadedScripts) loadedScripts->insert(scriptPath); LOGMESSAGE("Loading simulation script '%s'", scriptPath.string8()); if (!componentManager.LoadScript(scriptPath)) ok = false; } return ok; } bool CSimulation2Impl::LoadTriggerScripts(CComponentManager& componentManager, JS::HandleValue mapSettings, std::set* loadedScripts) { bool ok = true; if (componentManager.GetScriptInterface().HasProperty(mapSettings, "TriggerScripts")) { std::vector scriptNames; componentManager.GetScriptInterface().GetProperty(mapSettings, "TriggerScripts", scriptNames); for (const std::string& triggerScript : scriptNames) { std::string scriptName = "maps/" + triggerScript; if (loadedScripts) { if (loadedScripts->find(scriptName) != loadedScripts->end()) continue; loadedScripts->insert(scriptName); } LOGMESSAGE("Loading trigger script '%s'", scriptName.c_str()); if (!componentManager.LoadScript(scriptName.data())) ok = false; } } return ok; } Status CSimulation2Impl::ReloadChangedFile(const VfsPath& path) { // Ignore if this file wasn't loaded as a script // (TODO: Maybe we ought to load in any new .js files that are created in the right directories) if (m_LoadedScripts.find(path) == m_LoadedScripts.end()) return INFO::OK; // If the file doesn't exist (e.g. it was deleted), don't bother loading it since that'll give an error message. // (Also don't bother trying to 'unload' it from the component manager, because that's not possible) if (!VfsFileExists(path)) return INFO::OK; LOGMESSAGE("Reloading simulation script '%s'", path.string8()); if (!m_ComponentManager.LoadScript(path, true)) return ERR::FAIL; return INFO::OK; } int CSimulation2Impl::ProgressiveLoad() { // yield after this time is reached. balances increased progress bar // smoothness vs. slowing down loading. const double end_time = timer_Time() + 200e-3; int ret; do { bool progressed = false; int total = 0; int progress = 0; CMessageProgressiveLoad msg(&progressed, &total, &progress); m_ComponentManager.BroadcastMessage(msg); if (!progressed || total == 0) return 0; // we have nothing left to load ret = Clamp(100*progress / total, 1, 100); } while (timer_Time() < end_time); return ret; } void CSimulation2Impl::DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix) { if (!state.hash.empty()) { std::ofstream file (OsString(path / (L"hash." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc); file << Hexify(state.hash); } if (!state.debug.str().empty()) { std::ofstream file (OsString(path / (L"debug." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc); file << state.debug.str(); } if (!state.state.str().empty()) { std::ofstream file (OsString(path / (L"state." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); file << state.state.str(); } } void CSimulation2Impl::ReportSerializationFailure( SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter, SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter) { const OsPath path = createDateIndexSubdirectory(psLogDir() / "serializationtest"); debug_printf("Writing serializationtest-data to %s\n", path.string8().c_str()); // Clean up obsolete files from previous runs wunlink(path / "hash.before.a"); wunlink(path / "hash.before.b"); wunlink(path / "debug.before.a"); wunlink(path / "debug.before.b"); wunlink(path / "state.before.a"); wunlink(path / "state.before.b"); wunlink(path / "hash.after.a"); wunlink(path / "hash.after.b"); wunlink(path / "debug.after.a"); wunlink(path / "debug.after.b"); wunlink(path / "state.after.a"); wunlink(path / "state.after.b"); if (primaryStateBefore) DumpSerializationTestState(*primaryStateBefore, path, L"before.a"); if (primaryStateAfter) DumpSerializationTestState(*primaryStateAfter, path, L"after.a"); if (secondaryStateBefore) DumpSerializationTestState(*secondaryStateBefore, path, L"before.b"); if (secondaryStateAfter) DumpSerializationTestState(*secondaryStateAfter, path, L"after.b"); debug_warn(L"Serialization test failure"); } void CSimulation2Impl::InitRNGSeedSimulation() { u32 seed = 0; if (!m_ComponentManager.GetScriptInterface().HasProperty(m_MapSettings, "Seed") || !m_ComponentManager.GetScriptInterface().GetProperty(m_MapSettings, "Seed", seed)) LOGWARNING("CSimulation2Impl::InitRNGSeedSimulation: No seed value specified - using %d", seed); m_ComponentManager.SetRNGSeed(seed); } void CSimulation2Impl::InitRNGSeedAI() { u32 seed = 0; if (!m_ComponentManager.GetScriptInterface().HasProperty(m_MapSettings, "AISeed") || !m_ComponentManager.GetScriptInterface().GetProperty(m_MapSettings, "AISeed", seed)) LOGWARNING("CSimulation2Impl::InitRNGSeedAI: No seed value specified - using %d", seed); CmpPtr cmpAIManager(m_SimContext, SYSTEM_ENTITY); if (cmpAIManager) cmpAIManager->SetRNGSeed(seed); } void CSimulation2Impl::Update(int turnLength, const std::vector& commands) { PROFILE3("sim update"); PROFILE2_ATTR("turn %d", (int)m_TurnNumber); fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000; /* * In serialization test mode, we save the original (primary) simulation state before each turn update. * We run the update, then load the saved state into a secondary context. * We serialize that again and compare to the original serialization (to check that * serialize->deserialize->serialize is equivalent to serialize). * Then we run the update on the secondary context, and check that its new serialized * state matches the primary context after the update (to check that the simulation doesn't depend * on anything that's not serialized). * * In rejoin test mode, the secondary simulation is initialized from serialized data at turn N, then both * simulations run independantly while comparing their states each turn. This is way faster than a * complete serialization test and allows us to reproduce OOSes on rejoin. */ const bool serializationTestDebugDump = false; // set true to save human-readable state dumps before an error is detected, for debugging (but slow) const bool serializationTestHash = true; // set true to save and compare hash of state SerializationTestState primaryStateBefore; const ScriptInterface& scriptInterface = m_ComponentManager.GetScriptInterface(); const bool startRejoinTest = (int64_t) m_RejoinTestTurn == m_TurnNumber; if (startRejoinTest) m_TestingRejoin = true; if (m_EnableSerializationTest || m_TestingRejoin) { ENSURE(m_ComponentManager.SerializeState(primaryStateBefore.state)); if (serializationTestDebugDump) ENSURE(m_ComponentManager.DumpDebugState(primaryStateBefore.debug, false)); if (serializationTestHash) ENSURE(m_ComponentManager.ComputeStateHash(primaryStateBefore.hash, false)); } UpdateComponents(m_SimContext, turnLengthFixed, commands); if (m_EnableSerializationTest || startRejoinTest) { if (startRejoinTest) debug_printf("Initializing the secondary simulation\n"); m_SecondaryTerrain = std::make_unique(); m_SecondaryContext = std::make_unique(); m_SecondaryContext->m_Terrain = m_SecondaryTerrain.get(); m_SecondaryComponentManager = std::make_unique(*m_SecondaryContext, scriptInterface.GetContext()); m_SecondaryComponentManager->LoadComponentTypes(); m_SecondaryLoadedScripts = std::make_unique>(); ENSURE(LoadDefaultScripts(*m_SecondaryComponentManager, m_SecondaryLoadedScripts.get())); ResetComponentState(*m_SecondaryComponentManager, false, false); // Load the trigger scripts after we have loaded the simulation. { ScriptRequest rq2(m_SecondaryComponentManager->GetScriptInterface()); JS::RootedValue mapSettingsCloned(rq2.cx, m_SecondaryComponentManager->GetScriptInterface().CloneValueFromOtherCompartment(scriptInterface, m_MapSettings)); ENSURE(LoadTriggerScripts(*m_SecondaryComponentManager, mapSettingsCloned, m_SecondaryLoadedScripts.get())); } // Load the map into the secondary simulation LDR_BeginRegistering(); std::unique_ptr mapReader = std::make_unique(); std::string mapType; scriptInterface.GetProperty(m_InitAttributes, "mapType", mapType); if (mapType == "random") { // TODO: support random map scripts debug_warn(L"Serialization test mode does not support random maps"); } else { std::wstring mapFile; scriptInterface.GetProperty(m_InitAttributes, "map", mapFile); VfsPath mapfilename = VfsPath(mapFile).ChangeExtension(L".pmp"); mapReader->LoadMap(mapfilename, *scriptInterface.GetContext(), JS::UndefinedHandleValue, m_SecondaryTerrain.get(), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, m_SecondaryContext.get(), INVALID_PLAYER, true); // throws exception on failure } LDR_EndRegistering(); ENSURE(LDR_NonprogressiveLoad() == INFO::OK); ENSURE(m_SecondaryComponentManager->DeserializeState(primaryStateBefore.state)); } if (m_EnableSerializationTest || m_TestingRejoin) { SerializationTestState secondaryStateBefore; ENSURE(m_SecondaryComponentManager->SerializeState(secondaryStateBefore.state)); if (serializationTestDebugDump) ENSURE(m_SecondaryComponentManager->DumpDebugState(secondaryStateBefore.debug, false)); if (serializationTestHash) ENSURE(m_SecondaryComponentManager->ComputeStateHash(secondaryStateBefore.hash, false)); if (primaryStateBefore.state.str() != secondaryStateBefore.state.str() || primaryStateBefore.hash != secondaryStateBefore.hash) { ReportSerializationFailure(&primaryStateBefore, NULL, &secondaryStateBefore, NULL); } SerializationTestState primaryStateAfter; ENSURE(m_ComponentManager.SerializeState(primaryStateAfter.state)); if (serializationTestHash) ENSURE(m_ComponentManager.ComputeStateHash(primaryStateAfter.hash, false)); UpdateComponents(*m_SecondaryContext, turnLengthFixed, CloneCommandsFromOtherCompartment(scriptInterface, m_SecondaryComponentManager->GetScriptInterface(), commands)); SerializationTestState secondaryStateAfter; ENSURE(m_SecondaryComponentManager->SerializeState(secondaryStateAfter.state)); if (serializationTestHash) ENSURE(m_SecondaryComponentManager->ComputeStateHash(secondaryStateAfter.hash, false)); if (primaryStateAfter.state.str() != secondaryStateAfter.state.str() || primaryStateAfter.hash != secondaryStateAfter.hash) { // Only do the (slow) dumping now we know we're going to need to report it ENSURE(m_ComponentManager.DumpDebugState(primaryStateAfter.debug, false)); ENSURE(m_SecondaryComponentManager->DumpDebugState(secondaryStateAfter.debug, false)); ReportSerializationFailure(&primaryStateBefore, &primaryStateAfter, &secondaryStateBefore, &secondaryStateAfter); } } // Run the GC occasionally // No delay because a lot of garbage accumulates in one turn and in non-visual replays there are // much more turns in the same time than in normal games. // Every 500 turns we run a shrinking GC, which decommits unused memory and frees all JIT code. // Based on testing, this seems to be a good compromise between memory usage and performance. // Also check the comment about gcPreserveCode in the ScriptInterface code and this forum topic: // http://www.wildfiregames.com/forum/index.php?showtopic=18466&p=300323 // // (TODO: we ought to schedule this for a frame where we're not // running the sim update, to spread the load) if (m_TurnNumber % 500 == 0) scriptInterface.GetContext()->ShrinkingGC(); else scriptInterface.GetContext()->MaybeIncrementalGC(0.0f); if (m_EnableOOSLog) DumpState(); // Start computing AI for the next turn CmpPtr cmpAIManager(m_SimContext, SYSTEM_ENTITY); if (cmpAIManager) cmpAIManager->StartComputation(); ++m_TurnNumber; } void CSimulation2Impl::UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector& commands) { // TODO: the update process is pretty ugly, with lots of messages and dependencies // between different components. Ought to work out a nicer way to do this. CComponentManager& componentManager = simContext.GetComponentManager(); CmpPtr cmpPathfinder(simContext, SYSTEM_ENTITY); if (cmpPathfinder) cmpPathfinder->SendRequestedPaths(); { PROFILE2("Sim - Update Start"); CMessageTurnStart msgTurnStart; componentManager.BroadcastMessage(msgTurnStart); } // Push AI commands onto the queue before we use them CmpPtr cmpAIManager(simContext, SYSTEM_ENTITY); if (cmpAIManager) cmpAIManager->PushCommands(); CmpPtr cmpCommandQueue(simContext, SYSTEM_ENTITY); if (cmpCommandQueue) cmpCommandQueue->FlushTurn(commands); // Process newly generated move commands so the UI feels snappy if (cmpPathfinder) { cmpPathfinder->StartProcessingMoves(true); cmpPathfinder->SendRequestedPaths(); } // Send all the update phases { PROFILE2("Sim - Update"); CMessageUpdate msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } { CMessageUpdate_MotionFormation msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } // Process move commands for formations (group proxy) if (cmpPathfinder) { cmpPathfinder->StartProcessingMoves(true); cmpPathfinder->SendRequestedPaths(); } { PROFILE2("Sim - Motion Unit"); CMessageUpdate_MotionUnit msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } { PROFILE2("Sim - Update Final"); CMessageUpdate_Final msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } // Clean up any entities destroyed during the simulation update componentManager.FlushDestroyedComponents(); // Process all remaining moves if (cmpPathfinder) { cmpPathfinder->UpdateGrid(); cmpPathfinder->StartProcessingMoves(false); } } void CSimulation2Impl::Interpolate(float simFrameLength, float frameOffset, float realFrameLength) { PROFILE3("sim interpolate"); m_LastFrameOffset = frameOffset; CMessageInterpolate msg(simFrameLength, frameOffset, realFrameLength); m_ComponentManager.BroadcastMessage(msg); // Clean up any entities destroyed during interpolate (e.g. local corpses) m_ComponentManager.FlushDestroyedComponents(); } void CSimulation2Impl::DumpState() { PROFILE("DumpState"); std::stringstream name;\ name << std::setw(5) << std::setfill('0') << m_TurnNumber << ".txt"; const OsPath path = m_OOSLogPath / name.str(); std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); if (!DirectoryExists(m_OOSLogPath)) { LOGWARNING("OOS-log directory %s was deleted, creating it again.", m_OOSLogPath.string8().c_str()); CreateDirectories(m_OOSLogPath, 0700); } file << "State hash: " << std::hex; std::string hashRaw; m_ComponentManager.ComputeStateHash(hashRaw, false); for (size_t i = 0; i < hashRaw.size(); ++i) file << std::setfill('0') << std::setw(2) << (int)(unsigned char)hashRaw[i]; file << std::dec << "\n"; file << "\n"; m_ComponentManager.DumpDebugState(file, true); std::ofstream binfile (OsString(path.ChangeExtension(L".dat")).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); m_ComponentManager.SerializeState(binfile); } //////////////////////////////////////////////////////////////// CSimulation2::CSimulation2(CUnitManager* unitManager, shared_ptr cx, CTerrain* terrain) : m(new CSimulation2Impl(unitManager, cx, terrain)) { } CSimulation2::~CSimulation2() { delete m; } // Forward all method calls to the appropriate CSimulation2Impl/CComponentManager methods: void CSimulation2::EnableSerializationTest() { m->m_EnableSerializationTest = true; } void CSimulation2::EnableRejoinTest(int rejoinTestTurn) { m->m_RejoinTestTurn = rejoinTestTurn; } void CSimulation2::EnableOOSLog() { if (m->m_EnableOOSLog) return; m->m_EnableOOSLog = true; m->m_OOSLogPath = createDateIndexSubdirectory(psLogDir() / "oos_logs"); debug_printf("Writing ooslogs to %s\n", m->m_OOSLogPath.string8().c_str()); } entity_id_t CSimulation2::AddEntity(const std::wstring& templateName) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity()); } entity_id_t CSimulation2::AddEntity(const std::wstring& templateName, entity_id_t preferredId) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity(preferredId)); } entity_id_t CSimulation2::AddLocalEntity(const std::wstring& templateName) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewLocalEntity()); } void CSimulation2::DestroyEntity(entity_id_t ent) { m->m_ComponentManager.DestroyComponentsSoon(ent); } void CSimulation2::FlushDestroyedEntities() { m->m_ComponentManager.FlushDestroyedComponents(); } IComponent* CSimulation2::QueryInterface(entity_id_t ent, int iid) const { return m->m_ComponentManager.QueryInterface(ent, iid); } void CSimulation2::PostMessage(entity_id_t ent, const CMessage& msg) const { m->m_ComponentManager.PostMessage(ent, msg); } void CSimulation2::BroadcastMessage(const CMessage& msg) const { m->m_ComponentManager.BroadcastMessage(msg); } CSimulation2::InterfaceList CSimulation2::GetEntitiesWithInterface(int iid) { return m->m_ComponentManager.GetEntitiesWithInterface(iid); } const CSimulation2::InterfaceListUnordered& CSimulation2::GetEntitiesWithInterfaceUnordered(int iid) { return m->m_ComponentManager.GetEntitiesWithInterfaceUnordered(iid); } const CSimContext& CSimulation2::GetSimContext() const { return m->m_SimContext; } ScriptInterface& CSimulation2::GetScriptInterface() const { return m->m_ComponentManager.GetScriptInterface(); } void CSimulation2::PreInitGame() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); - GetScriptInterface().CallFunctionVoid(global, "PreInitGame"); + ScriptFunction::CallVoid(rq, global, "PreInitGame"); } void CSimulation2::InitGame() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue settings(rq.cx); JS::RootedValue tmpInitAttributes(rq.cx, GetInitAttributes()); GetScriptInterface().GetProperty(tmpInitAttributes, "settings", &settings); - GetScriptInterface().CallFunctionVoid(global, "InitGame", settings); + ScriptFunction::CallVoid(rq, global, "InitGame", settings); } void CSimulation2::Update(int turnLength) { std::vector commands; m->Update(turnLength, commands); } void CSimulation2::Update(int turnLength, const std::vector& commands) { m->Update(turnLength, commands); } void CSimulation2::Interpolate(float simFrameLength, float frameOffset, float realFrameLength) { m->Interpolate(simFrameLength, frameOffset, realFrameLength); } void CSimulation2::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) { PROFILE3("sim submit"); CMessageRenderSubmit msg(collector, frustum, culling); m->m_ComponentManager.BroadcastMessage(msg); } float CSimulation2::GetLastFrameOffset() const { return m->m_LastFrameOffset; } bool CSimulation2::LoadScripts(const VfsPath& path) { return m->LoadScripts(m->m_ComponentManager, &m->m_LoadedScripts, path); } bool CSimulation2::LoadDefaultScripts() { return m->LoadDefaultScripts(m->m_ComponentManager, &m->m_LoadedScripts); } void CSimulation2::SetStartupScript(const std::string& code) { m->m_StartupScript = code; } const std::string& CSimulation2::GetStartupScript() { return m->m_StartupScript; } void CSimulation2::SetInitAttributes(JS::HandleValue attribs) { m->m_InitAttributes = attribs; } JS::Value CSimulation2::GetInitAttributes() { return m->m_InitAttributes.get(); } void CSimulation2::GetInitAttributes(JS::MutableHandleValue ret) { ret.set(m->m_InitAttributes); } void CSimulation2::SetMapSettings(const std::string& settings) { m->m_ComponentManager.GetScriptInterface().ParseJSON(settings, &m->m_MapSettings); } void CSimulation2::SetMapSettings(JS::HandleValue settings) { m->m_MapSettings = settings; m->InitRNGSeedSimulation(); m->InitRNGSeedAI(); } std::string CSimulation2::GetMapSettingsString() { return m->m_ComponentManager.GetScriptInterface().StringifyJSON(&m->m_MapSettings); } void CSimulation2::GetMapSettings(JS::MutableHandleValue ret) { ret.set(m->m_MapSettings); } void CSimulation2::LoadPlayerSettings(bool newPlayers) { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); - GetScriptInterface().CallFunctionVoid(global, "LoadPlayerSettings", m->m_MapSettings, newPlayers); + ScriptFunction::CallVoid(rq, global, "LoadPlayerSettings", m->m_MapSettings, newPlayers); } void CSimulation2::LoadMapSettings() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); // Initialize here instead of in Update() - GetScriptInterface().CallFunctionVoid(global, "LoadMapSettings", m->m_MapSettings); + ScriptFunction::CallVoid(rq, global, "LoadMapSettings", m->m_MapSettings); GetScriptInterface().FreezeObject(m->m_InitAttributes, true); GetScriptInterface().SetGlobal("InitAttributes", m->m_InitAttributes, true, true, true); if (!m->m_StartupScript.empty()) GetScriptInterface().LoadScript(L"map startup script", m->m_StartupScript); // Load the trigger scripts after we have loaded the simulation and the map. m->LoadTriggerScripts(m->m_ComponentManager, m->m_MapSettings, &m->m_LoadedScripts); } int CSimulation2::ProgressiveLoad() { return m->ProgressiveLoad(); } Status CSimulation2::ReloadChangedFile(const VfsPath& path) { return m->ReloadChangedFile(path); } void CSimulation2::ResetState(bool skipScriptedComponents, bool skipAI) { m->ResetState(skipScriptedComponents, skipAI); } bool CSimulation2::ComputeStateHash(std::string& outHash, bool quick) { return m->m_ComponentManager.ComputeStateHash(outHash, quick); } bool CSimulation2::DumpDebugState(std::ostream& stream) { stream << "sim turn: " << m->m_TurnNumber << std::endl; return m->m_ComponentManager.DumpDebugState(stream, true); } bool CSimulation2::SerializeState(std::ostream& stream) { return m->m_ComponentManager.SerializeState(stream); } bool CSimulation2::DeserializeState(std::istream& stream) { // TODO: need to make sure the required SYSTEM_ENTITY components get constructed return m->m_ComponentManager.DeserializeState(stream); } void CSimulation2::ActivateRejoinTest(int turn) { if (m->m_RejoinTestTurn != -1) return; LOGMESSAGERENDER("Rejoin test will activate in %i turns", turn - m->m_TurnNumber); m->m_RejoinTestTurn = turn; } std::string CSimulation2::GenerateSchema() { return m->m_ComponentManager.GenerateSchema(); } static std::vector GetJSONData(const VfsPath& path) { VfsPaths pathnames; Status ret = vfs::GetPathnames(g_VFS, path, L"*.json", pathnames); if (ret != INFO::OK) { // Some error reading directory wchar_t error[200]; LOGERROR("Error reading directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error)))); return std::vector(); } std::vector data; for (const VfsPath& p : pathnames) { // Load JSON file CVFSFile file; PSRETURN loadStatus = file.Load(g_VFS, p); if (loadStatus != PSRETURN_OK) { LOGERROR("GetJSONData: Failed to load file '%s': %s", p.string8(), GetErrorString(loadStatus)); continue; } data.push_back(file.DecodeUTF8()); // assume it's UTF-8 } return data; } std::vector CSimulation2::GetRMSData() { return GetJSONData(L"maps/random/"); } std::vector CSimulation2::GetCivData() { return GetJSONData(L"simulation/data/civs/"); } std::vector CSimulation2::GetVictoryConditiondData() { return GetJSONData(L"simulation/data/settings/victory_conditions/"); } static std::string ReadJSON(const VfsPath& path) { if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return std::string(); } // Load JSON file CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return std::string(); } return file.DecodeUTF8(); // assume it's UTF-8 } std::string CSimulation2::GetPlayerDefaults() { return ReadJSON(L"simulation/data/settings/player_defaults.json"); } std::string CSimulation2::GetMapSizes() { return ReadJSON(L"simulation/data/settings/map_sizes.json"); } std::string CSimulation2::GetAIData() { const ScriptInterface& scriptInterface = GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue aiData(rq.cx, ICmpAIManager::GetAIs(scriptInterface)); // Build single JSON string with array of AI data JS::RootedValue ais(rq.cx); if (!ScriptInterface::CreateObject(rq, &ais, "AIData", aiData)) return std::string(); return scriptInterface.StringifyJSON(&ais); } Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 25353) +++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 25354) @@ -1,1099 +1,1102 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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 "simulation2/system/Component.h" #include "ICmpAIManager.h" #include "simulation2/MessageTypes.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "lib/tex/tex.h" #include "lib/allocators/shared_ptr.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_VFS.h" #include "ps/TemplateLoader.h" #include "ps/Util.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "simulation2/components/ICmpAIInterface.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/helpers/HierarchicalPathfinder.h" #include "simulation2/helpers/LongPathfinder.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/SerializedTypes.h" #include "simulation2/serialization/StdDeserializer.h" #include "simulation2/serialization/StdSerializer.h" extern void QuitEngine(); /** * @file * Player AI interface. * AI is primarily scripted, and the CCmpAIManager component defined here * takes care of managing all the scripts. * * To avoid slow AI scripts causing jerky rendering, they are run in a background * thread (maintained by CAIWorker) so that it's okay if they take a whole simulation * turn before returning their results (though preferably they shouldn't use nearly * that much CPU). * * CCmpAIManager grabs the world state after each turn (making use of AIInterface.js * and AIProxy.js to decide what data to include) then passes it to CAIWorker. * The AI scripts will then run asynchronously and return a list of commands to execute. * Any attempts to read the command list (including indirectly via serialization) * will block until it's actually completed, so the rest of the engine should avoid * reading it for as long as possible. * * JS::Values are passed between the game and AI threads using ScriptInterface::StructuredClone. * * TODO: actually the thread isn't implemented yet, because performance hasn't been * sufficiently problematic to justify the complexity yet, but the CAIWorker interface * is designed to hopefully support threading when we want it. */ /** * Implements worker thread for CCmpAIManager. */ class CAIWorker { private: class CAIPlayer { NONCOPYABLE(CAIPlayer); public: CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior, shared_ptr scriptInterface) : m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_Behavior(behavior), m_ScriptInterface(scriptInterface), m_Obj(scriptInterface->GetGeneralJSContext()) { } bool Initialise() { // LoadScripts will only load each script once even though we call it for each player if (!m_Worker.LoadScripts(m_AIName)) return false; ScriptRequest rq(m_ScriptInterface); OsPath path = L"simulation/ai/" + m_AIName + L"/data.json"; JS::RootedValue metadata(rq.cx); m_Worker.LoadMetadata(path, &metadata); if (metadata.isUndefined()) { LOGERROR("Failed to create AI player: can't find %s", path.string8()); return false; } // Get the constructor name from the metadata std::string moduleName; std::string constructor; JS::RootedValue objectWithConstructor(rq.cx); // object that should contain the constructor function JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue ctor(rq.cx); if (!m_ScriptInterface->HasProperty(metadata, "moduleName")) { LOGERROR("Failed to create AI player: %s: missing 'moduleName'", path.string8()); return false; } m_ScriptInterface->GetProperty(metadata, "moduleName", moduleName); if (!m_ScriptInterface->GetProperty(global, moduleName.c_str(), &objectWithConstructor) || objectWithConstructor.isUndefined()) { LOGERROR("Failed to create AI player: %s: can't find the module that should contain the constructor: '%s'", path.string8(), moduleName); return false; } if (!m_ScriptInterface->GetProperty(metadata, "constructor", constructor)) { LOGERROR("Failed to create AI player: %s: missing 'constructor'", path.string8()); return false; } // Get the constructor function from the loaded scripts if (!m_ScriptInterface->GetProperty(objectWithConstructor, constructor.c_str(), &ctor) || ctor.isNull()) { LOGERROR("Failed to create AI player: %s: can't find constructor '%s'", path.string8(), constructor); return false; } m_ScriptInterface->GetProperty(metadata, "useShared", m_UseSharedComponent); // Set up the data to pass as the constructor argument JS::RootedValue settings(rq.cx); ScriptInterface::CreateObject( rq, &settings, "player", m_Player, "difficulty", m_Difficulty, "behavior", m_Behavior); if (!m_UseSharedComponent) { ENSURE(m_Worker.m_HasLoadedEntityTemplates); m_ScriptInterface->SetProperty(settings, "templates", m_Worker.m_EntityTemplates, false); } JS::RootedValueVector argv(rq.cx); ignore_result(argv.append(settings.get())); m_ScriptInterface->CallConstructor(ctor, argv, &m_Obj); if (m_Obj.get().isNull()) { LOGERROR("Failed to create AI player: %s: error calling constructor '%s'", path.string8(), constructor); return false; } return true; } void Run(JS::HandleValue state, int playerID) { m_Commands.clear(); - m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID); + ScriptRequest rq(m_ScriptInterface); + ScriptFunction::CallVoid(rq, m_Obj, "HandleMessage", state, playerID); } // overloaded with a sharedAI part. // javascript can handle both natively on the same function. void Run(JS::HandleValue state, int playerID, JS::HandleValue SharedAI) { m_Commands.clear(); - m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID, SharedAI); + ScriptRequest rq(m_ScriptInterface); + ScriptFunction::CallVoid(rq, m_Obj, "HandleMessage", state, playerID, SharedAI); } void InitAI(JS::HandleValue state, JS::HandleValue SharedAI) { m_Commands.clear(); - m_ScriptInterface->CallFunctionVoid(m_Obj, "Init", state, m_Player, SharedAI); + ScriptRequest rq(m_ScriptInterface); + ScriptFunction::CallVoid(rq, m_Obj, "Init", state, m_Player, SharedAI); } CAIWorker& m_Worker; std::wstring m_AIName; player_id_t m_Player; u8 m_Difficulty; std::wstring m_Behavior; bool m_UseSharedComponent; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. shared_ptr m_ScriptInterface; JS::PersistentRootedValue m_Obj; std::vector m_Commands; }; public: struct SCommandSets { player_id_t player; std::vector commands; }; CAIWorker() : m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptContext)), m_TurnNum(0), m_CommandsComputed(true), m_HasLoadedEntityTemplates(false), m_HasSharedComponent(false), m_EntityTemplates(g_ScriptContext->GetGeneralJSContext()), m_SharedAIObj(g_ScriptContext->GetGeneralJSContext()), m_PassabilityMapVal(g_ScriptContext->GetGeneralJSContext()), m_TerritoryMapVal(g_ScriptContext->GetGeneralJSContext()) { m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG); m_ScriptInterface->SetCallbackData(static_cast (this)); JS_AddExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); ScriptRequest rq(m_ScriptInterface); #define REGISTER_FUNC_NAME(func, name) \ ScriptFunction::Register<&CAIWorker::func, ScriptFunction::ObjectFromCBData>(rq, name); REGISTER_FUNC_NAME(PostCommand, "PostCommand"); REGISTER_FUNC_NAME(LoadScripts, "IncludeModule"); ScriptFunction::Register(rq, "Exit"); REGISTER_FUNC_NAME(ComputePathScript, "ComputePath"); REGISTER_FUNC_NAME(DumpImage, "DumpImage"); REGISTER_FUNC_NAME(GetTemplate, "GetTemplate"); #undef REGISTER_FUNC_NAME JSI_VFS::RegisterScriptFunctions_Simulation(rq); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); } ~CAIWorker() { JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); } bool HasLoadedEntityTemplates() const { return m_HasLoadedEntityTemplates; } bool LoadScripts(const std::wstring& moduleName) { // Ignore modules that are already loaded if (m_LoadedModules.find(moduleName) != m_LoadedModules.end()) return true; // Mark this as loaded, to prevent it recursively loading itself m_LoadedModules.insert(moduleName); // Load and execute *.js VfsPaths pathnames; if (vfs::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames) < 0) { LOGERROR("Failed to load AI scripts for module %s", utf8_from_wstring(moduleName)); return false; } for (const VfsPath& path : pathnames) { if (!m_ScriptInterface->LoadGlobalScriptFile(path)) { LOGERROR("Failed to load script %s", path.string8()); return false; } } return true; } void PostCommand(int playerid, JS::HandleValue cmd) { for (size_t i=0; im_Player == playerid) { m_Players[i]->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(cmd)); return; } } LOGERROR("Invalid playerid in PostCommand!"); } JS::Value ComputePathScript(JS::HandleValue position, JS::HandleValue goal, pass_class_t passClass) { ScriptRequest rq(m_ScriptInterface); CFixedVector2D pos, goalPos; std::vector waypoints; JS::RootedValue retVal(rq.cx); m_ScriptInterface->FromJSVal(rq, position, pos); m_ScriptInterface->FromJSVal(rq, goal, goalPos); ComputePath(pos, goalPos, passClass, waypoints); m_ScriptInterface->ToJSVal >(rq, &retVal, waypoints); return retVal; } void ComputePath(const CFixedVector2D& pos, const CFixedVector2D& goal, pass_class_t passClass, std::vector& waypoints) { WaypointPath ret; PathGoal pathGoal = { PathGoal::POINT, goal.X, goal.Y }; m_LongPathfinder.ComputePath(m_HierarchicalPathfinder, pos.X, pos.Y, pathGoal, passClass, ret); for (Waypoint& wp : ret.m_Waypoints) waypoints.emplace_back(wp.x, wp.z); } CParamNode GetTemplate(const std::string& name) { if (!m_TemplateLoader.TemplateExists(name)) return CParamNode(false); return m_TemplateLoader.GetTemplateFileData(name).GetChild("Entity"); } /** * Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights). */ void DumpImage(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& name, const std::vector& data, u32 w, u32 h, u32 max) { // TODO: this is totally not threadsafe. VfsPath filename = L"screenshots/aidump/" + name; if (data.size() != w*h) { debug_warn(L"DumpImage: data size doesn't match w*h"); return; } if (max == 0) { debug_warn(L"DumpImage: max must not be 0"); return; } const size_t bpp = 8; int flags = TEX_BOTTOM_UP|TEX_GREY; const size_t img_size = w * h * bpp/8; const size_t hdr_size = tex_hdr_size(filename); shared_ptr buf; AllocateAligned(buf, hdr_size+img_size, maxSectorSize); Tex t; if (t.wrap(w, h, bpp, flags, buf, hdr_size) < 0) return; u8* img = buf.get() + hdr_size; for (size_t i = 0; i < data.size(); ++i) img[i] = (u8)((data[i] * 255) / max); tex_write(&t, filename); } void SetRNGSeed(u32 seed) { m_RNG.seed(seed); } bool TryLoadSharedComponent() { ScriptRequest rq(m_ScriptInterface); // we don't need to load it. if (!m_HasSharedComponent) return false; // reset the value so it can be used to determine if we actually initialized it. m_HasSharedComponent = false; if (LoadScripts(L"common-api")) m_HasSharedComponent = true; else return false; // mainly here for the error messages OsPath path = L"simulation/ai/common-api/"; // Constructor name is SharedScript, it's in the module API3 // TODO: Hardcoding this is bad, we need a smarter way. JS::RootedValue AIModule(rq.cx); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue ctor(rq.cx); if (!m_ScriptInterface->GetProperty(global, "API3", &AIModule) || AIModule.isUndefined()) { LOGERROR("Failed to create shared AI component: %s: can't find module '%s'", path.string8(), "API3"); return false; } if (!m_ScriptInterface->GetProperty(AIModule, "SharedScript", &ctor) || ctor.isUndefined()) { LOGERROR("Failed to create shared AI component: %s: can't find constructor '%s'", path.string8(), "SharedScript"); return false; } // Set up the data to pass as the constructor argument JS::RootedValue playersID(rq.cx); ScriptInterface::CreateObject(rq, &playersID); for (size_t i = 0; i < m_Players.size(); ++i) { JS::RootedValue val(rq.cx); m_ScriptInterface->ToJSVal(rq, &val, m_Players[i]->m_Player); m_ScriptInterface->SetPropertyInt(playersID, i, val, true); } ENSURE(m_HasLoadedEntityTemplates); JS::RootedValue settings(rq.cx); ScriptInterface::CreateObject( rq, &settings, "players", playersID, "templates", m_EntityTemplates); JS::RootedValueVector argv(rq.cx); ignore_result(argv.append(settings)); m_ScriptInterface->CallConstructor(ctor, argv, &m_SharedAIObj); if (m_SharedAIObj.get().isNull()) { LOGERROR("Failed to create shared AI component: %s: error calling constructor '%s'", path.string8(), "SharedScript"); return false; } return true; } bool AddPlayer(const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior) { shared_ptr ai = std::make_shared(*this, aiName, player, difficulty, behavior, m_ScriptInterface); if (!ai->Initialise()) return false; // this will be set to true if we need to load the shared Component. if (!m_HasSharedComponent) m_HasSharedComponent = ai->m_UseSharedComponent; m_Players.push_back(ai); return true; } bool RunGamestateInit(const ScriptInterface::StructuredClone& gameState, const Grid& passabilityMap, const Grid& territoryMap, const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks) { // this will be run last by InitGame.js, passing the full game representation. // For now it will run for the shared Component. // This is NOT run during deserialization. ScriptRequest rq(m_ScriptInterface); JS::RootedValue state(rq.cx); m_ScriptInterface->ReadStructuredClone(gameState, &state); ScriptInterface::ToJSVal(rq, &m_PassabilityMapVal, passabilityMap); ScriptInterface::ToJSVal(rq, &m_TerritoryMapVal, territoryMap); m_PassabilityMap = passabilityMap; m_NonPathfindingPassClasses = nonPathfindingPassClassMasks; m_PathfindingPassClasses = pathfindingPassClassMasks; m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); if (m_HasSharedComponent) { m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true); m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true); - m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "init", state); + ScriptFunction::CallVoid(rq, m_SharedAIObj, "init", state); for (size_t i = 0; i < m_Players.size(); ++i) { if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->InitAI(state, m_SharedAIObj); } } return true; } void UpdateGameState(const ScriptInterface::StructuredClone& gameState) { ENSURE(m_CommandsComputed); m_GameState = gameState; } void UpdatePathfinder(const Grid& passabilityMap, bool globallyDirty, const Grid& dirtinessGrid, bool justDeserialized, const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks) { ENSURE(m_CommandsComputed); bool dimensionChange = m_PassabilityMap.m_W != passabilityMap.m_W || m_PassabilityMap.m_H != passabilityMap.m_H; m_PassabilityMap = passabilityMap; if (globallyDirty) { m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } else { m_LongPathfinder.Update(&m_PassabilityMap); m_HierarchicalPathfinder.Update(&m_PassabilityMap, dirtinessGrid); } ScriptRequest rq(m_ScriptInterface); if (dimensionChange || justDeserialized) ScriptInterface::ToJSVal(rq, &m_PassabilityMapVal, m_PassabilityMap); else { // Avoid a useless memory reallocation followed by a garbage collection. JS::RootedObject mapObj(rq.cx, &m_PassabilityMapVal.toObject()); JS::RootedValue mapData(rq.cx); ENSURE(JS_GetProperty(rq.cx, mapObj, "data", &mapData)); JS::RootedObject dataObj(rq.cx, &mapData.toObject()); u32 length = 0; ENSURE(JS::GetArrayLength(rq.cx, dataObj, &length)); u32 nbytes = (u32)(length * sizeof(NavcellData)); bool sharedMemory; JS::AutoCheckCannotGC nogc; memcpy((void*)JS_GetUint16ArrayData(dataObj, &sharedMemory, nogc), m_PassabilityMap.m_Data, nbytes); } } void UpdateTerritoryMap(const Grid& territoryMap) { ENSURE(m_CommandsComputed); bool dimensionChange = m_TerritoryMap.m_W != territoryMap.m_W || m_TerritoryMap.m_H != territoryMap.m_H; m_TerritoryMap = territoryMap; ScriptRequest rq(m_ScriptInterface); if (dimensionChange) ScriptInterface::ToJSVal(rq, &m_TerritoryMapVal, m_TerritoryMap); else { // Avoid a useless memory reallocation followed by a garbage collection. JS::RootedObject mapObj(rq.cx, &m_TerritoryMapVal.toObject()); JS::RootedValue mapData(rq.cx); ENSURE(JS_GetProperty(rq.cx, mapObj, "data", &mapData)); JS::RootedObject dataObj(rq.cx, &mapData.toObject()); u32 length = 0; ENSURE(JS::GetArrayLength(rq.cx, dataObj, &length)); u32 nbytes = (u32)(length * sizeof(u8)); bool sharedMemory; JS::AutoCheckCannotGC nogc; memcpy((void*)JS_GetUint8ArrayData(dataObj, &sharedMemory, nogc), m_TerritoryMap.m_Data, nbytes); } } void StartComputation() { m_CommandsComputed = false; } void WaitToFinishComputation() { if (!m_CommandsComputed) { PerformComputation(); m_CommandsComputed = true; } } void GetCommands(std::vector& commands) { WaitToFinishComputation(); commands.clear(); commands.resize(m_Players.size()); for (size_t i = 0; i < m_Players.size(); ++i) { commands[i].player = m_Players[i]->m_Player; commands[i].commands = m_Players[i]->m_Commands; } } void LoadEntityTemplates(const std::vector >& templates) { ScriptRequest rq(m_ScriptInterface); m_HasLoadedEntityTemplates = true; ScriptInterface::CreateObject(rq, &m_EntityTemplates); JS::RootedValue val(rq.cx); for (size_t i = 0; i < templates.size(); ++i) { templates[i].second->ToJSVal(rq, false, &val); m_ScriptInterface->SetProperty(m_EntityTemplates, templates[i].first.c_str(), val, true); } } void Serialize(std::ostream& stream, bool isDebug) { WaitToFinishComputation(); if (isDebug) { CDebugSerializer serializer(*m_ScriptInterface, stream); serializer.Indent(4); SerializeState(serializer); } else { CStdSerializer serializer(*m_ScriptInterface, stream); SerializeState(serializer); } } void SerializeState(ISerializer& serializer) { if (m_Players.empty()) return; ScriptRequest rq(m_ScriptInterface); std::stringstream rngStream; rngStream << m_RNG; serializer.StringASCII("rng", rngStream.str(), 0, 32); serializer.NumberU32_Unbounded("turn", m_TurnNum); serializer.Bool("useSharedScript", m_HasSharedComponent); if (m_HasSharedComponent) serializer.ScriptVal("sharedData", &m_SharedAIObj); for (size_t i = 0; i < m_Players.size(); ++i) { serializer.String("name", m_Players[i]->m_AIName, 1, 256); serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player); serializer.NumberU8_Unbounded("difficulty", m_Players[i]->m_Difficulty); serializer.String("behavior", m_Players[i]->m_Behavior, 1, 256); serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size()); for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j) { JS::RootedValue val(rq.cx); m_ScriptInterface->ReadStructuredClone(m_Players[i]->m_Commands[j], &val); serializer.ScriptVal("command", &val); } serializer.ScriptVal("data", &m_Players[i]->m_Obj); } // AI pathfinder Serializer(serializer, "non pathfinding pass classes", m_NonPathfindingPassClasses); Serializer(serializer, "pathfinding pass classes", m_PathfindingPassClasses); serializer.NumberU16_Unbounded("pathfinder grid w", m_PassabilityMap.m_W); serializer.NumberU16_Unbounded("pathfinder grid h", m_PassabilityMap.m_H); serializer.RawBytes("pathfinder grid data", (const u8*)m_PassabilityMap.m_Data, m_PassabilityMap.m_W*m_PassabilityMap.m_H*sizeof(NavcellData)); } void Deserialize(std::istream& stream, u32 numAis) { m_PlayerMetadata.clear(); m_Players.clear(); if (numAis == 0) return; ScriptRequest rq(m_ScriptInterface); ENSURE(m_CommandsComputed); // deserializing while we're still actively computing would be bad CStdDeserializer deserializer(*m_ScriptInterface, stream); std::string rngString; std::stringstream rngStream; deserializer.StringASCII("rng", rngString, 0, 32); rngStream << rngString; rngStream >> m_RNG; deserializer.NumberU32_Unbounded("turn", m_TurnNum); deserializer.Bool("useSharedScript", m_HasSharedComponent); if (m_HasSharedComponent) { TryLoadSharedComponent(); deserializer.ScriptObjectAssign("sharedData", m_SharedAIObj); } for (size_t i = 0; i < numAis; ++i) { std::wstring name; player_id_t player; u8 difficulty; std::wstring behavior; deserializer.String("name", name, 1, 256); deserializer.NumberI32_Unbounded("player", player); deserializer.NumberU8_Unbounded("difficulty",difficulty); deserializer.String("behavior", behavior, 1, 256); if (!AddPlayer(name, player, difficulty, behavior)) throw PSERROR_Deserialize_ScriptError(); u32 numCommands; deserializer.NumberU32_Unbounded("num commands", numCommands); m_Players.back()->m_Commands.reserve(numCommands); for (size_t j = 0; j < numCommands; ++j) { JS::RootedValue val(rq.cx); deserializer.ScriptVal("command", &val); m_Players.back()->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(val)); } deserializer.ScriptObjectAssign("data", m_Players.back()->m_Obj); } // AI pathfinder Serializer(deserializer, "non pathfinding pass classes", m_NonPathfindingPassClasses); Serializer(deserializer, "pathfinding pass classes", m_PathfindingPassClasses); u16 mapW, mapH; deserializer.NumberU16_Unbounded("pathfinder grid w", mapW); deserializer.NumberU16_Unbounded("pathfinder grid h", mapH); m_PassabilityMap = Grid(mapW, mapH); deserializer.RawBytes("pathfinder grid data", (u8*)m_PassabilityMap.m_Data, mapW*mapH*sizeof(NavcellData)); m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, m_NonPathfindingPassClasses, m_PathfindingPassClasses); } int getPlayerSize() { return m_Players.size(); } private: static void Trace(JSTracer *trc, void *data) { reinterpret_cast(data)->TraceMember(trc); } void TraceMember(JSTracer *trc) { for (std::pair>& metadata : m_PlayerMetadata) JS::TraceEdge(trc, &metadata.second, "CAIWorker::m_PlayerMetadata"); } void LoadMetadata(const VfsPath& path, JS::MutableHandleValue out) { if (m_PlayerMetadata.find(path) == m_PlayerMetadata.end()) { // Load and cache the AI player metadata m_ScriptInterface->ReadJSONFile(path, out); m_PlayerMetadata[path] = JS::Heap(out); return; } out.set(m_PlayerMetadata[path].get()); } void PerformComputation() { // Deserialize the game state, to pass to the AI's HandleMessage ScriptRequest rq(m_ScriptInterface); JS::RootedValue state(rq.cx); { PROFILE3("AI compute read state"); m_ScriptInterface->ReadStructuredClone(m_GameState, &state); m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true); m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true); } // It would be nice to do // m_ScriptInterface->FreezeObject(state.get(), true); // to prevent AI scripts accidentally modifying the state and // affecting other AI scripts they share it with. But the performance // cost is far too high, so we won't do that. // If there is a shared component, run it if (m_HasSharedComponent) { PROFILE3("AI run shared component"); - m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "onUpdate", state); + ScriptFunction::CallVoid(rq, m_SharedAIObj, "onUpdate", state); } for (size_t i = 0; i < m_Players.size(); ++i) { PROFILE3("AI script"); PROFILE2_ATTR("player: %d", m_Players[i]->m_Player); PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str()); if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->Run(state, m_Players[i]->m_Player, m_SharedAIObj); else m_Players[i]->Run(state, m_Players[i]->m_Player); } } // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. shared_ptr m_ScriptContext; shared_ptr m_ScriptInterface; boost::rand48 m_RNG; u32 m_TurnNum; JS::PersistentRootedValue m_EntityTemplates; bool m_HasLoadedEntityTemplates; std::map > m_PlayerMetadata; std::vector > m_Players; // use shared_ptr just to avoid copying bool m_HasSharedComponent; JS::PersistentRootedValue m_SharedAIObj; std::vector m_Commands; std::set m_LoadedModules; ScriptInterface::StructuredClone m_GameState; Grid m_PassabilityMap; JS::PersistentRootedValue m_PassabilityMapVal; Grid m_TerritoryMap; JS::PersistentRootedValue m_TerritoryMapVal; std::map m_NonPathfindingPassClasses; std::map m_PathfindingPassClasses; HierarchicalPathfinder m_HierarchicalPathfinder; LongPathfinder m_LongPathfinder; bool m_CommandsComputed; CTemplateLoader m_TemplateLoader; }; /** * Implementation of ICmpAIManager. */ class CCmpAIManager : public ICmpAIManager { public: static void ClassInit(CComponentManager& UNUSED(componentManager)) { } DEFAULT_COMPONENT_ALLOCATOR(AIManager) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_TerritoriesDirtyID = 0; m_TerritoriesDirtyBlinkingID = 0; m_JustDeserialized = false; } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { serialize.NumberU32_Unbounded("num ais", m_Worker.getPlayerSize()); // Because the AI worker uses its own ScriptInterface, we can't use the // ISerializer (which was initialised with the simulation ScriptInterface) // directly. So we'll just grab the ISerializer's stream and write to it // with an independent serializer. m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug()); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); u32 numAis; deserialize.NumberU32_Unbounded("num ais", numAis); if (numAis > 0) LoadUsedEntityTemplates(); m_Worker.Deserialize(deserialize.GetStream(), numAis); m_JustDeserialized = true; } virtual void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty, const std::wstring& behavior) { LoadUsedEntityTemplates(); m_Worker.AddPlayer(id, player, difficulty, behavior); // AI players can cheat and see through FoW/SoD, since that greatly simplifies // their implementation. // (TODO: maybe cleverer AIs should be able to optionally retain FoW/SoD) CmpPtr cmpRangeManager(GetSystemEntity()); if (cmpRangeManager) cmpRangeManager->SetLosRevealAll(player, true); } virtual void SetRNGSeed(u32 seed) { m_Worker.SetRNGSeed(seed); } virtual void TryLoadSharedComponent() { m_Worker.TryLoadSharedComponent(); } virtual void RunGamestateInit() { const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface // We flush events from the initialization so we get a clean state now. JS::RootedValue state(rq.cx); cmpAIInterface->GetFullRepresentation(&state, true); // Get the passability data Grid dummyGrid; const Grid* passabilityMap = &dummyGrid; CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) passabilityMap = &cmpPathfinder->GetPassabilityGrid(); // Get the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdateAI first Grid dummyGrid2; const Grid* territoryMap = &dummyGrid2; CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdateAI(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID)) territoryMap = &cmpTerritoryManager->GetTerritoryGrid(); LoadPathfinderClasses(state); std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks; if (cmpPathfinder) cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks); m_Worker.RunGamestateInit(scriptInterface.WriteStructuredClone(state), *passabilityMap, *territoryMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } virtual void StartComputation() { PROFILE("AI setup"); const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); if (m_Worker.getPlayerSize() == 0) return; CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface JS::RootedValue state(rq.cx); if (m_JustDeserialized) cmpAIInterface->GetFullRepresentation(&state, false); else cmpAIInterface->GetRepresentation(&state); LoadPathfinderClasses(state); // add the pathfinding classes to it // Update the game state m_Worker.UpdateGameState(scriptInterface.WriteStructuredClone(state)); // Update the pathfinding data CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) { const GridUpdateInformation& dirtinessInformations = cmpPathfinder->GetAIPathfinderDirtinessInformation(); if (dirtinessInformations.dirty || m_JustDeserialized) { const Grid& passabilityMap = cmpPathfinder->GetPassabilityGrid(); std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks; cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks); m_Worker.UpdatePathfinder(passabilityMap, dirtinessInformations.globallyDirty, dirtinessInformations.dirtinessGrid, m_JustDeserialized, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } cmpPathfinder->FlushAIPathfinderDirtinessInformation(); } // Update the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdateAI first CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && (cmpTerritoryManager->NeedUpdateAI(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID) || m_JustDeserialized)) { const Grid& territoryMap = cmpTerritoryManager->GetTerritoryGrid(); m_Worker.UpdateTerritoryMap(territoryMap); } m_Worker.StartComputation(); m_JustDeserialized = false; } virtual void PushCommands() { std::vector commands; m_Worker.GetCommands(commands); CmpPtr cmpCommandQueue(GetSystemEntity()); if (!cmpCommandQueue) return; const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue clonedCommandVal(rq.cx); for (size_t i = 0; i < commands.size(); ++i) { for (size_t j = 0; j < commands[i].commands.size(); ++j) { scriptInterface.ReadStructuredClone(commands[i].commands[j], &clonedCommandVal); cmpCommandQueue->PushLocalCommand(commands[i].player, clonedCommandVal); } } } private: size_t m_TerritoriesDirtyID; size_t m_TerritoriesDirtyBlinkingID; bool m_JustDeserialized; /** * Load the templates of all entities on the map (called when adding a new AI player for a new game * or when deserializing) */ void LoadUsedEntityTemplates() { if (m_Worker.HasLoadedEntityTemplates()) return; CmpPtr cmpTemplateManager(GetSystemEntity()); ENSURE(cmpTemplateManager); std::vector templateNames = cmpTemplateManager->FindUsedTemplates(); std::vector > usedTemplates; usedTemplates.reserve(templateNames.size()); for (const std::string& name : templateNames) { const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(name); if (node) usedTemplates.emplace_back(name, node); } // Send the data to the worker m_Worker.LoadEntityTemplates(usedTemplates); } void LoadPathfinderClasses(JS::HandleValue state) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue classesVal(rq.cx); ScriptInterface::CreateObject(rq, &classesVal); std::map classes; cmpPathfinder->GetPassabilityClasses(classes); for (std::map::iterator it = classes.begin(); it != classes.end(); ++it) scriptInterface.SetProperty(classesVal, it->first.c_str(), it->second, true); scriptInterface.SetProperty(state, "passabilityClasses", classesVal, true); } CAIWorker m_Worker; }; REGISTER_COMPONENT_TYPE(AIManager) Index: ps/trunk/source/simulation2/components/CCmpCommandQueue.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpCommandQueue.cpp (revision 25353) +++ ps/trunk/source/simulation2/components/CCmpCommandQueue.cpp (revision 25354) @@ -1,126 +1,127 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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 "simulation2/system/Component.h" #include "ICmpCommandQueue.h" #include "ps/CLogger.h" #include "ps/Game.h" #include "ps/Profile.h" +#include "scriptinterface/FunctionWrapper.h" #include "simulation2/system/TurnManager.h" class CCmpCommandQueue : public ICmpCommandQueue { public: static void ClassInit(CComponentManager& UNUSED(componentManager)) { } DEFAULT_COMPONENT_ALLOCATOR(CommandQueue) std::vector m_LocalQueue; static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { ScriptRequest rq(GetSimContext().GetScriptInterface()); serialize.NumberU32_Unbounded("num commands", (u32)m_LocalQueue.size()); for (size_t i = 0; i < m_LocalQueue.size(); ++i) { serialize.NumberI32_Unbounded("player", m_LocalQueue[i].player); serialize.ScriptVal("data", &m_LocalQueue[i].data); } } virtual void Deserialize(const CParamNode& UNUSED(paramNode), IDeserializer& deserialize) { ScriptRequest rq(GetSimContext().GetScriptInterface()); u32 numCmds; deserialize.NumberU32_Unbounded("num commands", numCmds); for (size_t i = 0; i < numCmds; ++i) { i32 player; JS::RootedValue data(rq.cx); deserialize.NumberI32_Unbounded("player", player); deserialize.ScriptVal("data", &data); m_LocalQueue.emplace_back(SimulationCommand(player, rq.cx, data)); } } virtual void PushLocalCommand(player_id_t player, JS::HandleValue cmd) { ScriptRequest rq(GetSimContext().GetScriptInterface()); m_LocalQueue.emplace_back(SimulationCommand(player, rq.cx, cmd)); } virtual void PostNetworkCommand(JS::HandleValue cmd1) { ScriptRequest rq(GetSimContext().GetScriptInterface()); // TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON. JS::RootedValue cmd(rq.cx, cmd1.get()); PROFILE2_EVENT("post net command"); PROFILE2_ATTR("command: %s", GetSimContext().GetScriptInterface().StringifyJSON(&cmd, false).c_str()); // TODO: would be nicer to not use globals if (g_Game && g_Game->GetTurnManager()) g_Game->GetTurnManager()->PostCommand(cmd); } virtual void FlushTurn(const std::vector& commands) { const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); std::vector localCommands; m_LocalQueue.swap(localCommands); for (size_t i = 0; i < localCommands.size(); ++i) { - bool ok = scriptInterface.CallFunctionVoid(global, "ProcessCommand", localCommands[i].player, localCommands[i].data); + bool ok = ScriptFunction::CallVoid(rq, global, "ProcessCommand", localCommands[i].player, localCommands[i].data); if (!ok) LOGERROR("Failed to call ProcessCommand() global script function"); } for (size_t i = 0; i < commands.size(); ++i) { - bool ok = scriptInterface.CallFunctionVoid(global, "ProcessCommand", commands[i].player, commands[i].data); + bool ok = ScriptFunction::CallVoid(rq, global, "ProcessCommand", commands[i].player, commands[i].data); if (!ok) LOGERROR("Failed to call ProcessCommand() global script function"); } } }; REGISTER_COMPONENT_TYPE(CommandQueue) Index: ps/trunk/source/simulation2/scripting/ScriptComponent.cpp =================================================================== --- ps/trunk/source/simulation2/scripting/ScriptComponent.cpp (revision 25353) +++ ps/trunk/source/simulation2/scripting/ScriptComponent.cpp (revision 25354) @@ -1,69 +1,72 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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 "ScriptComponent.h" +#include "scriptinterface/FunctionWrapper.h" #include "simulation2/serialization/ISerializer.h" #include "simulation2/serialization/IDeserializer.h" CComponentTypeScript::CComponentTypeScript(const ScriptInterface& scriptInterface, JS::HandleValue instance) : m_ScriptInterface(scriptInterface), m_Instance(scriptInterface.GetGeneralJSContext(), instance) { } void CComponentTypeScript::Init(const CParamNode& paramNode, entity_id_t ent) { + ScriptRequest rq(m_ScriptInterface); m_ScriptInterface.SetProperty(m_Instance, "entity", (int)ent, true, false); m_ScriptInterface.SetProperty(m_Instance, "template", paramNode, true, false); - m_ScriptInterface.CallFunctionVoid(m_Instance, "Init"); + ScriptFunction::CallVoid(rq, m_Instance, "Init"); } void CComponentTypeScript::Deinit() { - m_ScriptInterface.CallFunctionVoid(m_Instance, "Deinit"); + ScriptRequest rq(m_ScriptInterface); + ScriptFunction::CallVoid(rq, m_Instance, "Deinit"); } void CComponentTypeScript::HandleMessage(const CMessage& msg, bool global) { ScriptRequest rq(m_ScriptInterface); const char* name = global ? msg.GetScriptGlobalHandlerName() : msg.GetScriptHandlerName(); JS::RootedValue msgVal(rq.cx, msg.ToJSValCached(m_ScriptInterface)); - if (!m_ScriptInterface.CallFunctionVoid(m_Instance, name, msgVal)) + if (!ScriptFunction::CallVoid(rq, m_Instance, name, msgVal)) LOGERROR("Script message handler %s failed", name); } void CComponentTypeScript::Serialize(ISerializer& serialize) { ScriptRequest rq(m_ScriptInterface); serialize.ScriptVal("comp", &m_Instance); } void CComponentTypeScript::Deserialize(const CParamNode& paramNode, IDeserializer& deserialize, entity_id_t ent) { ScriptRequest rq(m_ScriptInterface); m_ScriptInterface.SetProperty(m_Instance, "entity", (int)ent, true, false); m_ScriptInterface.SetProperty(m_Instance, "template", paramNode, true, false); deserialize.ScriptObjectAssign("comp", m_Instance); } Index: ps/trunk/source/simulation2/scripting/ScriptComponent.h =================================================================== --- ps/trunk/source/simulation2/scripting/ScriptComponent.h (revision 25353) +++ ps/trunk/source/simulation2/scripting/ScriptComponent.h (revision 25354) @@ -1,125 +1,129 @@ /* Copyright (C) 2021 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_SCRIPTCOMPONENT #define INCLUDED_SCRIPTCOMPONENT +#include "scriptinterface/FunctionWrapper.h" #include "simulation2/system/Component.h" #include "ps/CLogger.h" class CComponentTypeScript { NONCOPYABLE(CComponentTypeScript); public: CComponentTypeScript(const ScriptInterface& scriptInterface, JS::HandleValue instance); JS::Value GetInstance() const { return m_Instance.get(); } void Init(const CParamNode& paramNode, entity_id_t ent); void Deinit(); void HandleMessage(const CMessage& msg, bool global); void Serialize(ISerializer& serialize); void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize, entity_id_t ent); template R Call(const char* funcname, const Ts&... params) const { R ret; - if (m_ScriptInterface.CallFunction(m_Instance, funcname, ret, params...)) + ScriptRequest rq(m_ScriptInterface); + if (ScriptFunction::Call(rq, m_Instance, funcname, ret, params...)) return ret; LOGERROR("Error calling component script function %s", funcname); return R(); } // CallRef is mainly used for returning script values with correct stack rooting. template void CallRef(const char* funcname, R ret, const Ts&... params) const { - if (!m_ScriptInterface.CallFunction(m_Instance, funcname, ret, params...)) + ScriptRequest rq(m_ScriptInterface); + if (!ScriptFunction::Call(rq, m_Instance, funcname, ret, params...)) LOGERROR("Error calling component script function %s", funcname); } template void CallVoid(const char* funcname, const Ts&... params) const { - if (!m_ScriptInterface.CallFunctionVoid(m_Instance, funcname, params...)) + ScriptRequest rq(m_ScriptInterface); + if (!ScriptFunction::CallVoid(rq, m_Instance, funcname, params...)) LOGERROR("Error calling component script function %s", funcname); } private: const ScriptInterface& m_ScriptInterface; JS::PersistentRootedValue m_Instance; }; #define REGISTER_COMPONENT_SCRIPT_WRAPPER(cname) \ void RegisterComponentType_##cname(CComponentManager& mgr) \ { \ mgr.RegisterComponentTypeScriptWrapper(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname, CCmp##cname::GetSchema()); \ CCmp##cname::ClassInit(mgr); \ } #define DEFAULT_SCRIPT_WRAPPER(cname) \ static void ClassInit(CComponentManager& UNUSED(componentManager)) { } \ static IComponent* Allocate(const ScriptInterface& scriptInterface, JS::HandleValue instance) \ { \ return new CCmp##cname(scriptInterface, instance); \ } \ static void Deallocate(IComponent* cmp) \ { \ delete static_cast (cmp); \ } \ CCmp##cname(const ScriptInterface& scriptInterface, JS::HandleValue instance) : m_Script(scriptInterface, instance) { } \ static std::string GetSchema() \ { \ return ""; \ } \ virtual void Init(const CParamNode& paramNode) \ { \ m_Script.Init(paramNode, GetEntityId()); \ } \ virtual void Deinit() \ { \ m_Script.Deinit(); \ } \ virtual void HandleMessage(const CMessage& msg, bool global) \ { \ m_Script.HandleMessage(msg, global); \ } \ virtual void Serialize(ISerializer& serialize) \ { \ m_Script.Serialize(serialize); \ } \ virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) \ { \ m_Script.Deserialize(paramNode, deserialize, GetEntityId()); \ } \ virtual JS::Value GetJSInstance() const \ { \ return m_Script.GetInstance(); \ } \ virtual int GetComponentTypeId() const \ { \ return CID_##cname; \ } \ private: \ CComponentTypeScript m_Script; \ public: #endif // INCLUDED_SCRIPTCOMPONENT Index: ps/trunk/source/simulation2/serialization/BinarySerializer.cpp =================================================================== --- ps/trunk/source/simulation2/serialization/BinarySerializer.cpp (revision 25353) +++ ps/trunk/source/simulation2/serialization/BinarySerializer.cpp (revision 25354) @@ -1,490 +1,491 @@ /* Copyright (C) 2021 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 "BinarySerializer.h" #include "lib/alignment.h" #include "ps/CLogger.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptExtraHeaders.h" #include "SerializedScriptTypes.h" static u8 GetArrayType(js::Scalar::Type arrayType) { switch(arrayType) { case js::Scalar::Int8: return SCRIPT_TYPED_ARRAY_INT8; case js::Scalar::Uint8: return SCRIPT_TYPED_ARRAY_UINT8; case js::Scalar::Int16: return SCRIPT_TYPED_ARRAY_INT16; case js::Scalar::Uint16: return SCRIPT_TYPED_ARRAY_UINT16; case js::Scalar::Int32: return SCRIPT_TYPED_ARRAY_INT32; case js::Scalar::Uint32: return SCRIPT_TYPED_ARRAY_UINT32; case js::Scalar::Float32: return SCRIPT_TYPED_ARRAY_FLOAT32; case js::Scalar::Float64: return SCRIPT_TYPED_ARRAY_FLOAT64; case js::Scalar::Uint8Clamped: return SCRIPT_TYPED_ARRAY_UINT8_CLAMPED; default: LOGERROR("Cannot serialize unrecognized typed array view: %d", arrayType); throw PSERROR_Serialize_InvalidScriptValue(); } } CBinarySerializerScriptImpl::CBinarySerializerScriptImpl(const ScriptInterface& scriptInterface, ISerializer& serializer) : m_ScriptInterface(scriptInterface), m_Serializer(serializer), m_ScriptBackrefsNext(0) { ScriptRequest rq(m_ScriptInterface); JS_AddExtraGCRootsTracer(m_ScriptInterface.GetGeneralJSContext(), Trace, this); } CBinarySerializerScriptImpl::~CBinarySerializerScriptImpl() { JS_RemoveExtraGCRootsTracer(m_ScriptInterface.GetGeneralJSContext(), Trace, this); } void CBinarySerializerScriptImpl::HandleScriptVal(JS::HandleValue val) { ScriptRequest rq(m_ScriptInterface); switch (JS_TypeOfValue(rq.cx, val)) { case JSTYPE_UNDEFINED: { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_VOID); break; } case JSTYPE_NULL: // This type is never actually returned (it's a JS2 feature) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_NULL); break; } case JSTYPE_OBJECT: { if (val.isNull()) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_NULL); break; } JS::RootedObject obj(rq.cx, &val.toObject()); // If we've already serialized this object, just output a reference to it u32 tag = GetScriptBackrefTag(obj); if (tag != 0) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_BACKREF); m_Serializer.NumberU32("tag", tag, 0, JSVAL_INT_MAX); break; } // Arrays, Maps and Sets are special cases of Objects bool isArray; bool isMap; bool isSet; if (JS::IsArrayObject(rq.cx, obj, &isArray) && isArray) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_ARRAY); // TODO: probably should have a more efficient storage format // Arrays like [1, 2, ] have an 'undefined' at the end which is part of the // length but seemingly isn't enumerated, so store the length explicitly uint length = 0; if (!JS::GetArrayLength(rq.cx, obj, &length)) throw PSERROR_Serialize_ScriptError("JS::GetArrayLength failed"); m_Serializer.NumberU32_Unbounded("array length", length); } else if (JS_IsTypedArrayObject(obj)) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_TYPED_ARRAY); m_Serializer.NumberU8_Unbounded("array type", GetArrayType(JS_GetArrayBufferViewType(obj))); m_Serializer.NumberU32_Unbounded("byte offset", JS_GetTypedArrayByteOffset(obj)); m_Serializer.NumberU32_Unbounded("length", JS_GetTypedArrayLength(obj)); bool sharedMemory; // Now handle its array buffer // this may be a backref, since ArrayBuffers can be shared by multiple views JS::RootedValue bufferVal(rq.cx, JS::ObjectValue(*JS_GetArrayBufferViewBuffer(rq.cx, obj, &sharedMemory))); HandleScriptVal(bufferVal); break; } else if (JS::IsArrayBufferObject(obj)) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_ARRAY_BUFFER); #if BYTE_ORDER != LITTLE_ENDIAN #error TODO: need to convert JS ArrayBuffer data to little-endian #endif u32 length = JS::GetArrayBufferByteLength(obj); m_Serializer.NumberU32_Unbounded("buffer length", length); JS::AutoCheckCannotGC nogc; bool sharedMemory; m_Serializer.RawBytes("buffer data", (const u8*)JS::GetArrayBufferData(obj, &sharedMemory, nogc), length); break; } else if (JS::IsMapObject(rq.cx, obj, &isMap) && isMap) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_MAP); m_Serializer.NumberU32_Unbounded("map size", JS::MapSize(rq.cx, obj)); JS::RootedValue keyValueIterator(rq.cx); if (!JS::MapEntries(rq.cx, obj, &keyValueIterator)) throw PSERROR_Serialize_ScriptError("JS::MapEntries failed"); JS::ForOfIterator it(rq.cx); if (!it.init(keyValueIterator)) throw PSERROR_Serialize_ScriptError("JS::ForOfIterator::init failed"); JS::RootedValue keyValuePair(rq.cx); bool done; while (true) { if (!it.next(&keyValuePair, &done)) throw PSERROR_Serialize_ScriptError("JS::ForOfIterator::next failed"); if (done) break; JS::RootedObject keyValuePairObj(rq.cx, &keyValuePair.toObject()); JS::RootedValue key(rq.cx); JS::RootedValue value(rq.cx); ENSURE(JS_GetElement(rq.cx, keyValuePairObj, 0, &key)); ENSURE(JS_GetElement(rq.cx, keyValuePairObj, 1, &value)); HandleScriptVal(key); HandleScriptVal(value); } break; } else if (JS::IsSetObject(rq.cx, obj, &isSet) && isSet) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_SET); m_Serializer.NumberU32_Unbounded("set size", JS::SetSize(rq.cx, obj)); JS::RootedValue valueIterator(rq.cx); if (!JS::SetValues(rq.cx, obj, &valueIterator)) throw PSERROR_Serialize_ScriptError("JS::SetValues failed"); JS::ForOfIterator it(rq.cx); if (!it.init(valueIterator)) throw PSERROR_Serialize_ScriptError("JS::ForOfIterator::init failed"); JS::RootedValue value(rq.cx); bool done; while (true) { if (!it.next(&value, &done)) throw PSERROR_Serialize_ScriptError("JS::ForOfIterator::next failed"); if (done) break; HandleScriptVal(value); } break; } else { // Find type of object const JSClass* jsclass = JS_GetClass(obj); if (!jsclass) throw PSERROR_Serialize_ScriptError("JS_GetClass failed"); JSProtoKey protokey = JSCLASS_CACHED_PROTO_KEY(jsclass); if (protokey == JSProto_Object) { // Object class - check for user-defined prototype JS::RootedObject proto(rq.cx); if (!JS_GetPrototype(rq.cx, obj, &proto)) throw PSERROR_Serialize_ScriptError("JS_GetPrototype failed"); SPrototypeSerialization protoInfo = GetPrototypeInfo(rq, proto); if (protoInfo.name == "Object") m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT); else { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_PROTOTYPE); m_Serializer.String("proto", wstring_from_utf8(protoInfo.name), 0, 256); // Does it have custom Serialize function? // if so, we serialize the data it returns, rather than the object's properties directly if (protoInfo.hasCustomSerialize) { // If serialize is null, don't serialize anything more if (!protoInfo.hasNullSerialize) { JS::RootedValue data(rq.cx); - if (!m_ScriptInterface.CallFunction(val, "Serialize", &data)) + if (!ScriptFunction::Call(rq, val, "Serialize", &data)) throw PSERROR_Serialize_ScriptError("Prototype Serialize function failed"); m_Serializer.ScriptVal("data", &data); } // Break here to skip the custom object property serialization logic below. break; } } } else if (protokey == JSProto_Number) { // Get primitive value double d; if (!JS::ToNumber(rq.cx, val, &d)) throw PSERROR_Serialize_ScriptError("JS::ToNumber failed"); // Refuse to serialize NaN values: their representation can differ, leading to OOS // and in general this is indicative of an underlying bug rather than desirable behaviour. if (std::isnan(d)) { LOGERROR("Cannot serialize NaN values."); throw PSERROR_Serialize_InvalidScriptValue(); } // Standard Number object m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_NUMBER); m_Serializer.NumberDouble_Unbounded("value", d); break; } else if (protokey == JSProto_String) { // Standard String object m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_STRING); // Get primitive value JS::RootedString str(rq.cx, JS::ToString(rq.cx, val)); if (!str) throw PSERROR_Serialize_ScriptError("JS_ValueToString failed"); ScriptString("value", str); break; } else if (protokey == JSProto_Boolean) { // Standard Boolean object m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_BOOLEAN); // Get primitive value bool b = JS::ToBoolean(val); m_Serializer.Bool("value", b); break; } else { // Unrecognized class LOGERROR("Cannot serialise JS objects with unrecognized class '%s'", jsclass->name); throw PSERROR_Serialize_InvalidScriptValue(); } } // Find all properties (ordered by insertion time) JS::Rooted ida(rq.cx, JS::IdVector(rq.cx)); if (!JS_Enumerate(rq.cx, obj, &ida)) throw PSERROR_Serialize_ScriptError("JS_Enumerate failed"); m_Serializer.NumberU32_Unbounded("num props", (u32)ida.length()); for (size_t i = 0; i < ida.length(); ++i) { JS::RootedId id(rq.cx, ida[i]); JS::RootedValue idval(rq.cx); JS::RootedValue propval(rq.cx); // Forbid getters, which might delete values and mess things up. JS::Rooted desc(rq.cx); if (!JS_GetPropertyDescriptorById(rq.cx, obj, id, &desc)) throw PSERROR_Serialize_ScriptError("JS_GetPropertyDescriptorById failed"); if (desc.hasGetterObject()) throw PSERROR_Serialize_ScriptError("Cannot serialize property getters"); // Get the property name as a string if (!JS_IdToValue(rq.cx, id, &idval)) throw PSERROR_Serialize_ScriptError("JS_IdToValue failed"); JS::RootedString idstr(rq.cx, JS::ToString(rq.cx, idval)); if (!idstr) throw PSERROR_Serialize_ScriptError("JS_ValueToString failed"); ScriptString("prop name", idstr); if (!JS_GetPropertyById(rq.cx, obj, id, &propval)) throw PSERROR_Serialize_ScriptError("JS_GetPropertyById failed"); HandleScriptVal(propval); } break; } case JSTYPE_FUNCTION: { // We can't serialise functions, but we can at least name the offender (hopefully) std::wstring funcname(L"(unnamed)"); JS::RootedFunction func(rq.cx, JS_ValueToFunction(rq.cx, val)); if (func) { JS::RootedString string(rq.cx, JS_GetFunctionId(func)); if (string) { if (JS_StringHasLatin1Chars(string)) { size_t length; JS::AutoCheckCannotGC nogc; const JS::Latin1Char* ch = JS_GetLatin1StringCharsAndLength(rq.cx, nogc, string, &length); if (ch && length > 0) funcname.assign(ch, ch + length); } else { size_t length; JS::AutoCheckCannotGC nogc; const char16_t* ch = JS_GetTwoByteStringCharsAndLength(rq.cx, nogc, string, &length); if (ch && length > 0) funcname.assign(ch, ch + length); } } } LOGERROR("Cannot serialise JS objects of type 'function': %s", utf8_from_wstring(funcname)); throw PSERROR_Serialize_InvalidScriptValue(); } case JSTYPE_STRING: { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_STRING); JS::RootedString stringVal(rq.cx, val.toString()); ScriptString("string", stringVal); break; } case JSTYPE_NUMBER: { // Refuse to serialize NaN values: their representation can differ, leading to OOS // and in general this is indicative of an underlying bug rather than desirable behaviour. if (val == JS::NaNValue()) { LOGERROR("Cannot serialize NaN values."); throw PSERROR_Serialize_InvalidScriptValue(); } // To reduce the size of the serialized data, we handle integers and doubles separately. // We can't check for val.isInt32 and val.isDouble directly, because integer numbers are not guaranteed // to be represented as integers. A number like 33 could be stored as integer on the computer of one player // and as double on the other player's computer. That would cause out of sync errors in multiplayer games because // their binary representation and thus the hash would be different. double d; d = val.toNumber(); i32 integer; if (JS_DoubleIsInt32(d, &integer)) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_INT); m_Serializer.NumberI32_Unbounded("value", integer); } else { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_DOUBLE); m_Serializer.NumberDouble_Unbounded("value", d); } break; } case JSTYPE_BOOLEAN: { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_BOOLEAN); bool b = val.toBoolean(); m_Serializer.NumberU8_Unbounded("value", b ? 1 : 0); break; } default: { debug_warn(L"Invalid TypeOfValue"); throw PSERROR_Serialize_InvalidScriptValue(); } } } void CBinarySerializerScriptImpl::ScriptString(const char* name, JS::HandleString string) { ScriptRequest rq(m_ScriptInterface); #if BYTE_ORDER != LITTLE_ENDIAN #error TODO: probably need to convert JS strings to little-endian #endif size_t length; JS::AutoCheckCannotGC nogc; // Serialize strings directly as UTF-16 or Latin1, to avoid expensive encoding conversions bool isLatin1 = JS_StringHasLatin1Chars(string); m_Serializer.Bool("isLatin1", isLatin1); if (isLatin1) { const JS::Latin1Char* chars = JS_GetLatin1StringCharsAndLength(rq.cx, nogc, string, &length); if (!chars) throw PSERROR_Serialize_ScriptError("JS_GetLatin1StringCharsAndLength failed"); m_Serializer.NumberU32_Unbounded("string length", (u32)length); m_Serializer.RawBytes(name, (const u8*)chars, length); } else { const char16_t* chars = JS_GetTwoByteStringCharsAndLength(rq.cx, nogc, string, &length); if (!chars) throw PSERROR_Serialize_ScriptError("JS_GetTwoByteStringCharsAndLength failed"); m_Serializer.NumberU32_Unbounded("string length", (u32)length); m_Serializer.RawBytes(name, (const u8*)chars, length*2); } } void CBinarySerializerScriptImpl::Trace(JSTracer *trc, void *data) { CBinarySerializerScriptImpl* serializer = static_cast(data); serializer->m_ScriptBackrefTags.trace(trc); } u32 CBinarySerializerScriptImpl::GetScriptBackrefTag(JS::HandleObject obj) { // To support non-tree structures (e.g. "var x = []; var y = [x, x];"), we need a way // to indicate multiple references to one object(/array). So every time we serialize a // new object, we give it a new tag; when we serialize it a second time we just refer // to that tag. ScriptRequest rq(m_ScriptInterface); ObjectTagMap::Ptr ptr = m_ScriptBackrefTags.lookup(JS::Heap(obj.get())); if (!ptr.found()) { if (!m_ScriptBackrefTags.put(JS::Heap(obj.get()), ++m_ScriptBackrefsNext)) { JS::RootedValue objval(rq.cx, JS::ObjectValue(*obj.get())); LOGERROR("BinarySerializer: error at insertion. Object was %s", m_ScriptInterface.ToString(&objval)); return 0; } // Return 0 to mean "you have to serialize this object"; return 0; } else return ptr->value(); } Index: ps/trunk/source/simulation2/serialization/DebugSerializer.cpp =================================================================== --- ps/trunk/source/simulation2/serialization/DebugSerializer.cpp (revision 25353) +++ ps/trunk/source/simulation2/serialization/DebugSerializer.cpp (revision 25354) @@ -1,192 +1,193 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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 "DebugSerializer.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "lib/secure_crt.h" #include "lib/utf8.h" #include "ps/CStr.h" #include #include /* * The output format here is intended to be compatible with YAML, * so it is human readable and usable in diff and can also be parsed with * external tools. */ // MSVC and GCC give slightly different serializations of floats // (e.g. "1e+010" vs "1e+10"). To make the debug serialization easily comparable // across platforms, we want to convert to a canonical form. // TODO: we just do e+0xx now; ought to handle varying precisions and inf and nan etc too template std::string canonfloat(T value, int prec) { std::stringstream str; str << std::setprecision(prec) << value; std::string r = str.str(); size_t e = r.find('e'); if (e == r.npos) // no "e" return r; if (e == r.length() - 5 && r[e + 2] == '0') // e.g. "1e+010" r.erase(e + 2, 1); return r; } CDebugSerializer::CDebugSerializer(const ScriptInterface& scriptInterface, std::ostream& stream, bool includeDebugInfo) : m_ScriptInterface(scriptInterface), m_Stream(stream), m_IsDebug(includeDebugInfo), m_Indent(0) { } void CDebugSerializer::Indent(int spaces) { m_Indent += spaces; } void CDebugSerializer::Dedent(int spaces) { ENSURE(spaces <= m_Indent); m_Indent -= spaces; } #define INDENT std::string(m_Indent, ' ') void CDebugSerializer::Comment(const std::string& comment) { m_Stream << INDENT << "# " << comment << "\n"; } void CDebugSerializer::TextLine(const std::string& text) { m_Stream << INDENT << text << "\n"; } void CDebugSerializer::PutNumber(const char* name, uint8_t value) { m_Stream << INDENT << name << ": " << (int)value << "\n"; } void CDebugSerializer::PutNumber(const char* name, int8_t value) { m_Stream << INDENT << name << ": " << (int)value << "\n"; } void CDebugSerializer::PutNumber(const char* name, uint16_t value) { m_Stream << INDENT << name << ": " << value << "\n"; } void CDebugSerializer::PutNumber(const char* name, int16_t value) { m_Stream << INDENT << name << ": " << value << "\n"; } void CDebugSerializer::PutNumber(const char* name, uint32_t value) { m_Stream << INDENT << name << ": " << value << "\n"; } void CDebugSerializer::PutNumber(const char* name, int32_t value) { m_Stream << INDENT << name << ": " << value << "\n"; } void CDebugSerializer::PutNumber(const char* name, float value) { m_Stream << INDENT << name << ": " << canonfloat(value, 8) << "\n"; } void CDebugSerializer::PutNumber(const char* name, double value) { m_Stream << INDENT << name << ": " << canonfloat(value, 17) << "\n"; } void CDebugSerializer::PutNumber(const char* name, fixed value) { m_Stream << INDENT << name << ": " << value.ToString() << "\n"; } void CDebugSerializer::PutBool(const char* name, bool value) { m_Stream << INDENT << name << ": " << (value ? "true" : "false") << "\n"; } void CDebugSerializer::PutString(const char* name, const std::string& value) { std::string escaped; escaped.reserve(value.size()); for (size_t i = 0; i < value.size(); ++i) if (value[i] == '"') escaped += "\\\""; else if (value[i] == '\\') escaped += "\\\\"; else if (value[i] == '\n') escaped += "\\n"; else escaped += value[i]; m_Stream << INDENT << name << ": " << "\"" << escaped << "\"\n"; } void CDebugSerializer::PutScriptVal(const char* name, JS::MutableHandleValue value) { ScriptRequest rq(m_ScriptInterface); JS::RootedValue serialize(rq.cx); if (m_ScriptInterface.GetProperty(value, "Serialize", &serialize) && !serialize.isNullOrUndefined()) { // If the value has a Serialize property, pretty-parse that instead. // (this gives more accurate OOS reports). - m_ScriptInterface.CallFunction(value, "Serialize", &serialize); + ScriptFunction::Call(rq, value, "Serialize", &serialize); std::string serialized_source = m_ScriptInterface.ToString(&serialize, true); m_Stream << INDENT << name << ": " << serialized_source << "\n"; } else { std::string source = m_ScriptInterface.ToString(value, true); m_Stream << INDENT << name << ": " << source << "\n"; } } void CDebugSerializer::PutRaw(const char* name, const u8* data, size_t len) { m_Stream << INDENT << name << ": (" << len << " bytes)"; char buf[4]; for (size_t i = 0; i < len; ++i) { sprintf_s(buf, ARRAY_SIZE(buf), " %02x", (unsigned int)data[i]); m_Stream << buf; } m_Stream << "\n"; } bool CDebugSerializer::IsDebug() const { return m_IsDebug; } std::ostream& CDebugSerializer::GetStream() { return m_Stream; } Index: ps/trunk/source/simulation2/serialization/StdDeserializer.cpp =================================================================== --- ps/trunk/source/simulation2/serialization/StdDeserializer.cpp (revision 25353) +++ ps/trunk/source/simulation2/serialization/StdDeserializer.cpp (revision 25354) @@ -1,493 +1,494 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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 "StdDeserializer.h" #include "lib/byte_order.h" #include "ps/CLogger.h" #include "ps/CStr.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptExtraHeaders.h" // For typed arrays and ArrayBuffer #include "simulation2/serialization/ISerializer.h" #include "simulation2/serialization/SerializedScriptTypes.h" #include "simulation2/serialization/StdSerializer.h" // for DEBUG_SERIALIZER_ANNOTATE CStdDeserializer::CStdDeserializer(const ScriptInterface& scriptInterface, std::istream& stream) : m_ScriptInterface(scriptInterface), m_Stream(stream) { JS_AddExtraGCRootsTracer(m_ScriptInterface.GetGeneralJSContext(), CStdDeserializer::Trace, this); // Insert a dummy object in front, as valid tags start at 1. m_ScriptBackrefs.emplace_back(nullptr); } CStdDeserializer::~CStdDeserializer() { JS_RemoveExtraGCRootsTracer(m_ScriptInterface.GetGeneralJSContext(), CStdDeserializer::Trace, this); } void CStdDeserializer::Trace(JSTracer *trc, void *data) { reinterpret_cast(data)->TraceMember(trc); } void CStdDeserializer::TraceMember(JSTracer *trc) { for (JS::Heap& backref : m_ScriptBackrefs) JS::TraceEdge(trc, &backref, "StdDeserializer::m_ScriptBackrefs"); } void CStdDeserializer::Get(const char* name, u8* data, size_t len) { #if DEBUG_SERIALIZER_ANNOTATE std::string strName; char c = m_Stream.get(); ENSURE(c == '<'); while (1) { c = m_Stream.get(); if (c == '>') break; else strName += c; } ENSURE(strName == name); #else UNUSED2(name); #endif m_Stream.read((char*)data, (std::streamsize)len); if (!m_Stream.good()) { // hit eof before len, or other errors // NOTE: older libc++ versions incorrectly set eofbit on the last char; test gcount as a workaround // see https://llvm.org/bugs/show_bug.cgi?id=9335 if (m_Stream.bad() || m_Stream.fail() || (m_Stream.eof() && m_Stream.gcount() != (std::streamsize)len)) throw PSERROR_Deserialize_ReadFailed(); } } std::istream& CStdDeserializer::GetStream() { return m_Stream; } void CStdDeserializer::RequireBytesInStream(size_t numBytes) { // It would be nice to do: // if (numBytes > (size_t)m_Stream.rdbuf()->in_avail()) // throw PSERROR_Deserialize_OutOfBounds("RequireBytesInStream"); // but that doesn't work (at least on MSVC) since in_avail isn't // guaranteed to return the actual number of bytes available; see e.g. // http://social.msdn.microsoft.com/Forums/en/vclanguage/thread/13009a88-933f-4be7-bf3d-150e425e66a6#70ea562d-8605-4742-8851-1bae431ce6ce // Instead we'll just verify that it's not an extremely large number: if (numBytes > 64*MiB) throw PSERROR_Deserialize_OutOfBounds("RequireBytesInStream"); } void CStdDeserializer::AddScriptBackref(JS::HandleObject obj) { m_ScriptBackrefs.push_back(JS::Heap(obj)); } void CStdDeserializer::GetScriptBackref(size_t tag, JS::MutableHandleObject ret) { ENSURE(m_ScriptBackrefs.size() > tag); ret.set(m_ScriptBackrefs[tag]); } //////////////////////////////////////////////////////////////// JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleObject preexistingObject) { ScriptRequest rq(m_ScriptInterface); uint8_t type; NumberU8_Unbounded("type", type); switch (type) { case SCRIPT_TYPE_VOID: return JS::UndefinedValue(); case SCRIPT_TYPE_NULL: return JS::NullValue(); case SCRIPT_TYPE_ARRAY: case SCRIPT_TYPE_OBJECT: case SCRIPT_TYPE_OBJECT_PROTOTYPE: { JS::RootedObject obj(rq.cx); if (type == SCRIPT_TYPE_ARRAY) { u32 length; NumberU32_Unbounded("array length", length); obj.set(JS::NewArrayObject(rq.cx, length)); } else if (type == SCRIPT_TYPE_OBJECT) { obj.set(JS_NewPlainObject(rq.cx)); } else // SCRIPT_TYPE_OBJECT_PROTOTYPE { CStrW prototypeName; String("proto", prototypeName, 0, 256); // If an object was passed, no need to construct a new one. if (preexistingObject != nullptr) obj.set(preexistingObject); else { JS::RootedValue constructor(rq.cx); if (!ScriptInterface::GetGlobalProperty(rq, prototypeName.ToUTF8(), &constructor)) throw PSERROR_Deserialize_ScriptError("Deserializer failed to get constructor object"); JS::RootedObject newObj(rq.cx); if (!JS::Construct(rq.cx, constructor, JS::HandleValueArray::empty(), &newObj)) throw PSERROR_Deserialize_ScriptError("Deserializer failed to construct object"); obj.set(newObj); } JS::RootedObject prototype(rq.cx); JS_GetPrototype(rq.cx, obj, &prototype); SPrototypeSerialization info = GetPrototypeInfo(rq, prototype); if (preexistingObject != nullptr && prototypeName != wstring_from_utf8(info.name)) throw PSERROR_Deserialize_ScriptError("Deserializer failed: incorrect pre-existing object"); if (info.hasCustomDeserialize) { AddScriptBackref(obj); // If Serialize is null, we'll still call Deserialize but with undefined argument JS::RootedValue data(rq.cx); if (!info.hasNullSerialize) ScriptVal("data", &data); JS::RootedValue objVal(rq.cx, JS::ObjectValue(*obj)); - m_ScriptInterface.CallFunctionVoid(objVal, "Deserialize", data); + ScriptFunction::CallVoid(rq, objVal, "Deserialize", data); return JS::ObjectValue(*obj); } else if (info.hasNullSerialize) { // If we serialized null, this means we're pretty much a default-constructed object. // Nothing to do. AddScriptBackref(obj); return JS::ObjectValue(*obj); } } if (!obj) throw PSERROR_Deserialize_ScriptError("Deserializer failed to create new object"); AddScriptBackref(obj); uint32_t numProps; NumberU32_Unbounded("num props", numProps); bool isLatin1; for (uint32_t i = 0; i < numProps; ++i) { Bool("isLatin1", isLatin1); if (isLatin1) { std::vector propname; ReadStringLatin1("prop name", propname); JS::RootedValue propval(rq.cx, ReadScriptVal("prop value", nullptr)); utf16string prp(propname.begin(), propname.end());; // TODO: Should ask upstream about getting a variant of JS_SetProperty with a length param. if (!JS_SetUCProperty(rq.cx, obj, (const char16_t*)prp.data(), prp.length(), propval)) throw PSERROR_Deserialize_ScriptError(); } else { utf16string propname; ReadStringUTF16("prop name", propname); JS::RootedValue propval(rq.cx, ReadScriptVal("prop value", nullptr)); if (!JS_SetUCProperty(rq.cx, obj, (const char16_t*)propname.data(), propname.length(), propval)) throw PSERROR_Deserialize_ScriptError(); } } return JS::ObjectValue(*obj); } case SCRIPT_TYPE_STRING: { JS::RootedString str(rq.cx); ScriptString("string", &str); return JS::StringValue(str); } case SCRIPT_TYPE_INT: { int32_t value; NumberI32("value", value, JSVAL_INT_MIN, JSVAL_INT_MAX); return JS::NumberValue(value); } case SCRIPT_TYPE_DOUBLE: { double value; NumberDouble_Unbounded("value", value); JS::RootedValue rval(rq.cx, JS::NumberValue(value)); if (rval.isNull()) throw PSERROR_Deserialize_ScriptError("JS_NewNumberValue failed"); return rval; } case SCRIPT_TYPE_BOOLEAN: { uint8_t value; NumberU8("value", value, 0, 1); return JS::BooleanValue(value ? true : false); } case SCRIPT_TYPE_BACKREF: { i32 tag; NumberI32("tag", tag, 0, JSVAL_INT_MAX); JS::RootedObject obj(rq.cx); GetScriptBackref(tag, &obj); if (!obj) throw PSERROR_Deserialize_ScriptError("Invalid backref tag"); return JS::ObjectValue(*obj); } case SCRIPT_TYPE_OBJECT_NUMBER: { double value; NumberDouble_Unbounded("value", value); JS::RootedValue val(rq.cx, JS::NumberValue(value)); JS::RootedObject ctorobj(rq.cx); if (!JS_GetClassObject(rq.cx, JSProto_Number, &ctorobj)) throw PSERROR_Deserialize_ScriptError("JS_GetClassObject failed"); JS::RootedObject obj(rq.cx, JS_New(rq.cx, ctorobj, JS::HandleValueArray(val))); if (!obj) throw PSERROR_Deserialize_ScriptError("JS_New failed"); AddScriptBackref(obj); return JS::ObjectValue(*obj); } case SCRIPT_TYPE_OBJECT_STRING: { JS::RootedString str(rq.cx); ScriptString("value", &str); if (!str) throw PSERROR_Deserialize_ScriptError(); JS::RootedValue val(rq.cx, JS::StringValue(str)); JS::RootedObject ctorobj(rq.cx); if (!JS_GetClassObject(rq.cx, JSProto_String, &ctorobj)) throw PSERROR_Deserialize_ScriptError("JS_GetClassObject failed"); JS::RootedObject obj(rq.cx, JS_New(rq.cx, ctorobj, JS::HandleValueArray(val))); if (!obj) throw PSERROR_Deserialize_ScriptError("JS_New failed"); AddScriptBackref(obj); return JS::ObjectValue(*obj); } case SCRIPT_TYPE_OBJECT_BOOLEAN: { bool value; Bool("value", value); JS::RootedValue val(rq.cx, JS::BooleanValue(value)); JS::RootedObject ctorobj(rq.cx); if (!JS_GetClassObject(rq.cx, JSProto_Boolean, &ctorobj)) throw PSERROR_Deserialize_ScriptError("JS_GetClassObject failed"); JS::RootedObject obj(rq.cx, JS_New(rq.cx, ctorobj, JS::HandleValueArray(val))); if (!obj) throw PSERROR_Deserialize_ScriptError("JS_New failed"); AddScriptBackref(obj); return JS::ObjectValue(*obj); } case SCRIPT_TYPE_TYPED_ARRAY: { u8 arrayType; u32 byteOffset, length; NumberU8_Unbounded("array type", arrayType); NumberU32_Unbounded("byte offset", byteOffset); NumberU32_Unbounded("length", length); // To match the serializer order, we reserve the typed array's backref tag here JS::RootedObject arrayObj(rq.cx); AddScriptBackref(arrayObj); // Get buffer object JS::RootedValue bufferVal(rq.cx, ReadScriptVal("buffer", nullptr)); if (!bufferVal.isObject()) throw PSERROR_Deserialize_ScriptError(); JS::RootedObject bufferObj(rq.cx, &bufferVal.toObject()); if (!JS::IsArrayBufferObject(bufferObj)) throw PSERROR_Deserialize_ScriptError("js_IsArrayBuffer failed"); switch(arrayType) { case SCRIPT_TYPED_ARRAY_INT8: arrayObj = JS_NewInt8ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_UINT8: arrayObj = JS_NewUint8ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_INT16: arrayObj = JS_NewInt16ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_UINT16: arrayObj = JS_NewUint16ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_INT32: arrayObj = JS_NewInt32ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_UINT32: arrayObj = JS_NewUint32ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_FLOAT32: arrayObj = JS_NewFloat32ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_FLOAT64: arrayObj = JS_NewFloat64ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_UINT8_CLAMPED: arrayObj = JS_NewUint8ClampedArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; default: throw PSERROR_Deserialize_ScriptError("Failed to deserialize unrecognized typed array view"); } if (!arrayObj) throw PSERROR_Deserialize_ScriptError("js_CreateTypedArrayWithBuffer failed"); return JS::ObjectValue(*arrayObj); } case SCRIPT_TYPE_ARRAY_BUFFER: { u32 length; NumberU32_Unbounded("buffer length", length); #if BYTE_ORDER != LITTLE_ENDIAN #error TODO: need to convert JS ArrayBuffer data from little-endian #endif void* contents = malloc(length); ENSURE(contents); RawBytes("buffer data", (u8*)contents, length); JS::RootedObject bufferObj(rq.cx, JS::NewArrayBufferWithContents(rq.cx, length, contents)); AddScriptBackref(bufferObj); return JS::ObjectValue(*bufferObj); } case SCRIPT_TYPE_OBJECT_MAP: { JS::RootedObject obj(rq.cx, JS::NewMapObject(rq.cx)); AddScriptBackref(obj); u32 mapSize; NumberU32_Unbounded("map size", mapSize); for (u32 i=0; i& str) { uint32_t len; NumberU32_Unbounded("string length", len); RequireBytesInStream(len); str.resize(len); Get(name, (u8*)str.data(), len); } void CStdDeserializer::ReadStringUTF16(const char* name, utf16string& str) { uint32_t len; NumberU32_Unbounded("string length", len); RequireBytesInStream(len*2); str.resize(len); Get(name, (u8*)str.data(), len*2); } void CStdDeserializer::ScriptString(const char* name, JS::MutableHandleString out) { #if BYTE_ORDER != LITTLE_ENDIAN #error TODO: probably need to convert JS strings from little-endian #endif ScriptRequest rq(m_ScriptInterface); bool isLatin1; Bool("isLatin1", isLatin1); if (isLatin1) { std::vector str; ReadStringLatin1(name, str); out.set(JS_NewStringCopyN(rq.cx, (const char*)str.data(), str.size())); if (!out) throw PSERROR_Deserialize_ScriptError("JS_NewStringCopyN failed"); } else { utf16string str; ReadStringUTF16(name, str); out.set(JS_NewUCStringCopyN(rq.cx, (const char16_t*)str.data(), str.length())); if (!out) throw PSERROR_Deserialize_ScriptError("JS_NewUCStringCopyN failed"); } } void CStdDeserializer::ScriptVal(const char* name, JS::MutableHandleValue out) { out.set(ReadScriptVal(name, nullptr)); } void CStdDeserializer::ScriptObjectAssign(const char* name, JS::HandleValue objVal) { ScriptRequest rq(m_ScriptInterface); if (!objVal.isObject()) throw PSERROR_Deserialize_ScriptError(); JS::RootedObject obj(rq.cx, &objVal.toObject()); ReadScriptVal(name, obj); } Index: ps/trunk/source/simulation2/tests/test_Serializer.h =================================================================== --- ps/trunk/source/simulation2/tests/test_Serializer.h (revision 25353) +++ ps/trunk/source/simulation2/tests/test_Serializer.h (revision 25354) @@ -1,939 +1,940 @@ /* Copyright (C) 2021 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 "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/HashSerializer.h" #include "simulation2/serialization/StdSerializer.h" #include "simulation2/serialization/StdDeserializer.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Loader.h" #include "ps/XML/Xeromyces.h" #include "simulation2/Simulation2.h" #include "callgrind.h" #include #define TS_ASSERT_STREAM(stream, len, buffer) \ TS_ASSERT_EQUALS(stream.str().length(), (size_t)len); \ TS_ASSERT_SAME_DATA(stream.str().data(), buffer, len) #define TSM_ASSERT_STREAM(m, stream, len, buffer) \ TSM_ASSERT_EQUALS(m, stream.str().length(), (size_t)len); \ TSM_ASSERT_SAME_DATA(m, stream.str().data(), buffer, len) class TestSerializer : public CxxTest::TestSuite { public: void serialize_types(ISerializer& serialize) { serialize.NumberI8_Unbounded("i8", (signed char)-123); serialize.NumberU8_Unbounded("u8", (unsigned char)255); serialize.NumberI16_Unbounded("i16", -12345); serialize.NumberU16_Unbounded("u16", 56789); serialize.NumberI32_Unbounded("i32", -123); serialize.NumberU32_Unbounded("u32", (unsigned)-123); serialize.NumberFloat_Unbounded("float", 1e+30f); serialize.NumberDouble_Unbounded("double", 1e+300); serialize.NumberFixed_Unbounded("fixed", fixed::FromFloat(1234.5f)); serialize.Bool("t", true); serialize.Bool("f", false); serialize.StringASCII("string", "example", 0, 255); serialize.StringASCII("string 2", "example\"\\\"", 0, 255); serialize.StringASCII("string 3", "example\n\ntest", 0, 255); wchar_t testw[] = { 't', 0xEA, 's', 't', 0 }; serialize.String("string 4", testw, 0, 255); serialize.RawBytes("raw bytes", (const u8*)"\0\1\2\3\x0f\x10", 6); } void test_Debug_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_STR_EQUALS(stream.str(), "x: -123\ny: 1234\nz: 12345\n"); } void test_Debug_floats() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberFloat_Unbounded("x", 1e4f); serialize.NumberFloat_Unbounded("x", 1e-4f); serialize.NumberFloat_Unbounded("x", 1e5f); serialize.NumberFloat_Unbounded("x", 1e-5f); serialize.NumberFloat_Unbounded("x", 1e6f); serialize.NumberFloat_Unbounded("x", 1e-6f); serialize.NumberFloat_Unbounded("x", 1e10f); serialize.NumberFloat_Unbounded("x", 1e-10f); serialize.NumberDouble_Unbounded("x", 1e4); serialize.NumberDouble_Unbounded("x", 1e-4); serialize.NumberDouble_Unbounded("x", 1e5); serialize.NumberDouble_Unbounded("x", 1e-5); serialize.NumberDouble_Unbounded("x", 1e6); serialize.NumberDouble_Unbounded("x", 1e-6); serialize.NumberDouble_Unbounded("x", 1e10); serialize.NumberDouble_Unbounded("x", 1e-10); serialize.NumberDouble_Unbounded("x", 1e100); serialize.NumberDouble_Unbounded("x", 1e-100); serialize.NumberFixed_Unbounded("x", fixed::FromDouble(1e4)); TS_ASSERT_STR_EQUALS(stream.str(), "x: 10000\nx: 9.9999997e-05\nx: 100000\nx: 9.9999997e-06\nx: 1000000\nx: 1e-06\nx: 1e+10\nx: 1e-10\n" "x: 10000\nx: 0.0001\nx: 100000\nx: 1.0000000000000001e-05\nx: 1000000\nx: 9.9999999999999995e-07\nx: 10000000000\nx: 1e-10\nx: 1e+100\nx: 1e-100\n" "x: 10000\n" ); } void test_Debug_types() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.Comment("comment"); serialize_types(serialize); TS_ASSERT_STR_EQUALS(stream.str(), "# comment\n" "i8: -123\n" "u8: 255\n" "i16: -12345\n" "u16: 56789\n" "i32: -123\n" "u32: 4294967173\n" "float: 1e+30\n" "double: 1.0000000000000001e+300\n" "fixed: 1234.5\n" "t: true\n" "f: false\n" "string: \"example\"\n" "string 2: \"example\\\"\\\\\\\"\"\n" // C-escaped form of: "example\"\\\"" "string 3: \"example\\n\\ntest\"\n" "string 4: \"t\xC3\xAAst\"\n" "raw bytes: (6 bytes) 00 01 02 03 0f 10\n" ); } void test_Std_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CStdSerializer serialize(script, stream); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_STREAM(stream, 12, "\x85\xff\xff\xff" "\xd2\x04\x00\x00" "\x39\x30\x00\x00"); CStdDeserializer deserialize(script, stream); int32_t n; deserialize.NumberI32_Unbounded("x", n); TS_ASSERT_EQUALS(n, -123); deserialize.NumberI32_Unbounded("y", n); TS_ASSERT_EQUALS(n, 1234); deserialize.NumberI32("z", n, 0, 65535); TS_ASSERT_EQUALS(n, 12345); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TS_ASSERT(!stream.bad() && !stream.fail()); TS_ASSERT_EQUALS(stream.peek(), EOF); } void test_Std_types() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CStdSerializer serialize(script, stream); serialize_types(serialize); CStdDeserializer deserialize(script, stream); int8_t i8v; uint8_t u8v; int16_t i16v; uint16_t u16v; int32_t i32v; uint32_t u32v; float flt; double dbl; fixed fxd; bool bl; std::string str; std::wstring wstr; u8 cbuf[256]; deserialize.NumberI8_Unbounded("i8", i8v); TS_ASSERT_EQUALS(i8v, -123); deserialize.NumberU8_Unbounded("u8", u8v); TS_ASSERT_EQUALS(u8v, 255); deserialize.NumberI16_Unbounded("i16", i16v); TS_ASSERT_EQUALS(i16v, -12345); deserialize.NumberU16_Unbounded("u16", u16v); TS_ASSERT_EQUALS(u16v, 56789); deserialize.NumberI32_Unbounded("i32", i32v); TS_ASSERT_EQUALS(i32v, -123); deserialize.NumberU32_Unbounded("u32", u32v); TS_ASSERT_EQUALS(u32v, 4294967173u); deserialize.NumberFloat_Unbounded("float", flt); TS_ASSERT_EQUALS(flt, 1e+30f); deserialize.NumberDouble_Unbounded("double", dbl); TS_ASSERT_EQUALS(dbl, 1e+300); deserialize.NumberFixed_Unbounded("fixed", fxd); TS_ASSERT_EQUALS(fxd.ToDouble(), 1234.5); deserialize.Bool("t", bl); TS_ASSERT_EQUALS(bl, true); deserialize.Bool("f", bl); TS_ASSERT_EQUALS(bl, false); deserialize.StringASCII("string", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example"); deserialize.StringASCII("string 2", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example\"\\\""); deserialize.StringASCII("string 3", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example\n\ntest"); wchar_t testw[] = { 't', 0xEA, 's', 't', 0 }; deserialize.String("string 4", wstr, 0, 255); TS_ASSERT_WSTR_EQUALS(wstr, testw); cbuf[6] = 0x42; // sentinel deserialize.RawBytes("raw bytes", cbuf, 6); TS_ASSERT_SAME_DATA(cbuf, (const u8*)"\0\1\2\3\x0f\x10\x42", 7); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TS_ASSERT(!stream.bad() && !stream.fail()); TS_ASSERT_EQUALS(stream.peek(), EOF); } void test_Hash_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); CHashSerializer serialize(script); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_EQUALS(serialize.GetHashLength(), (size_t)16); TS_ASSERT_SAME_DATA(serialize.ComputeHash(), "\xa0\x3a\xe5\x3e\x9b\xd7\xfb\x11\x88\x35\xc6\xfb\xb9\x94\xa9\x72", 16); // echo -en "\x85\xff\xff\xff\xd2\x04\x00\x00\x39\x30\x00\x00" | openssl md5 -binary | xxd -p | perl -pe 's/(..)/\\x$1/g' } void test_Hash_stream() { ScriptInterface script("Test", "Test", g_ScriptContext); CHashSerializer hashSerialize(script); hashSerialize.NumberI32_Unbounded("x", -123); hashSerialize.NumberU32_Unbounded("y", 1234); hashSerialize.NumberI32("z", 12345, 0, 65535); ISerializer& serialize = hashSerialize; { CStdSerializer streamSerialize(script, serialize.GetStream()); streamSerialize.NumberI32_Unbounded("x2", -456); streamSerialize.NumberU32_Unbounded("y2", 5678); streamSerialize.NumberI32("z2", 45678, 0, 65535); } TS_ASSERT_EQUALS(hashSerialize.GetHashLength(), (size_t)16); TS_ASSERT_SAME_DATA(hashSerialize.ComputeHash(), "\x5c\xff\x33\xd1\x72\xdd\x6d\x77\xa8\xd4\xa1\xf6\x84\xcc\xaa\x10", 16); // echo -en "\x85\xff\xff\xff\xd2\x04\x00\x00\x39\x30\x00\x00\x38\xfe\xff\xff\x2e\x16\x00\x00\x6e\xb2\x00\x00" | openssl md5 -binary | xxd -p | perl -pe 's/(..)/\\x$1/g' } void test_bounds() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberI32("x", 16, -16, 16); serialize.NumberI32("x", -16, -16, 16); TS_ASSERT_THROWS(serialize.NumberI32("x", 99, -16, 16), const PSERROR_Serialize_OutOfBounds&); TS_ASSERT_THROWS(serialize.NumberI32("x", -17, -16, 16), const PSERROR_Serialize_OutOfBounds&); } // TODO: test exceptions more thoroughly void helper_script_roundtrip(const char* msg, const char* input, const char* expected, size_t expstreamlen = 0, const char* expstream = NULL, const char* debug = NULL) { ScriptInterface script("Test", "Test", g_ScriptContext); ScriptRequest rq(script); JS::RootedValue obj(rq.cx); TSM_ASSERT(msg, script.Eval(input, &obj)); if (debug) { std::stringstream dbgstream; CDebugSerializer serialize(script, dbgstream); serialize.ScriptVal("script", &obj); TS_ASSERT_STR_EQUALS(dbgstream.str(), debug); } std::stringstream stream; CStdSerializer serialize(script, stream); serialize.ScriptVal("script", &obj); if (expstream) { TSM_ASSERT_STREAM(msg, stream, expstreamlen, expstream); } CStdDeserializer deserialize(script, stream); JS::RootedValue newobj(rq.cx); deserialize.ScriptVal("script", &newobj); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TSM_ASSERT(msg, !stream.bad() && !stream.fail()); TSM_ASSERT_EQUALS(msg, stream.peek(), EOF); std::stringstream stream2; CStdSerializer serialize2(script, stream2); CStdDeserializer deserialize2(script, stream2); // Round-trip the deserialized value again. This helps ensure prototypes are correctly deserialized. serialize2.ScriptVal("script2", &newobj); deserialize2.ScriptVal("script2", &newobj); std::string source; - TSM_ASSERT(msg, script.CallFunction(newobj, "toSource", source)); + TSM_ASSERT(msg, ScriptFunction::Call(rq, newobj, "toSource", source)); TS_ASSERT_STR_EQUALS(source, expected); } void test_script_basic() { helper_script_roundtrip("Object", "({'x': 123, 'y': [1, 1.5, '2', 'test', undefined, null, true, false]})", /* expected: */ "({x:123, y:[1, 1.5, \"2\", \"test\", (void 0), null, true, false]})", /* expected stream: */ 116, "\x03" // SCRIPT_TYPE_OBJECT "\x02\0\0\0" // num props "\x01\x01\0\0\0" "x" // "x" "\x05" // SCRIPT_TYPE_INT "\x7b\0\0\0" // 123 "\x01\x01\0\0\0" "y" // "y" "\x02" // SCRIPT_TYPE_ARRAY "\x08\0\0\0" // array length "\x08\0\0\0" // num props "\x01\x01\0\0\0" "0" // "0" "\x05" "\x01\0\0\0" // SCRIPT_TYPE_INT 1 "\x01\x01\0\0\0" "1" // "1" "\x06" "\0\0\0\0\0\0\xf8\x3f" // SCRIPT_TYPE_DOUBLE 1.5 "\x01\x01\0\0\0" "2" // "2" "\x04" "\x01\x01\0\0\0" "2" // SCRIPT_TYPE_STRING "2" "\x01\x01\0\0\0" "3" // "3" "\x04" "\x01\x04\0\0\0" "test" // SCRIPT_TYPE_STRING "test" "\x01\x01\0\0\0" "4" // "4" "\x00" // SCRIPT_TYPE_VOID "\x01\x01\0\0\0" "5" // "5" "\x01" // SCRIPT_TYPE_NULL "\x01\x01\0\0\0" "6" // "6" "\x07" "\x01" // SCRIPT_TYPE_BOOLEAN true "\x01\x01\0\0\0" "7" // "7" "\x07" "\x00", // SCRIPT_TYPE_BOOLEAN false /* expected debug: */ "script: {\n" " \"x\": 123,\n" " \"y\": [\n" " 1,\n" " 1.5,\n" " \"2\",\n" " \"test\",\n" " null,\n" " null,\n" " true,\n" " false\n" " ]\n" "}\n" ); } void test_script_unicode() { helper_script_roundtrip("unicode", "({" "'x': \"\\x01\\x80\\xff\\u0100\\ud7ff\", " "'y': \"\\ue000\\ufffd\"" "})", /* expected: */ "({" "x:\"\\x01\\x80\\xFF\\u0100\\uD7FF\", " "y:\"\\uE000\\uFFFD\"" "})"); // Disabled since we no longer do the UTF-8 conversion that rejects invalid characters // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 1", "(\"\\ud7ff\\ud800\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 2", "(\"\\udfff\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 3", "(\"\\uffff\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 4", "(\"\\ud800\\udc00\")" /* U+10000 */, "..."), PSERROR_Serialize_InvalidCharInString); helper_script_roundtrip("unicode", "\"\\ud800\\uffff\"", "(new String(\"\\uD800\\uFFFF\"))"); } void test_script_objects() { helper_script_roundtrip("Number", "[1, new Number('2.0'), 3]", "[1, (new Number(2)), 3]"); helper_script_roundtrip("Number with props", "var n=new Number('2.0'); n.foo='bar'; n", "(new Number(2))"); helper_script_roundtrip("String", "['test1', new String('test2'), 'test3']", "[\"test1\", (new String(\"test2\")), \"test3\"]"); helper_script_roundtrip("String with props", "var s=new String('test'); s.foo='bar'; s", "(new String(\"test\"))"); helper_script_roundtrip("Boolean", "[new Boolean('true'), false]", "[(new Boolean(true)), false]"); helper_script_roundtrip("Boolean with props", "var b=new Boolean('true'); b.foo='bar'; b", "(new Boolean(true))"); } void test_script_fancy_objects() { // This asserts that objects are deserialized with their correct prototypes. helper_script_roundtrip("Custom Object", "" "function customObj() { this.a = this.customFunc.name; };" "customObj.prototype.customFunc = function customFunc(){};" "new customObj();", "({a:\"customFunc\"})"); helper_script_roundtrip("Custom Class", "" "class customObj {" " constructor() { this.a = this.customFunc.name; }" " customFunc(){};" "}; new customObj();", "({a:\"customFunc\"})"); helper_script_roundtrip("Custom Class with Serialize/Deserialize()", "" "class customObj {" " constructor() { this.a = this.customFunc.name; }" " Serialize() { return { 'foo': 'bar' }; }" " Deserialize(data) { this.foo = data.foo; }" " customFunc(){};" "}; new customObj();", "({a:\"customFunc\", foo:\"bar\"})"); helper_script_roundtrip("Custom Class with null serialize & deserialize()", "" "class customObj {" " constructor() { this.a = this.customFunc.name; }" " Deserialize(data) { this.test = 'test'; };" " customFunc(){};" "}; customObj.prototype.Serialize=null;" "new customObj();", "({a:\"customFunc\", test:\"test\"})"); helper_script_roundtrip("Custom Class with arguments but still works", "" "class customObj {" " constructor(test) { this.a = test; }" " Serialize() { return { 'data': this.a }; };" " Deserialize(data) { this.a = data.data; };" "}; new customObj(4);", "({a:4})"); } void test_script_objects_properties() { helper_script_roundtrip("Object with null in prop name", "({\"foo\\0bar\":1})", "({\'foo\\x00bar\':1})"); } void test_script_typed_arrays_simple() { helper_script_roundtrip("Int8Array", "var arr=new Int8Array(8);" "for(var i=0; iMount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader = std::make_unique(); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/skirmishes/Greek Acropolis (2).pmp", *sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.Update(0); { std::stringstream str; std::string hash; sim2.SerializeState(str); sim2.ComputeStateHash(hash, false); debug_printf("\n"); debug_printf("# size = %d\n", (int)str.str().length()); debug_printf("# hash = "); for (size_t i = 0; i < hash.size(); ++i) debug_printf("%02x", (unsigned int)(u8)hash[i]); debug_printf("\n"); } double t = timer_Time(); CALLGRIND_START_INSTRUMENTATION; size_t reps = 128; for (size_t i = 0; i < reps; ++i) { std::string hash; sim2.ComputeStateHash(hash, false); } CALLGRIND_STOP_INSTRUMENTATION; t = timer_Time() - t; debug_printf("# time = %f (%f/%d)\n", t/reps, t, (int)reps); // Shut down the world delete &g_TexMan; g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); CXeromyces::Terminate(); } };