Index: ps/trunk/source/gui/CGUISetting.cpp =================================================================== --- ps/trunk/source/gui/CGUISetting.cpp (revision 22948) +++ ps/trunk/source/gui/CGUISetting.cpp (revision 22949) @@ -1,86 +1,87 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "CGUISetting.h" #include "gui/CGUI.h" template CGUISetting::CGUISetting(IGUIObject& pObject, const CStr& Name) : m_pSetting(T()), m_Name(Name), m_pObject(pObject) { } template bool CGUISetting::FromString(const CStrW& Value, const bool SendMessage) { T settingValue; if (!CGUI::ParseString(&m_pObject.GetGUI(), Value, settingValue)) return false; m_pObject.SetSetting(m_Name, settingValue, SendMessage); return true; }; template<> bool CGUISetting::FromJSVal(JSContext* cx, JS::HandleValue Value, const bool SendMessage) { CGUIColor settingValue; if (Value.isString()) { CStr name; if (!ScriptInterface::FromJSVal(cx, Value, name)) return false; if (!settingValue.ParseString(m_pObject.GetGUI(), name)) { + JSAutoRequest rq(cx); JS_ReportError(cx, "Invalid color '%s'", name.c_str()); return false; } } else if (!ScriptInterface::FromJSVal(cx, Value, settingValue)) return false; m_pObject.SetSetting(m_Name, settingValue, SendMessage); return true; }; template bool CGUISetting::FromJSVal(JSContext* cx, JS::HandleValue Value, const bool SendMessage) { T settingValue; if (!ScriptInterface::FromJSVal(cx, Value, settingValue)) return false; m_pObject.SetSetting(m_Name, settingValue, SendMessage); return true; }; template void CGUISetting::ToJSVal(JSContext* cx, JS::MutableHandleValue Value) { ScriptInterface::ToJSVal(cx, Value, m_pSetting); }; #define TYPE(T) \ template class CGUISetting; \ #include "GUItypes.h" #undef TYPE Index: ps/trunk/source/gui/scripting/GuiScriptConversions.cpp =================================================================== --- ps/trunk/source/gui/scripting/GuiScriptConversions.cpp (revision 22948) +++ ps/trunk/source/gui/scripting/GuiScriptConversions.cpp (revision 22949) @@ -1,359 +1,369 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "gui/CGUIColor.h" #include "gui/CGUIList.h" #include "gui/CGUISeries.h" #include "gui/GUIbase.h" #include "gui/IGUIObject.h" #include "lib/external_libraries/libsdl.h" #include "maths/Vector2D.h" #include "ps/Hotkey.h" #include "scriptinterface/ScriptConversions.h" #include #define SET(obj, name, value) STMT(JS::RootedValue v_(cx); AssignOrToJSVal(cx, &v_, (value)); JS_SetProperty(cx, obj, (name), v_)) // ignore JS_SetProperty return value, because errors should be impossible // and we can't do anything useful in the case of errors anyway template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, SDL_Event_ const& val) { JSAutoRequest rq(cx); const char* typeName; switch (val.ev.type) { case SDL_WINDOWEVENT: typeName = "windowevent"; break; case SDL_KEYDOWN: typeName = "keydown"; break; case SDL_KEYUP: typeName = "keyup"; break; case SDL_MOUSEMOTION: typeName = "mousemotion"; break; case SDL_MOUSEBUTTONDOWN: typeName = "mousebuttondown"; break; case SDL_MOUSEBUTTONUP: typeName = "mousebuttonup"; break; case SDL_QUIT: typeName = "quit"; break; case SDL_HOTKEYDOWN: typeName = "hotkeydown"; break; case SDL_HOTKEYUP: typeName = "hotkeyup"; break; default: typeName = "(unknown)"; break; } JS::RootedObject obj(cx, JS_NewPlainObject(cx)); if (!obj) { ret.setUndefined(); return; } SET(obj, "type", typeName); switch (val.ev.type) { case SDL_KEYDOWN: case SDL_KEYUP: { // SET(obj, "which", (int)val.ev.key.which); // (not in wsdl.h) // SET(obj, "state", (int)val.ev.key.state); // (not in wsdl.h) JS::RootedObject keysym(cx, JS_NewPlainObject(cx)); if (!keysym) { ret.setUndefined(); return; } JS::RootedValue keysymVal(cx, JS::ObjectValue(*keysym)); JS_SetProperty(cx, obj, "keysym", keysymVal); // SET(keysym, "scancode", (int)val.ev.key.keysym.scancode); // (not in wsdl.h) SET(keysym, "sym", (int)val.ev.key.keysym.sym); // SET(keysym, "mod", (int)val.ev.key.keysym.mod); // (not in wsdl.h) { SET(keysym, "unicode", JS::UndefinedHandleValue); } // TODO: scripts have no idea what all the key/mod enum values are; // we should probably expose them as constants if we expect scripts to use them break; } case SDL_MOUSEMOTION: { // SET(obj, "which", (int)val.ev.motion.which); // (not in wsdl.h) // SET(obj, "state", (int)val.ev.motion.state); // (not in wsdl.h) SET(obj, "x", (int)val.ev.motion.x); SET(obj, "y", (int)val.ev.motion.y); // SET(obj, "xrel", (int)val.ev.motion.xrel); // (not in wsdl.h) // SET(obj, "yrel", (int)val.ev.motion.yrel); // (not in wsdl.h) break; } case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: { // SET(obj, "which", (int)val.ev.button.which); // (not in wsdl.h) SET(obj, "button", (int)val.ev.button.button); SET(obj, "state", (int)val.ev.button.state); SET(obj, "x", (int)val.ev.button.x); SET(obj, "y", (int)val.ev.button.y); SET(obj, "clicks", (int)val.ev.button.clicks); break; } case SDL_HOTKEYDOWN: case SDL_HOTKEYUP: { SET(obj, "hotkey", static_cast(val.ev.user.data1)); break; } } ret.setObject(*obj); } template<> void ScriptInterface::ToJSVal(JSContext* UNUSED(cx), JS::MutableHandleValue ret, IGUIObject* const& val) { if (val == NULL) ret.setNull(); else ret.setObject(*val->GetJSObject()); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CGUIString& val) { ScriptInterface::ToJSVal(cx, ret, val.GetOriginalString()); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CGUIString& out) { std::wstring val; if (!FromJSVal(cx, v, val)) return false; out.SetValue(val); return true; } JSVAL_VECTOR(CVector2D) JSVAL_VECTOR(std::vector) JSVAL_VECTOR(CGUIString) template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CGUIColor& val) { ToJSVal(cx, ret, val); } /** * The color depends on the predefined color database stored in the current GUI page. */ template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CGUIColor& out) = delete; template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CSize& val) { CreateObject(cx, ret, "width", val.cx, "height", val.cy); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CSize& out) { if (!v.isObject()) { + JSAutoRequest rq(cx); JS_ReportError(cx, "CSize value must be an object!"); return false; } if (!FromJSProperty(cx, v, "width", out.cx)) { + JSAutoRequest rq(cx); JS_ReportError(cx, "Failed to get CSize.cx property"); return false; } if (!FromJSProperty(cx, v, "height", out.cy)) { + JSAutoRequest rq(cx); JS_ReportError(cx, "Failed to get CSize.cy property"); return false; } return true; } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CPos& val) { CreateObject(cx, ret, "x", val.x, "y", val.y); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CPos& out) { if (!v.isObject()) { + JSAutoRequest rq(cx); JS_ReportError(cx, "CPos value must be an object!"); return false; } if (!FromJSProperty(cx, v, "x", out.x)) { + JSAutoRequest rq(cx); JS_ReportError(cx, "Failed to get CPos.x property"); return false; } if (!FromJSProperty(cx, v, "y", out.y)) { + JSAutoRequest rq(cx); JS_ReportError(cx, "Failed to get CPos.y property"); return false; } return true; } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CRect& val) { CreateObject( cx, ret, "left", val.left, "right", val.right, "top", val.top, "bottom", val.bottom); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CClientArea& val) { val.ToJSVal(cx, ret); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CClientArea& out) { return out.FromJSVal(cx, v); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CGUIList& val) { ToJSVal(cx, ret, val.m_Items); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CGUIList& out) { return FromJSVal(cx, v, out.m_Items); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CGUISeries& val) { ToJSVal(cx, ret, val.m_Series); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CGUISeries& out) { return FromJSVal(cx, v, out.m_Series); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const EVAlign& val) { std::string word; switch (val) { case EVAlign_Top: word = "top"; break; case EVAlign_Bottom: word = "bottom"; break; case EVAlign_Center: word = "center"; break; default: word = "error"; + JSAutoRequest rq(cx); JS_ReportError(cx, "Invalid EVAlign"); break; } ToJSVal(cx, ret, word); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, EVAlign& out) { std::string word; FromJSVal(cx, v, word); if (word == "top") out = EVAlign_Top; else if (word == "bottom") out = EVAlign_Bottom; else if (word == "center") out = EVAlign_Center; else { out = EVAlign_Top; + JSAutoRequest rq(cx); JS_ReportError(cx, "Invalid alignment (should be 'left', 'right' or 'center')"); return false; } return true; } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const EAlign& val) { std::string word; switch (val) { case EAlign_Left: word = "left"; break; case EAlign_Right: word = "right"; break; case EAlign_Center: word = "center"; break; default: word = "error"; + JSAutoRequest rq(cx); JS_ReportError(cx, "Invalid alignment (should be 'left', 'right' or 'center')"); break; } ToJSVal(cx, ret, word); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, EAlign& out) { std::string word; FromJSVal(cx, v, word); if (word == "left") out = EAlign_Left; else if (word == "right") out = EAlign_Right; else if (word == "center") out = EAlign_Center; else { out = EAlign_Left; + JSAutoRequest rq(cx); JS_ReportError(cx, "Invalid alignment (should be 'left', 'right' or 'center')"); return false; } return true; } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CGUISpriteInstance& val) { ToJSVal(cx, ret, val.GetName()); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CGUISpriteInstance& out) { std::string name; if (!FromJSVal(cx, v, name)) return false; out.SetName(name); return true; } #undef SET Index: ps/trunk/source/ps/scripting/JSInterface_Game.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Game.cpp (revision 22948) +++ ps/trunk/source/ps/scripting/JSInterface_Game.cpp (revision 22949) @@ -1,185 +1,189 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "JSInterface_Game.h" #include "graphics/Terrain.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/Game.h" #include "ps/Replay.h" #include "ps/World.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/system/TurnManager.h" #include "simulation2/Simulation2.h" #include "soundmanager/SoundManager.h" extern void EndGame(); bool JSI_Game::IsGameStarted(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return g_Game; } void JSI_Game::StartGame(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs, int playerID) { ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); g_Game = new CGame(true); // Convert from GUI script context to sim script context CSimulation2* sim = g_Game->GetSimulation2(); JSContext* cxSim = sim->GetScriptInterface().GetContext(); JSAutoRequest rqSim(cxSim); JS::RootedValue gameAttribs(cxSim, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), attribs)); g_Game->SetPlayerID(playerID); g_Game->StartGame(&gameAttribs, ""); } void JSI_Game::Script_EndGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { EndGame(); } int JSI_Game::GetPlayerID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_Game) return -1; return g_Game->GetPlayerID(); } void JSI_Game::SetPlayerID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int id) { if (!g_Game) return; g_Game->SetPlayerID(id); } void JSI_Game::SetViewedPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int id) { if (!g_Game) return; g_Game->SetViewedPlayerID(id); } float JSI_Game::GetSimRate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return g_Game->GetSimRate(); } void JSI_Game::SetSimRate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), float rate) { g_Game->SetSimRate(rate); } bool JSI_Game::IsPaused(ScriptInterface::CxPrivate* pCxPrivate) { if (!g_Game) { - JS_ReportError(pCxPrivate->pScriptInterface->GetContext(), "Game is not started"); + JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); + JSAutoRequest rq(cx); + JS_ReportError(cx, "Game is not started"); return false; } return g_Game->m_Paused; } void JSI_Game::SetPaused(ScriptInterface::CxPrivate* pCxPrivate, bool pause, bool sendMessage) { if (!g_Game) { - JS_ReportError(pCxPrivate->pScriptInterface->GetContext(), "Game is not started"); + JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); + JSAutoRequest rq(cx); + JS_ReportError(cx, "Game is not started"); return; } g_Game->m_Paused = pause; #if CONFIG2_AUDIO if (g_SoundManager) g_SoundManager->Pause(pause); #endif if (g_NetClient && sendMessage) g_NetClient->SendPausedMessage(pause); } bool JSI_Game::IsVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_Game) return false; return g_Game->IsVisualReplay(); } std::wstring JSI_Game::GetCurrentReplayDirectory(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_Game) return std::wstring(); if (g_Game->IsVisualReplay()) return g_Game->GetReplayPath().Parent().Filename().string(); return g_Game->GetReplayLogger().GetDirectory().Filename().string(); } void JSI_Game::EnableTimeWarpRecording(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), unsigned int numTurns) { g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns); } void JSI_Game::RewindTimeWarp(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { g_Game->GetTurnManager()->RewindTimeWarp(); } void JSI_Game::DumpTerrainMipmap(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { VfsPath filename(L"screenshots/terrainmipmap.png"); g_Game->GetWorld()->GetTerrain()->GetHeightMipmap().DumpToDisk(filename); OsPath realPath; g_VFS->GetRealPath(filename, realPath); LOGMESSAGERENDER("Terrain mipmap written to '%s'", realPath.string8()); } void JSI_Game::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("IsGameStarted"); scriptInterface.RegisterFunction("StartGame"); scriptInterface.RegisterFunction("EndGame"); scriptInterface.RegisterFunction("GetPlayerID"); scriptInterface.RegisterFunction("SetPlayerID"); scriptInterface.RegisterFunction("SetViewedPlayer"); scriptInterface.RegisterFunction("GetSimRate"); scriptInterface.RegisterFunction("SetSimRate"); scriptInterface.RegisterFunction("IsPaused"); scriptInterface.RegisterFunction("SetPaused"); scriptInterface.RegisterFunction("IsVisualReplay"); scriptInterface.RegisterFunction("GetCurrentReplayDirectory"); scriptInterface.RegisterFunction("EnableTimeWarpRecording"); scriptInterface.RegisterFunction("RewindTimeWarp"); scriptInterface.RegisterFunction("DumpTerrainMipmap"); } Index: ps/trunk/source/ps/scripting/JSInterface_VFS.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_VFS.cpp (revision 22948) +++ ps/trunk/source/ps/scripting/JSInterface_VFS.cpp (revision 22949) @@ -1,282 +1,281 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "JSInterface_VFS.h" #include "lib/file/vfs/vfs_util.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Filesystem.h" #include "scriptinterface/ScriptVal.h" #include "scriptinterface/ScriptInterface.h" #include // Only allow engine compartments to read files they may be concerned about. #define PathRestriction_GUI {L""} #define PathRestriction_Simulation {L"simulation/"} #define PathRestriction_Maps {L"simulation/", L"maps/"} // shared error handling code #define JS_CHECK_FILE_ERR(err)\ /* this is liable to happen often, so don't complain */\ if (err == ERR::VFS_FILE_NOT_FOUND)\ {\ return 0; \ }\ /* unknown failure. We output an error message. */\ else if (err < 0)\ LOGERROR("Unknown failure in VFS %i", err ); /* else: success */ // state held across multiple BuildDirEntListCB calls; init by BuildDirEntList. struct BuildDirEntListState { JSContext* cx; JS::PersistentRootedObject filename_array; int cur_idx; BuildDirEntListState(JSContext* cx_) : cx(cx_), filename_array(cx, JS_NewArrayObject(cx, JS::HandleValueArray::empty())), cur_idx(0) { } }; // called for each matching directory entry; add its full pathname to array. static Status BuildDirEntListCB(const VfsPath& pathname, const CFileInfo& UNUSED(fileINfo), uintptr_t cbData) { BuildDirEntListState* s = (BuildDirEntListState*)cbData; JSAutoRequest rq(s->cx); JS::RootedObject filenameArrayObj(s->cx, s->filename_array); JS::RootedValue val(s->cx); ScriptInterface::ToJSVal( s->cx, &val, CStrW(pathname.string()) ); JS_SetElement(s->cx, filenameArrayObj, s->cur_idx++, val); return INFO::OK; } // Return an array of pathname strings, one for each matching entry in the // specified directory. // filter_string: default "" matches everything; otherwise, see vfs_next_dirent. // recurse: should subdirectories be included in the search? default false. JS::Value JSI_VFS::BuildDirEntList(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const std::wstring& path, const std::wstring& filterStr, bool recurse) { if (!PathRestrictionMet(pCxPrivate, validPaths, path)) return JS::NullValue(); // convert to const wchar_t*; if there's no filter, pass 0 for speed // (interpreted as: "accept all files without comparing"). const wchar_t* filter = 0; if (!filterStr.empty()) filter = filterStr.c_str(); int flags = recurse ? vfs::DIR_RECURSIVE : 0; JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); // build array in the callback function BuildDirEntListState state(cx); vfs::ForEachFile(g_VFS, path, BuildDirEntListCB, (uintptr_t)&state, filter, flags); return JS::ObjectValue(*state.filename_array); } // Return true iff the file exits bool JSI_VFS::FileExists(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filename) { return PathRestrictionMet(pCxPrivate, validPaths, filename) && g_VFS->GetFileInfo(filename, 0) == INFO::OK; } // Return time [seconds since 1970] of the last modification to the specified file. double JSI_VFS::GetFileMTime(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& filename) { CFileInfo fileInfo; Status err = g_VFS->GetFileInfo(filename, &fileInfo); JS_CHECK_FILE_ERR(err); return (double)fileInfo.MTime(); } // Return current size of file. unsigned int JSI_VFS::GetFileSize(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& filename) { CFileInfo fileInfo; Status err = g_VFS->GetFileInfo(filename, &fileInfo); JS_CHECK_FILE_ERR(err); return (unsigned int)fileInfo.Size(); } // Return file contents in a string. Assume file is UTF-8 encoded text. JS::Value JSI_VFS::ReadFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename) { JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); CVFSFile file; if (file.Load(g_VFS, filename) != PSRETURN_OK) return JS::NullValue(); CStr contents = file.DecodeUTF8(); // assume it's UTF-8 // Fix CRLF line endings. (This function will only ever be used on text files.) contents.Replace("\r\n", "\n"); // Decode as UTF-8 JS::RootedValue ret(cx); ScriptInterface::ToJSVal(cx, &ret, contents.FromUTF8()); return ret; } // Return file contents as an array of lines. Assume file is UTF-8 encoded text. JS::Value JSI_VFS::ReadFileLines(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename) { const ScriptInterface& scriptInterface = *pCxPrivate->pScriptInterface; JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); CVFSFile file; if (file.Load(g_VFS, filename) != PSRETURN_OK) return JS::NullValue(); CStr contents = file.DecodeUTF8(); // assume it's UTF-8 // Fix CRLF line endings. (This function will only ever be used on text files.) contents.Replace("\r\n", "\n"); // split into array of strings (one per line) std::stringstream ss(contents); JS::RootedValue line_array(cx); ScriptInterface::CreateArray(cx, &line_array); std::string line; int cur_line = 0; while (std::getline(ss, line)) { // Decode each line as UTF-8 JS::RootedValue val(cx); ScriptInterface::ToJSVal(cx, &val, CStr(line).FromUTF8()); scriptInterface.SetPropertyInt(line_array, cur_line++, val); } return line_array; } JS::Value JSI_VFS::ReadJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filePath) { if (!PathRestrictionMet(pCxPrivate, validPaths, filePath)) return JS::NullValue(); JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue out(cx); pCxPrivate->pScriptInterface->ReadJSONFile(filePath, &out); return out; } void JSI_VFS::WriteJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath, JS::HandleValue val1) { JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); // TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON. JS::RootedValue val(cx, val1); std::string str(pCxPrivate->pScriptInterface->StringifyJSON(&val, false)); VfsPath path(filePath); WriteBuffer buf; buf.Append(str.c_str(), str.length()); g_VFS->CreateFile(path, buf.Data(), buf.Size()); } bool JSI_VFS::PathRestrictionMet(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filePath) { for (const CStrW& validPath : validPaths) if (filePath.find(validPath) == 0) return true; CStrW allowedPaths; for (std::size_t i = 0; i < validPaths.size(); ++i) { if (i != 0) allowedPaths += L", "; allowedPaths += L"\"" + validPaths[i] + L"\""; } - JS_ReportError( - pCxPrivate->pScriptInterface->GetContext(), - "This part of the engine may only read from %s!", - utf8_from_wstring(allowedPaths).c_str()); + JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); + JSAutoRequest rq(cx); + JS_ReportError(cx, "This part of the engine may only read from %s!", utf8_from_wstring(allowedPaths).c_str()); return false; } #define VFS_ScriptFunctions(context)\ JS::Value Script_ReadJSONFile_##context(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath)\ {\ return JSI_VFS::ReadJSONFile(pCxPrivate, PathRestriction_##context, filePath);\ }\ JS::Value Script_ListDirectoryFiles_##context(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& path, const std::wstring& filterStr, bool recurse)\ {\ return JSI_VFS::BuildDirEntList(pCxPrivate, PathRestriction_##context, path, filterStr, recurse);\ }\ bool Script_FileExists_##context(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath)\ {\ return JSI_VFS::FileExists(pCxPrivate, PathRestriction_##context, filePath);\ }\ VFS_ScriptFunctions(GUI); VFS_ScriptFunctions(Simulation); VFS_ScriptFunctions(Maps); #undef VFS_ScriptFunctions void JSI_VFS::RegisterScriptFunctions_GUI(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("ListDirectoryFiles"); scriptInterface.RegisterFunction("FileExists"); scriptInterface.RegisterFunction("GetFileMTime"); scriptInterface.RegisterFunction("GetFileSize"); scriptInterface.RegisterFunction("ReadFile"); scriptInterface.RegisterFunction("ReadFileLines"); scriptInterface.RegisterFunction("ReadJSONFile"); scriptInterface.RegisterFunction("WriteJSONFile"); } void JSI_VFS::RegisterScriptFunctions_Simulation(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("ListDirectoryFiles"); scriptInterface.RegisterFunction("FileExists"); scriptInterface.RegisterFunction("ReadJSONFile"); } void JSI_VFS::RegisterScriptFunctions_Maps(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("ListDirectoryFiles"); scriptInterface.RegisterFunction("FileExists"); scriptInterface.RegisterFunction("ReadJSONFile"); } Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 22948) +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 22949) @@ -1,1148 +1,1149 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ScriptInterface.h" #include "ScriptRuntime.h" #include "ScriptStats.h" #include "lib/debug.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/utf16string.h" #include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION #include #include #include #include #include #include #include #include "valgrind.h" #include "scriptinterface/ScriptExtraHeaders.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& runtime); ~ScriptInterface_impl(); void Register(const char* name, JSNative fptr, uint nargs) const; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the runtime destructor. shared_ptr m_runtime; JSContext* m_cx; JS::PersistentRootedObject m_glob; // global scope object JSCompartment* m_comp; boost::rand48* m_rng; JS::PersistentRootedObject m_nativeScope; // native function scope object }; namespace { JSClass global_class = { "global", JSCLASS_GLOBAL_FLAGS, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook }; void ErrorReporter(JSContext* cx, const char* message, JSErrorReport* report) { JSAutoRequest rq(cx); std::stringstream msg; bool isWarning = JSREPORT_IS_WARNING(report->flags); msg << (isWarning ? "JavaScript warning: " : "JavaScript error: "); if (report->filename) { msg << report->filename; msg << " line " << report->lineno << "\n"; } msg << message; // If there is an exception, then print its stack trace JS::RootedValue excn(cx); if (JS_GetPendingException(cx, &excn) && excn.isObject()) { JS::RootedValue stackVal(cx); JS::RootedObject excnObj(cx, &excn.toObject()); JS_GetProperty(cx, excnObj, "stack", &stackVal); std::string stackText; ScriptInterface::FromJSVal(cx, stackVal, stackText); std::istringstream stream(stackText); for (std::string line; std::getline(stream, line);) msg << "\n " << line; } if (isWarning) LOGWARNING("%s", msg.str().c_str()); else LOGERROR("%s", msg.str().c_str()); // When running under Valgrind, print more information in the error message // VALGRIND_PRINTF_BACKTRACE("->"); } // Functions in the global namespace: bool print(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); for (uint i = 0; i < args.length(); ++i) { std::wstring str; if (!ScriptInterface::FromJSVal(cx, 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; } std::wstring str; if (!ScriptInterface::FromJSVal(cx, 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; } std::wstring str; if (!ScriptInterface::FromJSVal(cx, 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; } std::wstring str; if (!ScriptInterface::FromJSVal(cx, args[0], str)) return false; LOGERROR("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool deepcopy(JSContext* cx, uint argc, JS::Value* vp) { JSAutoRequest rq(cx); JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } JS::RootedValue ret(cx); if (!JS_StructuredClone(cx, args[0], &ret, NULL, NULL)) return false; args.rval().set(ret); return true; } bool deepfreeze(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() != 1 || !args.get(0).isObject()) { + JSAutoRequest rq(cx); JS_ReportError(cx, "deepfreeze requires exactly one object as an argument."); return false; } ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->FreezeObject(args.get(0), true); args.rval().set(args.get(0)); return true; } bool ProfileStart(JSContext* cx, uint argc, JS::Value* vp) { const char* name = "(ProfileStart)"; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(cx, args[0], str)) return false; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; name = StringFlyweight(str).get().c_str(); } if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.StartScript(name); g_Profiler2.RecordRegionEnter(name); args.rval().setUndefined(); return true; } bool ProfileStop(JSContext* UNUSED(cx), uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave(); args.rval().setUndefined(); return true; } bool ProfileAttribute(JSContext* cx, uint argc, JS::Value* vp) { const char* name = "(ProfileAttribute)"; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(cx, args[0], str)) return false; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; name = StringFlyweight(str).get().c_str(); } g_Profiler2.RecordAttribute("%s", name); args.rval().setUndefined(); return true; } // 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& runtime) : m_runtime(runtime), m_glob(runtime->m_rt), m_nativeScope(runtime->m_rt) { bool ok; m_cx = JS_NewContext(m_runtime->m_rt, STACK_CHUNK_SIZE); ENSURE(m_cx); JS_SetOffthreadIonCompilationEnabled(m_runtime->m_rt, true); // For GC debugging: // JS_SetGCZeal(m_cx, 2, JS_DEFAULT_ZEAL_FREQ); JS_SetContextPrivate(m_cx, NULL); JS_SetErrorReporter(m_runtime->m_rt, ErrorReporter); JS_SetGlobalJitCompilerOption(m_runtime->m_rt, JSJITCOMPILER_ION_ENABLE, 1); JS_SetGlobalJitCompilerOption(m_runtime->m_rt, JSJITCOMPILER_BASELINE_ENABLE, 1); JS::RuntimeOptionsRef(m_cx) .setExtraWarnings(true) .setWerror(false) .setStrictMode(true); JS::CompartmentOptions opt; opt.setVersion(JSVERSION_LATEST); // Keep JIT code during non-shrinking GCs. This brings a quite big performance improvement. opt.setPreserveJitCode(true); JSAutoRequest rq(m_cx); JS::RootedObject globalRootedVal(m_cx, JS_NewGlobalObject(m_cx, &global_class, NULL, JS::OnNewGlobalHookOption::FireOnNewGlobalHook, opt)); m_comp = JS_EnterCompartment(m_cx, globalRootedVal); ok = JS_InitStandardClasses(m_cx, globalRootedVal); ENSURE(ok); m_glob = globalRootedVal.get(); JS_DefineProperty(m_cx, m_glob, "global", globalRootedVal, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, nullptr, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, globalRootedVal, "print", ::print, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, globalRootedVal, "log", ::logmsg, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, globalRootedVal, "warn", ::warn, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, globalRootedVal, "error", ::error, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, globalRootedVal, "clone", ::deepcopy, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, globalRootedVal, "deepfreeze", ::deepfreeze, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); Register("ProfileStart", ::ProfileStart, 1); Register("ProfileStop", ::ProfileStop, 0); Register("ProfileAttribute", ::ProfileAttribute, 1); runtime->RegisterContext(m_cx); } ScriptInterface_impl::~ScriptInterface_impl() { m_runtime->UnRegisterContext(m_cx); { JSAutoRequest rq(m_cx); JS_LeaveCompartment(m_cx, m_comp); } JS_DestroyContext(m_cx); } void ScriptInterface_impl::Register(const char* name, JSNative fptr, uint nargs) const { JSAutoRequest rq(m_cx); JS::RootedObject nativeScope(m_cx, m_nativeScope); JS::RootedFunction func(m_cx, JS_DefineFunction(m_cx, nativeScope, name, fptr, nargs, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); } ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& runtime) : m(new ScriptInterface_impl(nativeScopeName, runtime)) { // Profiler stats table isn't thread-safe, so only enable this on the main thread if (ThreadUtil::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Add(this, debugName); } m_CxPrivate.pScriptInterface = this; JS_SetContextPrivate(m->m_cx, (void*)&m_CxPrivate); } ScriptInterface::~ScriptInterface() { if (ThreadUtil::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Remove(this); } } void ScriptInterface::SetCallbackData(void* pCBData) { m_CxPrivate.pCBData = pCBData; } ScriptInterface::CxPrivate* ScriptInterface::GetScriptInterfaceAndCBData(JSContext* cx) { CxPrivate* pCxPrivate = (CxPrivate*)JS_GetContextPrivate(cx); return pCxPrivate; } 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) { JSAutoRequest rq(m->m_cx); JS::RootedValue math(m->m_cx); JS::RootedObject global(m->m_cx, m->m_glob); if (JS_GetProperty(m->m_cx, global, "Math", &math) && math.isObject()) { JS::RootedObject mathObj(m->m_cx, &math.toObject()); JS::RootedFunction random(m->m_cx, JS_DefineFunction(m->m_cx, mathObj, "random", Math_random, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); if (random) { m->m_rng = &rng; return true; } } LOGERROR("ReplaceNondeterministicRNG: failed to replace Math.random"); return false; } void ScriptInterface::Register(const char* name, JSNative fptr, size_t nargs) const { m->Register(name, fptr, (uint)nargs); } JSContext* ScriptInterface::GetContext() const { return m->m_cx; } JSRuntime* ScriptInterface::GetJSRuntime() const { return m->m_runtime->m_rt; } shared_ptr ScriptInterface::GetRuntime() const { return m->m_runtime; } void ScriptInterface::CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const { JSAutoRequest rq(m->m_cx); if (!ctor.isObject()) { LOGERROR("CallConstructor: ctor is not an object"); out.setNull(); return; } JS::RootedObject ctorObj(m->m_cx, &ctor.toObject()); out.setObjectOrNull(JS_New(m->m_cx, ctorObj, argv)); } void ScriptInterface::DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs) { JSAutoRequest rq(m->m_cx); 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(m->m_cx, m->m_glob); JS::RootedObject obj(m->m_cx, JS_InitClass(m->m_cx, global, nullptr, clasp, constructor, minArgs, // Constructor, min args ps, fs, // Properties, methods static_ps, static_fs)); // Constructor properties, methods if (obj == NULL) throw PSERROR_Scripting_DefineType_CreationFailed(); CustomType& type = m_CustomObjectTypes[typeName]; type.m_Prototype.init(m->m_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(); JS::RootedObject prototype(m->m_cx, it->second.m_Prototype.get()); return JS_NewObjectWithGivenProto(m->m_cx, it->second.m_Class, prototype); } bool ScriptInterface::CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) const { JSAutoRequest rq(m->m_cx); JS::RootedObject obj(m->m_cx); if (!JS_ValueToObject(m->m_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(m->m_cx, obj, name, &found) || !found) return false; bool ok = JS_CallFunctionName(m->m_cx, obj, name, argv, ret); return ok; } bool ScriptInterface::CreateObject_(JSContext* cx, JS::MutableHandleObject object) { // JSAutoRequest is the responsibility of the caller object.set(JS_NewPlainObject(cx)); if (!object) throw PSERROR_Scripting_CreateObjectFailed(); return true; } void ScriptInterface::CreateArray(JSContext* cx, JS::MutableHandleValue objectValue, size_t length) { JSAutoRequest rq(cx); objectValue.setObjectOrNull(JS_NewArrayObject(cx, length)); if (!objectValue.isObject()) throw PSERROR_Scripting_CreateObjectFailed(); } JS::Value ScriptInterface::GetGlobalObject() const { JSAutoRequest rq(m->m_cx); return JS::ObjectValue(*JS::CurrentGlobalOrNull(m->m_cx)); } bool ScriptInterface::SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate) { JSAutoRequest rq(m->m_cx); JS::RootedObject global(m->m_cx, m->m_glob); bool found; if (!JS_HasProperty(m->m_cx, global, name, &found)) return false; if (found) { JS::Rooted desc(m->m_cx); if (!JS_GetOwnPropertyDescriptor(m->m_cx, global, name, &desc)) return false; if (!desc.writable()) { if (!replace) { JS_ReportError(m->m_cx, "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()) { JS_ReportError(m->m_cx, "The global \"%s\" is permanent and cannot be hotloaded", name); return false; } LOGMESSAGE("Hotloading new value for global \"%s\".", name); ENSURE(JS_DeleteProperty(m->m_cx, global, name)); } } uint attrs = 0; if (constant) attrs |= JSPROP_READONLY; if (enumerate) attrs |= JSPROP_ENUMERATE; return JS_DefineProperty(m->m_cx, global, name, value, attrs); } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const { JSAutoRequest rq(m->m_cx); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(m->m_cx, &obj.toObject()); if (!JS_DefineProperty(m->m_cx, object, name, value, attrs)) return false; return true; } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const { JSAutoRequest rq(m->m_cx); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(m->m_cx, &obj.toObject()); utf16string name16(name, name + wcslen(name)); if (!JS_DefineUCProperty(m->m_cx, object, reinterpret_cast(name16.c_str()), name16.length(), value, attrs)) return false; return true; } bool ScriptInterface::SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const { JSAutoRequest rq(m->m_cx); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(m->m_cx, &obj.toObject()); JS::RootedId id(m->m_cx, INT_TO_JSID(name)); if (!JS_DefinePropertyById(m->m_cx, object, id, value, attrs)) return false; return true; } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const { return GetProperty_(obj, name, out); } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const { JSContext* cx = GetContext(); JSAutoRequest rq(cx); JS::RootedValue val(cx); if (!GetProperty_(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::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const { return GetPropertyInt_(obj, name, out); } bool ScriptInterface::GetProperty_(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const { JSAutoRequest rq(m->m_cx); if (!obj.isObject()) return false; JS::RootedObject object(m->m_cx, &obj.toObject()); if (!JS_GetProperty(m->m_cx, object, name, out)) return false; return true; } bool ScriptInterface::GetPropertyInt_(JS::HandleValue obj, int name, JS::MutableHandleValue out) const { JSAutoRequest rq(m->m_cx); JS::RootedId nameId(m->m_cx, INT_TO_JSID(name)); if (!obj.isObject()) return false; JS::RootedObject object(m->m_cx, &obj.toObject()); if (!JS_GetPropertyById(m->m_cx, object, nameId, out)) return false; return true; } bool ScriptInterface::HasProperty(JS::HandleValue obj, const char* name) const { // TODO: proper errorhandling JSAutoRequest rq(m->m_cx); if (!obj.isObject()) return false; JS::RootedObject object(m->m_cx, &obj.toObject()); bool found; if (!JS_HasProperty(m->m_cx, object, name, &found)) return false; return found; } bool ScriptInterface::EnumeratePropertyNamesWithPrefix(JS::HandleValue objVal, const char* prefix, std::vector& out) const { JSAutoRequest rq(m->m_cx); if (!objVal.isObjectOrNull()) { LOGERROR("EnumeratePropertyNamesWithPrefix expected object type!"); return false; } if (objVal.isNull()) return true; // reached the end of the prototype chain JS::RootedObject obj(m->m_cx, &objVal.toObject()); JS::Rooted props(m->m_cx, JS::IdVector(m->m_cx)); if (!JS_Enumerate(m->m_cx, obj, &props)) return false; for (size_t i = 0; i < props.length(); ++i) { JS::RootedId id(m->m_cx, props[i]); JS::RootedValue val(m->m_cx); if (!JS_IdToValue(m->m_cx, id, &val)) return false; if (!val.isString()) continue; // ignore integer properties JS::RootedString name(m->m_cx, val.toString()); size_t len = strlen(prefix)+1; std::vector buf(len); size_t prefixLen = strlen(prefix) * sizeof(char); JS_EncodeStringToBuffer(m->m_cx, name, &buf[0], prefixLen); buf[len-1]= '\0'; if (0 == strcmp(&buf[0], prefix)) { if (JS_StringHasLatin1Chars(name)) { size_t length; JS::AutoCheckCannotGC nogc; const JS::Latin1Char* chars = JS_GetLatin1StringCharsAndLength(m->m_cx, nogc, name, &length); if (chars) out.push_back(std::string(chars, chars+length)); } else { size_t length; JS::AutoCheckCannotGC nogc; const char16_t* chars = JS_GetTwoByteStringCharsAndLength(m->m_cx, nogc, name, &length); if (chars) out.push_back(std::string(chars, chars+length)); } } } // Recurse up the prototype chain JS::RootedObject prototype(m->m_cx); if (JS_GetPrototype(m->m_cx, obj, &prototype)) { JS::RootedValue prototypeVal(m->m_cx, JS::ObjectOrNullValue(prototype)); if (!EnumeratePropertyNamesWithPrefix(prototypeVal, prefix, out)) return false; } return true; } bool ScriptInterface::SetPrototype(JS::HandleValue objVal, JS::HandleValue protoVal) { JSAutoRequest rq(m->m_cx); if (!objVal.isObject() || !protoVal.isObject()) return false; JS::RootedObject obj(m->m_cx, &objVal.toObject()); JS::RootedObject proto(m->m_cx, &protoVal.toObject()); return JS_SetPrototype(m->m_cx, obj, proto); } bool ScriptInterface::FreezeObject(JS::HandleValue objVal, bool deep) const { JSAutoRequest rq(m->m_cx); if (!objVal.isObject()) return false; JS::RootedObject obj(m->m_cx, &objVal.toObject()); if (deep) return JS_DeepFreezeObject(m->m_cx, obj); else return JS_FreezeObject(m->m_cx, obj); } bool ScriptInterface::LoadScript(const VfsPath& filename, const std::string& code) const { JSAutoRequest rq(m->m_cx); JS::RootedObject global(m->m_cx, m->m_glob); utf16string codeUtf16(code.begin(), code.end()); 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 = filename.string8(); JS::CompileOptions options(m->m_cx); options.setFileAndLine(filenameStr.c_str(), lineNo); options.setIsRunOnce(false); JS::RootedFunction func(m->m_cx); JS::AutoObjectVector emptyScopeChain(m->m_cx); if (!JS::CompileFunction(m->m_cx, emptyScopeChain, options, NULL, 0, NULL, reinterpret_cast(codeUtf16.c_str()), (uint)(codeUtf16.length()), &func)) return false; JS::RootedValue rval(m->m_cx); return JS_CallFunction(m->m_cx, nullptr, func, JS::HandleValueArray::empty(), &rval); } shared_ptr ScriptInterface::CreateRuntime(shared_ptr parentRuntime, int runtimeSize, int heapGrowthBytesGCTrigger) { return shared_ptr(new ScriptRuntime(parentRuntime, runtimeSize, heapGrowthBytesGCTrigger)); } bool ScriptInterface::LoadGlobalScript(const VfsPath& filename, const std::wstring& code) const { JSAutoRequest rq(m->m_cx); utf16string codeUtf16(code.begin(), code.end()); 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 = filename.string8(); JS::RootedValue rval(m->m_cx); JS::CompileOptions opts(m->m_cx); opts.setFileAndLine(filenameStr.c_str(), lineNo); return JS::Evaluate(m->m_cx, opts, reinterpret_cast(codeUtf16.c_str()), (uint)(codeUtf16.length()), &rval); } bool ScriptInterface::LoadGlobalScriptFile(const VfsPath& path) const { JSAutoRequest rq(m->m_cx); 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; } std::wstring code = wstring_from_utf8(file.DecodeUTF8()); // assume it's UTF-8 utf16string codeUtf16(code.begin(), code.end()); 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(m->m_cx); JS::CompileOptions opts(m->m_cx); opts.setFileAndLine(filenameStr.c_str(), lineNo); return JS::Evaluate(m->m_cx, opts, reinterpret_cast(codeUtf16.c_str()), (uint)(codeUtf16.length()), &rval); } bool ScriptInterface::Eval(const char* code) const { JSAutoRequest rq(m->m_cx); JS::RootedValue rval(m->m_cx); return Eval_(code, &rval); } bool ScriptInterface::Eval_(const char* code, JS::MutableHandleValue rval) const { JSAutoRequest rq(m->m_cx); utf16string codeUtf16(code, code+strlen(code)); JS::CompileOptions opts(m->m_cx); opts.setFileAndLine("(eval)", 1); return JS::Evaluate(m->m_cx, opts, reinterpret_cast(codeUtf16.c_str()), (uint)codeUtf16.length(), rval); } bool ScriptInterface::Eval_(const wchar_t* code, JS::MutableHandleValue rval) const { JSAutoRequest rq(m->m_cx); utf16string codeUtf16(code, code+wcslen(code)); JS::CompileOptions opts(m->m_cx); opts.setFileAndLine("(eval)", 1); return JS::Evaluate(m->m_cx, opts, reinterpret_cast(codeUtf16.c_str()), (uint)codeUtf16.length(), rval); } bool ScriptInterface::ParseJSON(const std::string& string_utf8, JS::MutableHandleValue out) const { JSAutoRequest rq(m->m_cx); std::wstring attrsW = wstring_from_utf8(string_utf8); utf16string string(attrsW.begin(), attrsW.end()); if (JS_ParseJSON(m->m_cx, reinterpret_cast(string.c_str()), (u32)string.size(), out)) return true; LOGERROR("JS_ParseJSON failed!"); if (!JS_IsExceptionPending(m->m_cx)) return false; JS::RootedValue exc(m->m_cx); if (!JS_GetPendingException(m->m_cx, &exc)) return false; JS_ClearPendingException(m->m_cx); // We expect an object of type SyntaxError if (!exc.isObject()) return false; JS::RootedValue rval(m->m_cx); JS::RootedObject excObj(m->m_cx, &exc.toObject()); if (!JS_CallFunctionName(m->m_cx, excObj, "toString", JS::HandleValueArray::empty(), &rval)) return false; std::wstring error; ScriptInterface::FromJSVal(m->m_cx, rval, error); LOGERROR("%s", utf8_from_wstring(error)); 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 { JSAutoRequest rq(m->m_cx); Stringifier str; JS::RootedValue indentVal(m->m_cx, indent ? JS::Int32Value(2) : JS::UndefinedValue()); if (!JS_Stringify(m->m_cx, obj, nullptr, indentVal, &Stringifier::callback, &str)) { JS_ClearPendingException(m->m_cx); LOGERROR("StringifyJSON failed"); return std::string(); } return str.stream.str(); } std::string ScriptInterface::ToString(JS::MutableHandleValue obj, bool pretty) const { JSAutoRequest rq(m->m_cx); 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(m->m_cx, JS::Int32Value(2)); // Temporary disable the error reporter, so we don't print complaints about cyclic values JSErrorReporter er = JS_SetErrorReporter(m->m_runtime->m_rt, NULL); bool ok = JS_Stringify(m->m_cx, obj, nullptr, indentVal, &Stringifier::callback, &str); // Restore error reporter JS_SetErrorReporter(m->m_runtime->m_rt, er); if (ok) return str.stream.str(); // Clear the exception set when Stringify failed JS_ClearPendingException(m->m_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); return utf8_from_wstring(source); } void ScriptInterface::ReportError(const char* msg) const { JSAutoRequest rq(m->m_cx); // JS_ReportError by itself doesn't seem to set a JS-style exception, and so // script callers will be unable to catch anything. So use JS_SetPendingException // to make sure there really is a script-level exception. But just set it to undefined // because there's not much value yet in throwing a real exception object. JS_SetPendingException(m->m_cx, JS::UndefinedHandleValue); // And report the actual error JS_ReportError(m->m_cx, "%s", msg); // TODO: Why doesn't JS_ReportPendingException(m->m_cx); work? } bool ScriptInterface::IsExceptionPending(JSContext* cx) { JSAutoRequest rq(cx); return JS_IsExceptionPending(cx) ? true : false; } JS::Value ScriptInterface::CloneValueFromOtherContext(const ScriptInterface& otherContext, JS::HandleValue val) const { PROFILE("CloneValueFromOtherContext"); JSAutoRequest rq(m->m_cx); JS::RootedValue out(m->m_cx); shared_ptr structuredClone = otherContext.WriteStructuredClone(val); ReadStructuredClone(structuredClone, &out); return out.get(); } ScriptInterface::StructuredClone::StructuredClone() : m_Data(NULL), m_Size(0) { } ScriptInterface::StructuredClone::~StructuredClone() { if (m_Data) JS_ClearStructuredClone(m_Data, m_Size, NULL, NULL); } shared_ptr ScriptInterface::WriteStructuredClone(JS::HandleValue v) const { JSAutoRequest rq(m->m_cx); u64* data = NULL; size_t nbytes = 0; if (!JS_WriteStructuredClone(m->m_cx, v, &data, &nbytes, NULL, NULL, JS::UndefinedHandleValue)) { debug_warn(L"Writing a structured clone with JS_WriteStructuredClone failed!"); return shared_ptr(); } shared_ptr ret(new StructuredClone); ret->m_Data = data; ret->m_Size = nbytes; return ret; } void ScriptInterface::ReadStructuredClone(const shared_ptr& ptr, JS::MutableHandleValue ret) const { JSAutoRequest rq(m->m_cx); JS_ReadStructuredClone(m->m_cx, ptr->m_Data, ptr->m_Size, JS_STRUCTURED_CLONE_VERSION, ret, NULL, NULL); } Index: ps/trunk/source/simulation2/scripting/EngineScriptConversions.cpp =================================================================== --- ps/trunk/source/simulation2/scripting/EngineScriptConversions.cpp (revision 22948) +++ ps/trunk/source/simulation2/scripting/EngineScriptConversions.cpp (revision 22949) @@ -1,343 +1,346 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "scriptinterface/ScriptConversions.h" #include "graphics/Color.h" #include "maths/Fixed.h" #include "maths/FixedVector2D.h" #include "maths/FixedVector3D.h" #include "ps/CLogger.h" #include "ps/Shapes.h" #include "ps/utf16string.h" #include "simulation2/helpers/CinemaPath.h" #include "simulation2/helpers/Grid.h" #include "simulation2/system/IComponent.h" #include "simulation2/system/ParamNode.h" #define FAIL(msg) STMT(JS_ReportError(cx, msg); return false) #define FAIL_VOID(msg) STMT(JS_ReportError(cx, msg); return) template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, IComponent* const& val) { JSAutoRequest rq(cx); if (val == NULL) { ret.setNull(); return; } // If this is a scripted component, just return the JS object directly JS::RootedValue instance(cx, val->GetJSInstance()); if (!instance.isNull()) { ret.set(instance); return; } // Otherwise we need to construct a wrapper object // (TODO: cache wrapper objects?) JS::RootedObject obj(cx); if (!val->NewJSObject(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface, &obj)) { // Report as an error, since scripts really shouldn't try to use unscriptable interfaces LOGERROR("IComponent does not have a scriptable interface"); ret.setUndefined(); return; } JS_SetPrivate(obj, static_cast(val)); ret.setObject(*obj); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, CParamNode const& val) { JSAutoRequest rq(cx); val.ToJSVal(cx, true, ret); // Prevent modifications to the object, so that it's safe to share between // components and to reconstruct on deserialization if (ret.isObject()) { JS::RootedObject obj(cx, &ret.toObject()); JS_DeepFreezeObject(cx, obj); } } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CParamNode* const& val) { if (val) ToJSVal(cx, ret, *val); else ret.setUndefined(); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CColor& out) { + JSAutoRequest rq(cx); + if (!v.isObject()) FAIL("JS::HandleValue not an object"); - JSAutoRequest rq(cx); JS::RootedObject obj(cx, &v.toObject()); JS::RootedValue r(cx); JS::RootedValue g(cx); JS::RootedValue b(cx); JS::RootedValue a(cx); if (!JS_GetProperty(cx, obj, "r", &r) || !FromJSVal(cx, r, out.r)) FAIL("Failed to get property CColor.r"); if (!JS_GetProperty(cx, obj, "g", &g) || !FromJSVal(cx, g, out.g)) FAIL("Failed to get property CColor.g"); if (!JS_GetProperty(cx, obj, "b", &b) || !FromJSVal(cx, b, out.b)) FAIL("Failed to get property CColor.b"); if (!JS_GetProperty(cx, obj, "a", &a) || !FromJSVal(cx, a, out.a)) FAIL("Failed to get property CColor.a"); return true; } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, CColor const& val) { CreateObject( cx, ret, "r", val.r, "g", val.g, "b", val.b, "a", val.a); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, fixed& out) { JSAutoRequest rq(cx); double ret; if (!JS::ToNumber(cx, v, &ret)) return false; out = fixed::FromDouble(ret); // double can precisely represent the full range of fixed, so this is a non-lossy conversion return true; } template<> void ScriptInterface::ToJSVal(JSContext* UNUSED(cx), JS::MutableHandleValue ret, const fixed& val) { ret.set(JS::NumberValue(val.ToDouble())); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CFixedVector3D& out) { if (!v.isObject()) return false; // TODO: report type error JSAutoRequest rq(cx); JS::RootedObject obj(cx, &v.toObject()); JS::RootedValue p(cx); if (!JS_GetProperty(cx, obj, "x", &p)) return false; // TODO: report type errors if (!FromJSVal(cx, p, out.X)) return false; if (!JS_GetProperty(cx, obj, "y", &p)) return false; if (!FromJSVal(cx, p, out.Y)) return false; if (!JS_GetProperty(cx, obj, "z", &p)) return false; if (!FromJSVal(cx, p, out.Z)) return false; return true; } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CFixedVector3D& val) { JSAutoRequest rq(cx); ScriptInterface::CxPrivate* pCxPrivate = ScriptInterface::GetScriptInterfaceAndCBData(cx); JS::RootedObject global(cx, &pCxPrivate->pScriptInterface->GetGlobalObject().toObject()); JS::RootedValue valueVector3D(cx); if (!JS_GetProperty(cx, global, "Vector3D", &valueVector3D)) FAIL_VOID("Failed to get Vector3D constructor"); JS::AutoValueArray<3> args(cx); args[0].setNumber(val.X.ToDouble()); args[1].setNumber(val.Y.ToDouble()); args[2].setNumber(val.Z.ToDouble()); if (!JS::Construct(cx, valueVector3D, args, ret)) FAIL_VOID("Failed to construct Vector3D object"); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CFixedVector2D& out) { JSAutoRequest rq(cx); if (!v.isObject()) return false; // TODO: report type error JS::RootedObject obj(cx, &v.toObject()); JS::RootedValue p(cx); if (!JS_GetProperty(cx, obj, "x", &p)) return false; // TODO: report type errors if (!FromJSVal(cx, p, out.X)) return false; if (!JS_GetProperty(cx, obj, "y", &p)) return false; if (!FromJSVal(cx, p, out.Y)) return false; return true; } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CFixedVector2D& val) { JSAutoRequest rq(cx); ScriptInterface::CxPrivate* pCxPrivate = ScriptInterface::GetScriptInterfaceAndCBData(cx); JS::RootedObject global(cx, &pCxPrivate->pScriptInterface->GetGlobalObject().toObject()); JS::RootedValue valueVector2D(cx); if (!JS_GetProperty(cx, global, "Vector2D", &valueVector2D)) FAIL_VOID("Failed to get Vector2D constructor"); JS::AutoValueArray<2> args(cx); args[0].setNumber(val.X.ToDouble()); args[1].setNumber(val.Y.ToDouble()); if (!JS::Construct(cx, valueVector2D, args, ret)) FAIL_VOID("Failed to construct Vector2D object"); } template<> void ScriptInterface::ToJSVal >(JSContext* cx, JS::MutableHandleValue ret, const Grid& val) { JSAutoRequest rq(cx); u32 length = (u32)(val.m_W * val.m_H); u32 nbytes = (u32)(length * sizeof(u8)); JS::RootedObject objArr(cx, JS_NewUint8Array(cx, length)); // Copy the array data and then remove the no-GC check to allow further changes to the JS data { JS::AutoCheckCannotGC nogc; bool sharedMemory; memcpy((void*)JS_GetUint8ArrayData(objArr, &sharedMemory, nogc), val.m_Data, nbytes); } JS::RootedValue data(cx, JS::ObjectValue(*objArr)); CreateObject( cx, ret, "width", val.m_W, "height", val.m_H, "data", data); } template<> void ScriptInterface::ToJSVal >(JSContext* cx, JS::MutableHandleValue ret, const Grid& val) { JSAutoRequest rq(cx); u32 length = (u32)(val.m_W * val.m_H); u32 nbytes = (u32)(length * sizeof(u16)); JS::RootedObject objArr(cx, JS_NewUint16Array(cx, length)); // Copy the array data and then remove the no-GC check to allow further changes to the JS data { JS::AutoCheckCannotGC nogc; bool sharedMemory; memcpy((void*)JS_GetUint16ArrayData(objArr, &sharedMemory, nogc), val.m_Data, nbytes); } JS::RootedValue data(cx, JS::ObjectValue(*objArr)); CreateObject( cx, ret, "width", val.m_W, "height", val.m_H, "data", data); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, TNSpline& out) { + JSAutoRequest rq(cx); + if (!v.isObject()) FAIL("Argument must be an object"); - JSAutoRequest rq(cx); JS::RootedObject obj(cx, &v.toObject()); bool isArray; if (!JS_IsArrayObject(cx, obj, &isArray) || !isArray) FAIL("Argument must be an array"); u32 numberOfNodes = 0; if (!JS_GetArrayLength(cx, obj, &numberOfNodes)) FAIL("Failed to get array length"); for (u32 i = 0; i < numberOfNodes; ++i) { JS::RootedValue node(cx); if (!JS_GetElement(cx, obj, i, &node)) FAIL("Failed to read array element"); fixed deltaTime; if (!FromJSProperty(cx, node, "deltaTime", deltaTime)) FAIL("Failed to read Spline.deltaTime property"); CFixedVector3D position; if (!FromJSProperty(cx, node, "position", position)) FAIL("Failed to read Spline.position property"); out.AddNode(position, CFixedVector3D(), deltaTime); } if (out.GetAllNodes().empty()) FAIL("Spline must contain at least one node"); return true; } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CCinemaPath& out) { + JSAutoRequest rq(cx); + if (!v.isObject()) FAIL("Argument must be an object"); - JSAutoRequest rq(cx); JS::RootedObject obj(cx, &v.toObject()); CCinemaData pathData; TNSpline positionSpline, targetSpline; if (!FromJSProperty(cx, v, "name", pathData.m_Name)) FAIL("Failed to get CCinemaPath.name property"); if (!FromJSProperty(cx, v, "orientation", pathData.m_Orientation)) FAIL("Failed to get CCinemaPath.orientation property"); if (!FromJSProperty(cx, v, "positionNodes", positionSpline)) FAIL("Failed to get CCinemaPath.positionNodes property"); if (pathData.m_Orientation == L"target" && !FromJSProperty(cx, v, "targetNodes", targetSpline)) FAIL("Failed to get CCinemaPath.targetNodes property"); // Other properties are not necessary to be defined if (!FromJSProperty(cx, v, "timescale", pathData.m_Timescale)) pathData.m_Timescale = fixed::FromInt(1); if (!FromJSProperty(cx, v, "mode", pathData.m_Mode)) pathData.m_Mode = L"ease_inout"; if (!FromJSProperty(cx, v, "style", pathData.m_Style)) pathData.m_Style = L"default"; out = CCinemaPath(pathData, positionSpline, targetSpline); return true; } // define vectors JSVAL_VECTOR(CFixedVector2D) #undef FAIL #undef FAIL_VOID