Index: ps/trunk/source/gui/Scripting/ScriptFunctions.cpp =================================================================== --- ps/trunk/source/gui/Scripting/ScriptFunctions.cpp (revision 24968) +++ ps/trunk/source/gui/Scripting/ScriptFunctions.cpp (revision 24969) @@ -1,74 +1,76 @@ -/* Copyright (C) 2019 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 "ScriptFunctions.h" #include "graphics/scripting/JSInterface_GameView.h" #include "gui/Scripting/JSInterface_GUIManager.h" #include "gui/Scripting/JSInterface_GUISize.h" #include "i18n/scripting/JSInterface_L10n.h" #include "lobby/scripting/JSInterface_Lobby.h" #include "network/scripting/JSInterface_Network.h" #include "ps/scripting/JSInterface_ConfigDB.h" #include "ps/scripting/JSInterface_Console.h" #include "ps/scripting/JSInterface_Debug.h" #include "ps/scripting/JSInterface_Game.h" #include "ps/scripting/JSInterface_Hotkey.h" #include "ps/scripting/JSInterface_Main.h" #include "ps/scripting/JSInterface_Mod.h" #include "ps/scripting/JSInterface_ModIo.h" #include "ps/scripting/JSInterface_SavedGame.h" #include "ps/scripting/JSInterface_UserReport.h" #include "ps/scripting/JSInterface_VFS.h" #include "ps/scripting/JSInterface_VisualReplay.h" #include "renderer/scripting/JSInterface_Renderer.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/scripting/JSInterface_Simulation.h" #include "soundmanager/scripting/JSInterface_Sound.h" /* * This file defines a set of functions that are available to GUI scripts, to allow * interaction with the rest of the engine. * Functions are exposed to scripts within the global object 'Engine', so * scripts should call "Engine.FunctionName(...)" etc. */ void GuiScriptingInit(ScriptInterface& scriptInterface) { + ScriptRequest rq(scriptInterface); + JSI_GUISize::RegisterScriptClass(scriptInterface); JSI_ConfigDB::RegisterScriptFunctions(scriptInterface); - JSI_Console::RegisterScriptFunctions(scriptInterface); + JSI_Console::RegisterScriptFunctions(rq); JSI_Debug::RegisterScriptFunctions(scriptInterface); JSI_GUIManager::RegisterScriptFunctions(scriptInterface); JSI_Game::RegisterScriptFunctions(scriptInterface); JSI_GameView::RegisterScriptFunctions(scriptInterface); JSI_Hotkey::RegisterScriptFunctions(scriptInterface); JSI_L10n::RegisterScriptFunctions(scriptInterface); JSI_Lobby::RegisterScriptFunctions(scriptInterface); JSI_Main::RegisterScriptFunctions(scriptInterface); JSI_Mod::RegisterScriptFunctions(scriptInterface); JSI_ModIo::RegisterScriptFunctions(scriptInterface); JSI_Network::RegisterScriptFunctions(scriptInterface); JSI_Renderer::RegisterScriptFunctions(scriptInterface); JSI_SavedGame::RegisterScriptFunctions(scriptInterface); JSI_Simulation::RegisterScriptFunctions(scriptInterface); JSI_Sound::RegisterScriptFunctions(scriptInterface); JSI_UserReport::RegisterScriptFunctions(scriptInterface); JSI_VFS::RegisterScriptFunctions_GUI(scriptInterface); JSI_VisualReplay::RegisterScriptFunctions(scriptInterface); } Index: ps/trunk/source/ps/CConsole.h =================================================================== --- ps/trunk/source/ps/CConsole.h (revision 24968) +++ ps/trunk/source/ps/CConsole.h (revision 24969) @@ -1,144 +1,144 @@ -/* Copyright (C) 2019 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 . */ /* * Implements the in-game console with scripting support. */ #ifndef INCLUDED_CCONSOLE #define INCLUDED_CCONSOLE #include #include #include #include #include #include "graphics/ShaderProgramPtr.h" #include "lib/file/vfs/vfs_path.h" #include "lib/input.h" #include "ps/CStr.h" class CTextRenderer; #define CONSOLE_BUFFER_SIZE 1024 // for text being typed into the console #define CONSOLE_MESSAGE_SIZE 1024 // for messages being printed into the console #define CONSOLE_FONT "mono-10" /** * In-game console. * * Thread-safety: * - Expected to be constructed/destructed in the main thread. * - InsertMessage may be called from any thread while the object is alive. */ class CConsole { NONCOPYABLE(CConsole); public: CConsole(); ~CConsole(); void SetSize(float X = 300, float Y = 0, float W = 800, float H = 600); void UpdateScreenSize(int w, int h); void ToggleVisible(); void SetVisible(bool visible); void SetCursorBlinkRate(double rate); /** * @param deltaRealTime Elapsed real time since the last frame. */ void Update(const float deltaRealTime); void Render(); void InsertChar(const int szChar, const wchar_t cooked); void InsertMessage(const std::string& message); void SetBuffer(const wchar_t* szMessage); void UseHistoryFile(const VfsPath& filename, int historysize); // Only returns a pointer to the buffer; copy out of here if you want to keep it. const wchar_t* GetBuffer(); void FlushBuffer(); - bool IsActive() { return m_bVisible; } + bool IsActive() const { return m_bVisible; } int m_iFontHeight; int m_iFontWidth; int m_iFontOffset; // distance to move up before drawing size_t m_charsPerPage; private: // Lock for all state modified by InsertMessage std::mutex m_Mutex; float m_fX; float m_fY; float m_fHeight; float m_fWidth; // "position" in show/hide animation, how visible the console is (0..1). // allows implementing other animations than sliding, e.g. fading in/out. float m_fVisibleFrac; std::deque m_deqMsgHistory; // protected by m_Mutex std::deque m_deqBufHistory; int m_iMsgHistPos; wchar_t* m_szBuffer; int m_iBufferPos; int m_iBufferLength; VfsPath m_sHistoryFile; int m_MaxHistoryLines; bool m_bVisible; // console is to be drawn bool m_bToggle; // show/hide animation is currently active double m_prevTime; // the previous time the cursor draw state changed (used for blinking cursor) bool m_bCursorVisState; // if the cursor should be drawn or not double m_cursorBlinkRate; // cursor blink rate in seconds, if greater than 0.0 void DrawWindow(CShaderProgramPtr& shader); void DrawHistory(CTextRenderer& textRenderer); void DrawBuffer(CTextRenderer& textRenderer); void DrawCursor(CTextRenderer& textRenderer); bool IsEOB() { return (m_iBufferPos == m_iBufferLength); } // Is end of Buffer? bool IsBOB() { return (m_iBufferPos == 0); } // Is beginning of Buffer? bool IsFull() { return (m_iBufferLength == CONSOLE_BUFFER_SIZE); } bool IsEmpty() { return (m_iBufferLength == 0); } void ProcessBuffer(const wchar_t* szLine); void LoadHistory(); void SaveHistory(); }; extern CConsole* g_Console; extern InReaction conInputHandler(const SDL_Event_* ev); #endif Index: ps/trunk/source/ps/GameSetup/HWDetect.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/HWDetect.cpp (revision 24968) +++ ps/trunk/source/ps/GameSetup/HWDetect.cpp (revision 24969) @@ -1,655 +1,655 @@ /* 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 "scriptinterface/ScriptInterface.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" // 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(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), bool disabled) +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); - scriptInterface.RegisterFunction("SetDisableAudio"); + 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); #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); } static void ReportSDL(const ScriptInterface& scriptInterface, JS::HandleValue settings) { SDL_version build, runtime; SDL_VERSION(&build); char version[16]; snprintf(version, ARRAY_SIZE(version), "%d.%d.%d", build.major, build.minor, build.patch); scriptInterface.SetProperty(settings, "sdl_build_version", version); SDL_GetVersion(&runtime); snprintf(version, ARRAY_SIZE(version), "%d.%d.%d", runtime.major, runtime.minor, runtime.patch); scriptInterface.SetProperty(settings, "sdl_runtime_version", version); // This is null in atlas (and further the call triggers an assertion). const char* backend = g_VideoMode.GetWindow() ? GetSDLSubsystem(g_VideoMode.GetWindow()) : "none"; scriptInterface.SetProperty(settings, "sdl_video_backend", backend ? backend : "unknown"); } static void ReportGLLimits(const ScriptInterface& scriptInterface, JS::HandleValue settings) { const char* errstr = "(error)"; #define INTEGER(id) do { \ GLint i = -1; \ glGetIntegerv(GL_##id, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_" #id, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_" #id, i); \ } while (false) #define INTEGER2(id) do { \ GLint i[2] = { -1, -1 }; \ glGetIntegerv(GL_##id, i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) { \ scriptInterface.SetProperty(settings, "GL_" #id "[0]", errstr); \ scriptInterface.SetProperty(settings, "GL_" #id "[1]", errstr); \ } else { \ scriptInterface.SetProperty(settings, "GL_" #id "[0]", i[0]); \ scriptInterface.SetProperty(settings, "GL_" #id "[1]", i[1]); \ } \ } while (false) #define FLOAT(id) do { \ GLfloat f = std::numeric_limits::quiet_NaN(); \ glGetFloatv(GL_##id, &f); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_" #id, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_" #id, f); \ } while (false) #define FLOAT2(id) do { \ GLfloat f[2] = { std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN() }; \ glGetFloatv(GL_##id, f); \ if (ogl_SquelchError(GL_INVALID_ENUM)) { \ scriptInterface.SetProperty(settings, "GL_" #id "[0]", errstr); \ scriptInterface.SetProperty(settings, "GL_" #id "[1]", errstr); \ } else { \ scriptInterface.SetProperty(settings, "GL_" #id "[0]", f[0]); \ scriptInterface.SetProperty(settings, "GL_" #id "[1]", f[1]); \ } \ } while (false) #define STRING(id) do { \ const char* c = (const char*)glGetString(GL_##id); \ if (!c) c = ""; \ if (ogl_SquelchError(GL_INVALID_ENUM)) c = errstr; \ scriptInterface.SetProperty(settings, "GL_" #id, std::string(c)); \ } while (false) #define QUERY(target, pname) do { \ GLint i = -1; \ pglGetQueryivARB(GL_##target, GL_##pname, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_" #target ".GL_" #pname, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_" #target ".GL_" #pname, i); \ } while (false) #define VERTEXPROGRAM(id) do { \ GLint i = -1; \ pglGetProgramivARB(GL_VERTEX_PROGRAM_ARB, GL_##id, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_VERTEX_PROGRAM_ARB.GL_" #id, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_VERTEX_PROGRAM_ARB.GL_" #id, i); \ } while (false) #define FRAGMENTPROGRAM(id) do { \ GLint i = -1; \ pglGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_##id, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_FRAGMENT_PROGRAM_ARB.GL_" #id, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_FRAGMENT_PROGRAM_ARB.GL_" #id, i); \ } while (false) #define BOOL(id) INTEGER(id) ogl_WarnIfError(); // Core OpenGL 1.3: // (We don't bother checking extension strings for anything older than 1.3; // it'll just produce harmless warnings) STRING(VERSION); STRING(VENDOR); STRING(RENDERER); STRING(EXTENSIONS); #if !CONFIG2_GLES INTEGER(MAX_LIGHTS); INTEGER(MAX_CLIP_PLANES); // Skip MAX_COLOR_MATRIX_STACK_DEPTH (only in imaging subset) INTEGER(MAX_MODELVIEW_STACK_DEPTH); INTEGER(MAX_PROJECTION_STACK_DEPTH); INTEGER(MAX_TEXTURE_STACK_DEPTH); #endif INTEGER(SUBPIXEL_BITS); #if !CONFIG2_GLES INTEGER(MAX_3D_TEXTURE_SIZE); #endif INTEGER(MAX_TEXTURE_SIZE); INTEGER(MAX_CUBE_MAP_TEXTURE_SIZE); #if !CONFIG2_GLES INTEGER(MAX_PIXEL_MAP_TABLE); INTEGER(MAX_NAME_STACK_DEPTH); INTEGER(MAX_LIST_NESTING); INTEGER(MAX_EVAL_ORDER); #endif INTEGER2(MAX_VIEWPORT_DIMS); #if !CONFIG2_GLES INTEGER(MAX_ATTRIB_STACK_DEPTH); INTEGER(MAX_CLIENT_ATTRIB_STACK_DEPTH); INTEGER(AUX_BUFFERS); BOOL(RGBA_MODE); BOOL(INDEX_MODE); BOOL(DOUBLEBUFFER); BOOL(STEREO); #endif FLOAT2(ALIASED_POINT_SIZE_RANGE); #if !CONFIG2_GLES FLOAT2(SMOOTH_POINT_SIZE_RANGE); FLOAT(SMOOTH_POINT_SIZE_GRANULARITY); #endif FLOAT2(ALIASED_LINE_WIDTH_RANGE); #if !CONFIG2_GLES FLOAT2(SMOOTH_LINE_WIDTH_RANGE); FLOAT(SMOOTH_LINE_WIDTH_GRANULARITY); // Skip MAX_CONVOLUTION_WIDTH, MAX_CONVOLUTION_HEIGHT (only in imaging subset) INTEGER(MAX_ELEMENTS_INDICES); INTEGER(MAX_ELEMENTS_VERTICES); INTEGER(MAX_TEXTURE_UNITS); #endif INTEGER(SAMPLE_BUFFERS); INTEGER(SAMPLES); // TODO: compressed texture formats INTEGER(RED_BITS); INTEGER(GREEN_BITS); INTEGER(BLUE_BITS); INTEGER(ALPHA_BITS); #if !CONFIG2_GLES INTEGER(INDEX_BITS); #endif INTEGER(DEPTH_BITS); INTEGER(STENCIL_BITS); #if !CONFIG2_GLES INTEGER(ACCUM_RED_BITS); INTEGER(ACCUM_GREEN_BITS); INTEGER(ACCUM_BLUE_BITS); INTEGER(ACCUM_ALPHA_BITS); #endif #if !CONFIG2_GLES // Core OpenGL 2.0 (treated as extensions): if (ogl_HaveExtension("GL_EXT_texture_lod_bias")) { FLOAT(MAX_TEXTURE_LOD_BIAS_EXT); } if (ogl_HaveExtension("GL_ARB_occlusion_query")) { QUERY(SAMPLES_PASSED, QUERY_COUNTER_BITS); } if (ogl_HaveExtension("GL_ARB_shading_language_100")) { STRING(SHADING_LANGUAGE_VERSION_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_shader")) { INTEGER(MAX_VERTEX_ATTRIBS_ARB); INTEGER(MAX_VERTEX_UNIFORM_COMPONENTS_ARB); INTEGER(MAX_VARYING_FLOATS_ARB); INTEGER(MAX_COMBINED_TEXTURE_IMAGE_UNITS_ARB); INTEGER(MAX_VERTEX_TEXTURE_IMAGE_UNITS_ARB); } if (ogl_HaveExtension("GL_ARB_fragment_shader")) { INTEGER(MAX_FRAGMENT_UNIFORM_COMPONENTS_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_shader") || ogl_HaveExtension("GL_ARB_fragment_shader") || ogl_HaveExtension("GL_ARB_vertex_program") || ogl_HaveExtension("GL_ARB_fragment_program")) { INTEGER(MAX_TEXTURE_IMAGE_UNITS_ARB); INTEGER(MAX_TEXTURE_COORDS_ARB); } if (ogl_HaveExtension("GL_ARB_draw_buffers")) { INTEGER(MAX_DRAW_BUFFERS_ARB); } // Core OpenGL 3.0: if (ogl_HaveExtension("GL_EXT_gpu_shader4")) { INTEGER(MIN_PROGRAM_TEXEL_OFFSET); // no _EXT version of these in glext.h INTEGER(MAX_PROGRAM_TEXEL_OFFSET); } if (ogl_HaveExtension("GL_EXT_framebuffer_object")) { INTEGER(MAX_COLOR_ATTACHMENTS_EXT); INTEGER(MAX_RENDERBUFFER_SIZE_EXT); } if (ogl_HaveExtension("GL_EXT_framebuffer_multisample")) { INTEGER(MAX_SAMPLES_EXT); } if (ogl_HaveExtension("GL_EXT_texture_array")) { INTEGER(MAX_ARRAY_TEXTURE_LAYERS_EXT); } if (ogl_HaveExtension("GL_EXT_transform_feedback")) { INTEGER(MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS_EXT); INTEGER(MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS_EXT); INTEGER(MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS_EXT); } // Other interesting extensions: if (ogl_HaveExtension("GL_EXT_timer_query") || ogl_HaveExtension("GL_ARB_timer_query")) { QUERY(TIME_ELAPSED, QUERY_COUNTER_BITS); } if (ogl_HaveExtension("GL_ARB_timer_query")) { QUERY(TIMESTAMP, QUERY_COUNTER_BITS); } if (ogl_HaveExtension("GL_EXT_texture_filter_anisotropic")) { FLOAT(MAX_TEXTURE_MAX_ANISOTROPY_EXT); } if (ogl_HaveExtension("GL_ARB_texture_rectangle")) { INTEGER(MAX_RECTANGLE_TEXTURE_SIZE_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_program") || ogl_HaveExtension("GL_ARB_fragment_program")) { INTEGER(MAX_PROGRAM_MATRICES_ARB); INTEGER(MAX_PROGRAM_MATRIX_STACK_DEPTH_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_program")) { VERTEXPROGRAM(MAX_PROGRAM_ENV_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_LOCAL_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_TEMPORARIES_ARB); VERTEXPROGRAM(MAX_PROGRAM_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_ATTRIBS_ARB); VERTEXPROGRAM(MAX_PROGRAM_ADDRESS_REGISTERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEMPORARIES_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ATTRIBS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB); if (ogl_HaveExtension("GL_ARB_fragment_program")) { // The spec seems to say these should be supported, but // Mesa complains about them so let's not bother /* VERTEXPROGRAM(MAX_PROGRAM_ALU_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_TEX_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_TEX_INDIRECTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ALU_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEX_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEX_INDIRECTIONS_ARB); */ } } if (ogl_HaveExtension("GL_ARB_fragment_program")) { FRAGMENTPROGRAM(MAX_PROGRAM_ENV_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_LOCAL_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_ALU_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_TEX_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_TEX_INDIRECTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_TEMPORARIES_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_ATTRIBS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ALU_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEX_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEX_INDIRECTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEMPORARIES_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ATTRIBS_ARB); if (ogl_HaveExtension("GL_ARB_vertex_program")) { // The spec seems to say these should be supported, but // Intel drivers on Windows complain about them so let's not bother /* FRAGMENTPROGRAM(MAX_PROGRAM_ADDRESS_REGISTERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB); */ } } if (ogl_HaveExtension("GL_ARB_geometry_shader4")) { INTEGER(MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB); INTEGER(MAX_GEOMETRY_OUTPUT_VERTICES_ARB); INTEGER(MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS_ARB); INTEGER(MAX_GEOMETRY_UNIFORM_COMPONENTS_ARB); INTEGER(MAX_GEOMETRY_VARYING_COMPONENTS_ARB); INTEGER(MAX_VERTEX_VARYING_COMPONENTS_ARB); } #else // CONFIG2_GLES // Core OpenGL ES 2.0: STRING(SHADING_LANGUAGE_VERSION); INTEGER(MAX_VERTEX_ATTRIBS); INTEGER(MAX_VERTEX_UNIFORM_VECTORS); INTEGER(MAX_VARYING_VECTORS); INTEGER(MAX_COMBINED_TEXTURE_IMAGE_UNITS); INTEGER(MAX_VERTEX_TEXTURE_IMAGE_UNITS); INTEGER(MAX_FRAGMENT_UNIFORM_VECTORS); INTEGER(MAX_TEXTURE_IMAGE_UNITS); INTEGER(MAX_RENDERBUFFER_SIZE); #endif // CONFIG2_GLES // TODO: Support OpenGL platforms which don't use GLX as well. #if defined(SDL_VIDEO_DRIVER_X11) && !CONFIG2_GLES #define GLXQCR_INTEGER(id) do { \ unsigned int i = UINT_MAX; \ if (pglXQueryCurrentRendererIntegerMESA(id, &i)) \ scriptInterface.SetProperty(settings, #id, i); \ } while (false) #define GLXQCR_INTEGER2(id) do { \ unsigned int i[2] = { UINT_MAX, UINT_MAX }; \ if (pglXQueryCurrentRendererIntegerMESA(id, i)) { \ scriptInterface.SetProperty(settings, #id "[0]", i[0]); \ scriptInterface.SetProperty(settings, #id "[1]", i[1]); \ } \ } while (false) #define GLXQCR_INTEGER3(id) do { \ unsigned int i[3] = { UINT_MAX, UINT_MAX, UINT_MAX }; \ if (pglXQueryCurrentRendererIntegerMESA(id, i)) { \ scriptInterface.SetProperty(settings, #id "[0]", i[0]); \ scriptInterface.SetProperty(settings, #id "[1]", i[1]); \ scriptInterface.SetProperty(settings, #id "[2]", i[2]); \ } \ } while (false) #define GLXQCR_STRING(id) do { \ const char* str = pglXQueryCurrentRendererStringMESA(id); \ if (str) \ scriptInterface.SetProperty(settings, #id ".string", str); \ } while (false) SDL_SysWMinfo wminfo; SDL_VERSION(&wminfo.version); const int ret = SDL_GetWindowWMInfo(g_VideoMode.GetWindow(), &wminfo); if (ret && wminfo.subsystem == SDL_SYSWM_X11) { Display* dpy = wminfo.info.x11.display; int scrnum = DefaultScreen(dpy); const char* glxexts = glXQueryExtensionsString(dpy, scrnum); scriptInterface.SetProperty(settings, "glx_extensions", glxexts); if (strstr(glxexts, "GLX_MESA_query_renderer") && pglXQueryCurrentRendererIntegerMESA && pglXQueryCurrentRendererStringMESA) { GLXQCR_INTEGER(GLX_RENDERER_VENDOR_ID_MESA); GLXQCR_INTEGER(GLX_RENDERER_DEVICE_ID_MESA); GLXQCR_INTEGER3(GLX_RENDERER_VERSION_MESA); GLXQCR_INTEGER(GLX_RENDERER_ACCELERATED_MESA); GLXQCR_INTEGER(GLX_RENDERER_VIDEO_MEMORY_MESA); GLXQCR_INTEGER(GLX_RENDERER_UNIFIED_MEMORY_ARCHITECTURE_MESA); GLXQCR_INTEGER(GLX_RENDERER_PREFERRED_PROFILE_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_CORE_PROFILE_VERSION_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_COMPATIBILITY_PROFILE_VERSION_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_ES_PROFILE_VERSION_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_ES2_PROFILE_VERSION_MESA); GLXQCR_STRING(GLX_RENDERER_VENDOR_ID_MESA); GLXQCR_STRING(GLX_RENDERER_DEVICE_ID_MESA); } } #endif // SDL_VIDEO_DRIVER_X11 } Index: ps/trunk/source/ps/scripting/JSInterface_Console.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Console.cpp (revision 24968) +++ ps/trunk/source/ps/scripting/JSInterface_Console.cpp (revision 24969) @@ -1,54 +1,43 @@ -/* Copyright (C) 2018 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 "JSInterface_Console.h" #include "ps/CConsole.h" #include "ps/CLogger.h" -#include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/FunctionWrapper.h" -bool JSI_Console::CheckGlobalInitialized() +namespace +{ +CConsole* ConsoleGetter(const ScriptRequest&, JS::CallArgs&) { if (!g_Console) { LOGERROR("Trying to access the console when it's not initialized!"); - return false; + return nullptr; } - return true; -} - -bool JSI_Console::GetVisibleEnabled(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - if (!CheckGlobalInitialized()) - return false; - return g_Console->IsActive(); + return g_Console; } - -void JSI_Console::SetVisibleEnabled(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), bool Enabled) -{ - if (!CheckGlobalInitialized()) - return; - g_Console->SetVisible(Enabled); } -void JSI_Console::RegisterScriptFunctions(const ScriptInterface& scriptInterface) +void JSI_Console::RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("Console_GetVisibleEnabled"); - scriptInterface.RegisterFunction("Console_SetVisibleEnabled"); + ScriptFunction::Register<&CConsole::IsActive, ConsoleGetter>(rq, "Console_GetVisibleEnabled"); + ScriptFunction::Register<&CConsole::SetVisible, ConsoleGetter>(rq, "Console_SetVisibleEnabled"); } Index: ps/trunk/source/ps/scripting/JSInterface_Console.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Console.h (revision 24968) +++ ps/trunk/source/ps/scripting/JSInterface_Console.h (revision 24969) @@ -1,32 +1,28 @@ -/* Copyright (C) 2018 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 . */ #ifndef INCLUDED_JSI_CONSOLE #define INCLUDED_JSI_CONSOLE -#include "scriptinterface/ScriptInterface.h" +class ScriptRequest; namespace JSI_Console { - bool CheckGlobalInitialized(); - bool GetVisibleEnabled(ScriptInterface::CmptPrivate* pCmptPrivate); - void SetVisibleEnabled(ScriptInterface::CmptPrivate* pCmptPrivate, bool Enabled); - - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_CONSOLE Index: ps/trunk/source/ps/scripting/JSInterface_Hotkey.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Hotkey.cpp (revision 24968) +++ ps/trunk/source/ps/scripting/JSInterface_Hotkey.cpp (revision 24969) @@ -1,174 +1,171 @@ /* 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 "JSInterface_Hotkey.h" #include "lib/external_libraries/libsdl.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Hotkey.h" #include "ps/KeyName.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptConversions.h" #include #include #include /** * Convert an unordered map to a JS object, mapping keys to values. * Assumes T to have a c_str() method that returns a const char* * NB: this is unordered since no particular effort is made to preserve order. * TODO: this could be moved to ScriptConversions.cpp if the need arises. */ template static void ToJSVal_unordered_map(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::unordered_map& val) { JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx)); if (!obj) { ret.setUndefined(); return; } for (const std::pair& item : val) { JS::RootedValue el(rq.cx); ScriptInterface::ToJSVal(rq, &el, item.second); JS_SetProperty(rq.cx, obj, item.first.c_str(), el); } ret.setObject(*obj); } template<> void ScriptInterface::ToJSVal>>>(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::unordered_map>>& val) { ToJSVal_unordered_map(rq, ret, val); } template<> void ScriptInterface::ToJSVal>(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::unordered_map& val) { ToJSVal_unordered_map(rq, ret, val); } +namespace +{ /** * @return a (js) object mapping hotkey name (from cfg files) to a list ofscancode names */ -JS::Value GetHotkeyMap(ScriptInterface::CmptPrivate* pCmptPrivate) +JS::Value GetHotkeyMap(const ScriptRequest& rq) { - ScriptRequest rq(*pCmptPrivate->pScriptInterface); - JS::RootedValue hotkeyMap(rq.cx); std::unordered_map>> hotkeys; for (const std::pair& key : g_HotkeyMap) for (const SHotkeyMapping& mapping : key.second) { std::vector keymap; if (key.first != UNUSED_HOTKEY_CODE) keymap.push_back(FindScancodeName(static_cast(key.first))); for (const SKey& secondary_key : mapping.requires) keymap.push_back(FindScancodeName(static_cast(secondary_key.code))); // If keymap is empty (== unused) or size 1, push the combination. // Otherwise, all permutations of the combination will exist, so pick one using an arbitrary order. if (keymap.size() < 2 || keymap[0] < keymap[1]) hotkeys[mapping.name].emplace_back(keymap); } - pCmptPrivate->pScriptInterface->ToJSVal(rq, &hotkeyMap, hotkeys); + ScriptInterface::ToJSVal(rq, &hotkeyMap, hotkeys); return hotkeyMap; } /** * @return a (js) object mapping scancode names to their locale-dependent name. */ -JS::Value GetScancodeKeyNames(ScriptInterface::CmptPrivate* pCmptPrivate) +JS::Value GetScancodeKeyNames(const ScriptRequest& rq) { - ScriptRequest rq(*pCmptPrivate->pScriptInterface); - JS::RootedValue obj(rq.cx); std::unordered_map map; // Get the name of all scancodes. // This is slightly wasteful but should be fine overall, they are dense. for (int i = 0; i < MOUSE_LAST; ++i) map[FindScancodeName(static_cast(i))] = FindKeyName(static_cast(i)); - pCmptPrivate->pScriptInterface->ToJSVal(rq, &obj, map); + ScriptInterface::ToJSVal(rq, &obj, map); return obj; } -void ReloadHotkeys(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +void ReloadHotkeys() { UnloadHotkeys(); LoadHotkeys(g_ConfigDB); } -JS::Value GetConflicts(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue combination) +JS::Value GetConflicts(const ScriptRequest& rq, JS::HandleValue combination) { - ScriptInterface* scriptInterface = pCmptPrivate->pScriptInterface; - ScriptRequest rq(*scriptInterface); - std::vector keys; - if (!scriptInterface->FromJSVal(rq, combination, keys)) + if (!ScriptInterface::FromJSVal(rq, combination, keys)) { LOGERROR("Invalid hotkey combination"); return JS::NullValue(); } if (keys.empty()) return JS::NullValue(); // Pick a random code as a starting point of the hotkeys (they are all equivalent). SDL_Scancode_ startCode = FindScancode(keys.back()); std::unordered_map::const_iterator it = g_HotkeyMap.find(startCode); if (it == g_HotkeyMap.end()) return JS::NullValue(); // Create a sorted vector with the remaining keys. keys.pop_back(); std::set codes; for (const std::string& key : keys) codes.insert(SKey{ FindScancode(key) }); std::vector conflicts; // This isn't very efficient, but we shouldn't iterate too many hotkeys // since we at least have one matching key. for (const SHotkeyMapping& keymap : it->second) { std::set match(keymap.requires.begin(), keymap.requires.end()); if (codes == match) conflicts.emplace_back(keymap.name); } if (conflicts.empty()) return JS::NullValue(); JS::RootedValue ret(rq.cx); - scriptInterface->ToJSVal(rq, &ret, conflicts); + ScriptInterface::ToJSVal(rq, &ret, conflicts); return ret; } +} -void JSI_Hotkey::RegisterScriptFunctions(const ScriptInterface& scriptInterface) +void JSI_Hotkey::RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("GetHotkeyMap"); - scriptInterface.RegisterFunction("GetScancodeKeyNames"); - scriptInterface.RegisterFunction("ReloadHotkeys"); - scriptInterface.RegisterFunction("GetConflicts"); + ScriptFunction::Register<&GetHotkeyMap>(rq, "GetHotkeyMap"); + ScriptFunction::Register<&GetScancodeKeyNames>(rq, "GetScancodeKeyNames"); + ScriptFunction::Register<&ReloadHotkeys>(rq, "ReloadHotkeys"); + ScriptFunction::Register<&GetConflicts>(rq, "GetConflicts"); } Index: ps/trunk/source/ps/scripting/JSInterface_Hotkey.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Hotkey.h (revision 24968) +++ ps/trunk/source/ps/scripting/JSInterface_Hotkey.h (revision 24969) @@ -1,28 +1,28 @@ -/* 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 . */ #ifndef INCLUDED_JSI_HOTKEY #define INCLUDED_JSI_HOTKEY -#include "scriptinterface/ScriptInterface.h" +class ScriptRequest; namespace JSI_Hotkey { - void RegisterScriptFunctions(const ScriptInterface& ScriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_HOTKEY Index: ps/trunk/source/ps/scripting/JSInterface_Mod.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Mod.cpp (revision 24968) +++ ps/trunk/source/ps/scripting/JSInterface_Mod.cpp (revision 24969) @@ -1,63 +1,62 @@ -/* Copyright (C) 2018 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 "JSInterface_Mod.h" #include "ps/Mod.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" extern void RestartEngine(); -JS::Value JSI_Mod::GetEngineInfo(ScriptInterface::CmptPrivate* pCmptPrivate) +namespace +{ +JS::Value GetEngineInfo(ScriptInterface::CmptPrivate* pCmptPrivate) { return Mod::GetEngineInfo(*(pCmptPrivate->pScriptInterface)); } /** * Returns a JS object containing a listing of available mods that * have a modname.json file in their modname folder. The returned * object looks like { modname1: json1, modname2: json2, ... } where * jsonN is the content of the modnameN/modnameN.json file as a JS * object. * * @return JS object with available mods as the keys of the modname.json * properties. */ -JS::Value JSI_Mod::GetAvailableMods(ScriptInterface::CmptPrivate* pCmptPrivate) +JS::Value GetAvailableMods(ScriptInterface::CmptPrivate* pCmptPrivate) { return Mod::GetAvailableMods(*(pCmptPrivate->pScriptInterface)); } -void JSI_Mod::RestartEngine(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - ::RestartEngine(); -} - -void JSI_Mod::SetMods(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::vector& mods) +void SetMods(const std::vector& mods) { g_modsLoaded = mods; } +} -void JSI_Mod::RegisterScriptFunctions(const ScriptInterface& scriptInterface) +void JSI_Mod::RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("GetEngineInfo"); - scriptInterface.RegisterFunction("GetAvailableMods"); - scriptInterface.RegisterFunction("RestartEngine"); - scriptInterface.RegisterFunction, &JSI_Mod::SetMods>("SetMods"); + ScriptFunction::Register<&GetEngineInfo>(rq, "GetEngineInfo"); + ScriptFunction::Register<&GetAvailableMods>(rq, "GetAvailableMods"); + ScriptFunction::Register<&RestartEngine>(rq, "RestartEngine"); + ScriptFunction::Register<&SetMods>(rq, "SetMods"); } Index: ps/trunk/source/ps/scripting/JSInterface_Mod.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Mod.h (revision 24968) +++ ps/trunk/source/ps/scripting/JSInterface_Mod.h (revision 24969) @@ -1,34 +1,28 @@ -/* Copyright (C) 2018 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 . */ #ifndef INCLUDED_JSI_MOD #define INCLUDED_JSI_MOD -#include "ps/CStr.h" -#include "scriptinterface/ScriptInterface.h" +class ScriptRequest; namespace JSI_Mod { - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); - - JS::Value GetEngineInfo(ScriptInterface::CmptPrivate* pCmptPrivate); - JS::Value GetAvailableMods(ScriptInterface::CmptPrivate* pCmptPrivate); - void RestartEngine(ScriptInterface::CmptPrivate* pCmptPrivate); - void SetMods(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& mods); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_MOD Index: ps/trunk/source/scriptinterface/FunctionWrapper.h =================================================================== --- ps/trunk/source/scriptinterface/FunctionWrapper.h (nonexistent) +++ ps/trunk/source/scriptinterface/FunctionWrapper.h (revision 24969) @@ -0,0 +1,276 @@ +/* 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) + * Exceptions are: + * - const ScriptRequest& (as the first argument only, for implementation simplicity). + * - JS::HandleValue + */ + template + using type_transform = std::conditional_t, const ScriptRequest&, + 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) + { + // 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)); + } + + /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + + /** + * 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)); + } + + /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// +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); + +// GCC 7 triggers spurious warnings +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Waddress" + ObjType* obj = nullptr; + if constexpr (thisGetter != nullptr) + { + obj = thisGetter(rq, args); + if (!obj) + return false; + } +#pragma GCC diagnostic pop + + 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); + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * 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 Property changes on: ps/trunk/source/scriptinterface/FunctionWrapper.h ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24968) +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24969) @@ -1,1056 +1,1056 @@ /* 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 "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(); 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 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) + 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; } bool deepcopy(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); \ JS::RootedValue ret(cx); if (!JS_StructuredClone(rq.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); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() != 1 || !args.get(0).isObject()) { ScriptException::Raise(rq, "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); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(rq, 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() && Threading::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() && Threading::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); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(rq, 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& 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); m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, nullptr, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); 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); JS_DefineFunction(m_cx, m_glob, "clone", ::deepcopy, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "deepfreeze", ::deepfreeze, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); Register("ProfileStart", ::ProfileStart, 1); Register("ProfileStop", ::ProfileStop, 0); Register("ProfileAttribute", ::ProfileAttribute, 1); m_context->RegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } ScriptInterface_impl::~ScriptInterface_impl() { m_context->UnRegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } void ScriptInterface_impl::Register(const char* name, JSNative fptr, uint nargs) const { JSAutoRealm autoRealm(m_cx, m_glob); 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& 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; } void ScriptInterface::Register(const char* name, JSNative fptr, size_t nargs) const { m->Register(name, fptr, (uint)nargs); } 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); 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 24968) +++ ps/trunk/source/scriptinterface/ScriptInterface.h (revision 24969) @@ -1,632 +1,633 @@ -/* 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 . */ #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, RegisterFunctionFailed); 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 RegisterFunction) 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; }; void Register(const char* name, JSNative fptr, size_t nargs) const; // 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 // void RegisterFunction(const char* functionName) const; // // 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_FunctionWrapper.h =================================================================== --- ps/trunk/source/scriptinterface/tests/test_FunctionWrapper.h (nonexistent) +++ ps/trunk/source/scriptinterface/tests/test_FunctionWrapper.h (revision 24969) @@ -0,0 +1,107 @@ +/* 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" + +class TestFunctionWrapper : public CxxTest::TestSuite +{ +public: + + // TODO C++20: use lambda functions directly, names are 'N params, void/returns'. + static void _1p_v(int) {}; + static void _3p_v(int, bool, std::string) {}; + static int _3p_r(int a, bool, std::string) { return a; }; + + static void _0p_v() {}; + static int _0p_r() { return 1; }; + + void test_simple_wrappers() + { + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + } + + static void _handle(JS::HandleValue) {}; + static void _handle_2(int, JS::HandleValue, bool) {}; + + static void _cmpt_private(ScriptInterface::CmptPrivate*) {}; + static int _cmpt_private_2(ScriptInterface::CmptPrivate*, int a, bool) { return a; }; + + static void _script_request(const ScriptRequest&) {}; + static int _script_request_2(const ScriptRequest&, int a, bool) { return a; }; + + void test_special_wrappers() + { + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + } + + class test_method + { + public: + void method_1() {}; + int method_2(int, const int&) { return 4; }; + void const_method_1() const {}; + int const_method_2(int, const int&) const { return 4; }; + }; + + void test_method_wrappers() + { + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + static_assert(std::is_same_v), JSNative>); + } + + void test_calling() + { + ScriptInterface script("Test", "Test", g_ScriptContext); + ScriptRequest rq(script); + + ScriptFunction::Register<&TestFunctionWrapper::_1p_v>(script, "_1p_v"); + { + std::string input = "Test._1p_v(0);"; + JS::RootedValue val(rq.cx); + TS_ASSERT(script.Eval(input.c_str(), &val)); + } + + ScriptFunction::Register<&TestFunctionWrapper::_3p_r>(script, "_3p_r"); + { + std::string input = "Test._3p_r(4, false, 'test');"; + int ret = 0; + TS_ASSERT(script.Eval(input.c_str(), ret)); + TS_ASSERT_EQUALS(ret, 4); + } + + ScriptFunction::Register<&TestFunctionWrapper::_cmpt_private_2>(script, "_cmpt_private_2"); + { + std::string input = "Test._cmpt_private_2(4);"; + int ret = 0; + TS_ASSERT(script.Eval(input.c_str(), ret)); + TS_ASSERT_EQUALS(ret, 4); + } + } +}; Property changes on: ps/trunk/source/scriptinterface/tests/test_FunctionWrapper.h ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/simulation2/components/tests/test_scripts.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 24968) +++ ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 24969) @@ -1,131 +1,133 @@ -/* 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 "simulation2/system/ComponentTest.h" #include "simulation2/serialization/StdDeserializer.h" #include "simulation2/serialization/StdSerializer.h" #include "ps/Filesystem.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" class TestComponentScripts : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST); g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); } static void load_script(const ScriptInterface& scriptInterface, const VfsPath& pathname) { CVFSFile file; TS_ASSERT_EQUALS(file.Load(g_VFS, pathname), PSRETURN_OK); CStr content = file.DecodeUTF8(); // assume it's UTF-8 TSM_ASSERT(L"Running script "+pathname.string(), scriptInterface.LoadScript(pathname, content)); } static void Script_LoadComponentScript(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/components") / pathname)); } static void Script_LoadHelperScript(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname)); } static JS::Value Script_SerializationRoundTrip(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue value) { ScriptInterface& scriptInterface = *(pCmptPrivate->pScriptInterface); ScriptRequest rq(scriptInterface); JS::RootedValue val(rq.cx); val = value; std::stringstream stream; CStdSerializer serializer(scriptInterface, stream); serializer.ScriptVal("", &val); CStdDeserializer deserializer(scriptInterface, stream); deserializer.ScriptVal("", &val); return val; } void test_global_scripts() { if (!VfsDirectoryExists(L"globalscripts/tests/")) { debug_printf("Skipping globalscripts tests (can't find binaries/data/mods/public/globalscripts/tests/)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"globalscripts/tests/", L"test_*.js", paths)); for (const VfsPath& path : paths) { CSimContext context; CComponentManager componentManager(context, g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); componentManager.GetScriptInterface().RegisterFunction ("SerializationRoundTrip"); load_script(componentManager.GetScriptInterface(), path); } } void test_scripts() { if (!VfsFileExists(L"simulation/components/tests/setup.js")) { debug_printf("Skipping component scripts tests (can't find binaries/data/mods/public/simulation/components/tests/setup.js)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/components/tests/", L"test_*.js", paths)); TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/helpers/tests/", L"test_*.js", paths)); paths.push_back(VfsPath(L"simulation/components/tests/setup_test.js")); for (const VfsPath& path : paths) { // Clean up previous scripts. g_ScriptContext->ShrinkingGC(); CSimContext context; CComponentManager componentManager(context, g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); - componentManager.GetScriptInterface().RegisterFunction ("LoadComponentScript"); - componentManager.GetScriptInterface().RegisterFunction ("LoadHelperScript"); - componentManager.GetScriptInterface().RegisterFunction ("SerializationRoundTrip"); + ScriptRequest rq(componentManager.GetScriptInterface()); + ScriptFunction::Register(rq, "LoadComponentScript"); + ScriptFunction::Register(rq, "LoadHelperScript"); + ScriptFunction::Register(rq, "SerializationRoundTrip"); componentManager.LoadComponentTypes(); load_script(componentManager.GetScriptInterface(), L"simulation/components/tests/setup.js"); load_script(componentManager.GetScriptInterface(), path); } } }; Index: ps/trunk/source/simulation2/system/ComponentManager.cpp =================================================================== --- ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 24968) +++ ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 24969) @@ -1,1181 +1,1138 @@ -/* 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 "ComponentManager.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_VFS.h" +#include "scriptinterface/FunctionWrapper.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/DynamicSubscription.h" #include "simulation2/system/IComponent.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" /** * Used for script-only message types. */ class CMessageScripted : public CMessage { public: virtual int GetType() const { return mtid; } virtual const char* GetScriptHandlerName() const { return handlerName.c_str(); } virtual const char* GetScriptGlobalHandlerName() const { return globalHandlerName.c_str(); } virtual JS::Value ToJSVal(const ScriptInterface& UNUSED(scriptInterface)) const { return msg.get(); } CMessageScripted(const ScriptInterface& scriptInterface, int mtid, const std::string& name, JS::HandleValue msg) : mtid(mtid), handlerName("On" + name), globalHandlerName("OnGlobal" + name), msg(scriptInterface.GetGeneralJSContext(), msg) { } int mtid; std::string handlerName; std::string globalHandlerName; JS::PersistentRootedValue msg; }; CComponentManager::CComponentManager(CSimContext& context, shared_ptr cx, bool skipScriptFunctions) : m_NextScriptComponentTypeId(CID__LastNative), m_ScriptInterface("Engine", "Simulation", cx), m_SimContext(context), m_CurrentlyHotloading(false) { context.SetComponentManager(this); m_ScriptInterface.SetCallbackData(static_cast (this)); m_ScriptInterface.ReplaceNondeterministicRNG(m_RNG); // For component script tests, the test system sets up its own scripted implementation of // these functions, so we skip registering them here in those cases if (!skipScriptFunctions) { JSI_VFS::RegisterScriptFunctions_Simulation(m_ScriptInterface); - m_ScriptInterface.RegisterFunction ("RegisterComponentType"); - m_ScriptInterface.RegisterFunction ("RegisterSystemComponentType"); - m_ScriptInterface.RegisterFunction ("ReRegisterComponentType"); - m_ScriptInterface.RegisterFunction ("RegisterInterface"); - m_ScriptInterface.RegisterFunction ("RegisterMessageType"); - m_ScriptInterface.RegisterFunction ("RegisterGlobal"); - m_ScriptInterface.RegisterFunction ("QueryInterface"); - m_ScriptInterface.RegisterFunction, int, CComponentManager::Script_GetEntitiesWithInterface> ("GetEntitiesWithInterface"); - m_ScriptInterface.RegisterFunction, int, CComponentManager::Script_GetComponentsWithInterface> ("GetComponentsWithInterface"); - m_ScriptInterface.RegisterFunction ("PostMessage"); - m_ScriptInterface.RegisterFunction ("BroadcastMessage"); - m_ScriptInterface.RegisterFunction ("AddEntity"); - m_ScriptInterface.RegisterFunction ("AddLocalEntity"); - m_ScriptInterface.RegisterFunction ("DestroyEntity"); - m_ScriptInterface.RegisterFunction ("FlushDestroyedEntities"); + ScriptRequest rq(m_ScriptInterface); + constexpr ScriptFunction::ObjectGetter Getter = &ScriptFunction::ObjectFromCBData; + ScriptFunction::Register<&CComponentManager::Script_RegisterComponentType, Getter>(rq, "RegisterComponentType"); + ScriptFunction::Register<&CComponentManager::Script_RegisterSystemComponentType, Getter>(rq, "RegisterSystemComponentType"); + ScriptFunction::Register<&CComponentManager::Script_ReRegisterComponentType, Getter>(rq, "ReRegisterComponentType"); + ScriptFunction::Register<&CComponentManager::Script_RegisterInterface, Getter>(rq, "RegisterInterface"); + ScriptFunction::Register<&CComponentManager::Script_RegisterMessageType, Getter>(rq, "RegisterMessageType"); + ScriptFunction::Register<&CComponentManager::Script_RegisterGlobal, Getter>(rq, "RegisterGlobal"); + ScriptFunction::Register<&CComponentManager::Script_GetEntitiesWithInterface, Getter>(rq, "GetEntitiesWithInterface"); + ScriptFunction::Register<&CComponentManager::Script_GetComponentsWithInterface, Getter>(rq, "GetComponentsWithInterface"); + ScriptFunction::Register<&CComponentManager::Script_PostMessage, Getter>(rq, "PostMessage"); + ScriptFunction::Register<&CComponentManager::Script_BroadcastMessage, Getter>(rq, "BroadcastMessage"); + ScriptFunction::Register<&CComponentManager::Script_AddEntity, Getter>(rq, "AddEntity"); + ScriptFunction::Register<&CComponentManager::Script_AddLocalEntity, Getter>(rq, "AddLocalEntity"); + ScriptFunction::Register<&CComponentManager::QueryInterface, Getter>(rq, "QueryInterface"); + ScriptFunction::Register<&CComponentManager::DestroyComponentsSoon, Getter>(rq, "DestroyEntity"); + ScriptFunction::Register<&CComponentManager::FlushDestroyedComponents, Getter>(rq, "FlushDestroyedEntities"); } // Globalscripts may use VFS script functions m_ScriptInterface.LoadGlobalScripts(); // Define MT_*, IID_* as script globals, and store their names #define MESSAGE(name) m_ScriptInterface.SetGlobal("MT_" #name, (int)MT_##name); #define INTERFACE(name) \ m_ScriptInterface.SetGlobal("IID_" #name, (int)IID_##name); \ m_InterfaceIdsByName[#name] = IID_##name; #define COMPONENT(name) #include "simulation2/TypeList.h" #undef MESSAGE #undef INTERFACE #undef COMPONENT m_ScriptInterface.SetGlobal("INVALID_ENTITY", (int)INVALID_ENTITY); m_ScriptInterface.SetGlobal("INVALID_PLAYER", (int)INVALID_PLAYER); m_ScriptInterface.SetGlobal("SYSTEM_ENTITY", (int)SYSTEM_ENTITY); m_ComponentsByInterface.resize(IID__LastNative); ResetState(); } CComponentManager::~CComponentManager() { ResetState(); } void CComponentManager::LoadComponentTypes() { #define MESSAGE(name) \ RegisterMessageType(MT_##name, #name); #define INTERFACE(name) \ extern void RegisterComponentInterface_##name(ScriptInterface&); \ RegisterComponentInterface_##name(m_ScriptInterface); #define COMPONENT(name) \ extern void RegisterComponentType_##name(CComponentManager&); \ m_CurrentComponent = CID_##name; \ RegisterComponentType_##name(*this); #include "simulation2/TypeList.h" m_CurrentComponent = CID__Invalid; #undef MESSAGE #undef INTERFACE #undef COMPONENT } bool CComponentManager::LoadScript(const VfsPath& filename, bool hotload) { m_CurrentlyHotloading = hotload; CVFSFile file; PSRETURN loadOk = file.Load(g_VFS, filename); if (loadOk != PSRETURN_OK) // VFS will log the failed file and the reason return false; std::string content = file.DecodeUTF8(); // assume it's UTF-8 bool ok = m_ScriptInterface.LoadScript(filename, content); m_CurrentlyHotloading = false; return ok; } -void CComponentManager::Script_RegisterComponentType_Common(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor, bool reRegister, bool systemComponent) +void CComponentManager::Script_RegisterComponentType_Common(int iid, const std::string& cname, JS::HandleValue ctor, bool reRegister, bool systemComponent) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - ScriptRequest rq(componentManager->m_ScriptInterface); + ScriptRequest rq(m_ScriptInterface); // Find the C++ component that wraps the interface - int cidWrapper = componentManager->GetScriptWrapper(iid); + int cidWrapper = GetScriptWrapper(iid); if (cidWrapper == CID__Invalid) { ScriptException::Raise(rq, "Invalid interface id"); return; } - const ComponentType& ctWrapper = componentManager->m_ComponentTypesById[cidWrapper]; + const ComponentType& ctWrapper = m_ComponentTypesById[cidWrapper]; bool mustReloadComponents = false; // for hotloading - ComponentTypeId cid = componentManager->LookupCID(cname); + ComponentTypeId cid = LookupCID(cname); if (cid == CID__Invalid) { if (reRegister) { ScriptException::Raise(rq, "ReRegistering component type that was not registered before '%s'", cname.c_str()); return; } // Allocate a new cid number - cid = componentManager->m_NextScriptComponentTypeId++; - componentManager->m_ComponentTypeIdsByName[cname] = cid; + cid = m_NextScriptComponentTypeId++; + m_ComponentTypeIdsByName[cname] = cid; if (systemComponent) - componentManager->MarkScriptedComponentForSystemEntity(cid); + MarkScriptedComponentForSystemEntity(cid); } else { // Component type is already loaded, so do hotloading: - if (!componentManager->m_CurrentlyHotloading && !reRegister) + if (!m_CurrentlyHotloading && !reRegister) { ScriptException::Raise(rq, "Registering component type with already-registered name '%s'", cname.c_str()); return; } - const ComponentType& ctPrevious = componentManager->m_ComponentTypesById[cid]; + const ComponentType& ctPrevious = m_ComponentTypesById[cid]; // We can only replace scripted component types, not native ones if (ctPrevious.type != CT_Script) { ScriptException::Raise(rq, "Loading script component type with same name '%s' as native component", cname.c_str()); return; } // We don't support changing the IID of a component type (it would require fiddling // around with m_ComponentsByInterface and being careful to guarantee uniqueness per entity) if (ctPrevious.iid != iid) { // ...though it only matters if any components exist with this type - if (!componentManager->m_ComponentsByTypeId[cid].empty()) + if (!m_ComponentsByTypeId[cid].empty()) { ScriptException::Raise(rq, "Hotloading script component type mustn't change interface ID"); return; } } // Remove the old component type's message subscriptions std::map >::iterator it; - for (it = componentManager->m_LocalMessageSubscriptions.begin(); it != componentManager->m_LocalMessageSubscriptions.end(); ++it) + for (it = m_LocalMessageSubscriptions.begin(); it != m_LocalMessageSubscriptions.end(); ++it) { std::vector& types = it->second; std::vector::iterator ctit = find(types.begin(), types.end(), cid); if (ctit != types.end()) types.erase(ctit); } - for (it = componentManager->m_GlobalMessageSubscriptions.begin(); it != componentManager->m_GlobalMessageSubscriptions.end(); ++it) + for (it = m_GlobalMessageSubscriptions.begin(); it != m_GlobalMessageSubscriptions.end(); ++it) { std::vector& types = it->second; std::vector::iterator ctit = find(types.begin(), types.end(), cid); if (ctit != types.end()) types.erase(ctit); } mustReloadComponents = true; } JS::RootedValue protoVal(rq.cx); - if (!componentManager->m_ScriptInterface.GetProperty(ctor, "prototype", &protoVal)) + if (!m_ScriptInterface.GetProperty(ctor, "prototype", &protoVal)) { ScriptException::Raise(rq, "Failed to get property 'prototype'"); return; } if (!protoVal.isObject()) { ScriptException::Raise(rq, "Component has no constructor"); return; } std::string schema = ""; - if (componentManager->m_ScriptInterface.HasProperty(protoVal, "Schema")) - componentManager->m_ScriptInterface.GetProperty(protoVal, "Schema", schema); + if (m_ScriptInterface.HasProperty(protoVal, "Schema")) + m_ScriptInterface.GetProperty(protoVal, "Schema", schema); // Construct a new ComponentType, using the wrapper's alloc functions ComponentType ct{ CT_Script, iid, ctWrapper.alloc, ctWrapper.dealloc, cname, schema, std::make_unique(rq.cx, ctor) }; - componentManager->m_ComponentTypesById[cid] = std::move(ct); + m_ComponentTypesById[cid] = std::move(ct); - componentManager->m_CurrentComponent = cid; // needed by Subscribe + m_CurrentComponent = cid; // needed by Subscribe // Find all the ctor prototype's On* methods, and subscribe to the appropriate messages: std::vector methods; - if (!componentManager->m_ScriptInterface.EnumeratePropertyNames(protoVal, false, methods)) + if (!m_ScriptInterface.EnumeratePropertyNames(protoVal, false, methods)) { ScriptException::Raise(rq, "Failed to enumerate component properties."); return; } for (std::vector::const_iterator it = methods.begin(); it != methods.end(); ++it) { // TODO C++17: string_view if (strncmp((it->c_str()), "On", 2) != 0) continue; std::string name = (*it).substr(2); // strip the "On" prefix // Handle "OnGlobalFoo" functions specially bool isGlobal = false; if (strncmp(name.c_str(), "Global", 6) == 0) { isGlobal = true; name = name.substr(6); } - std::map::const_iterator mit = componentManager->m_MessageTypeIdsByName.find(name); - if (mit == componentManager->m_MessageTypeIdsByName.end()) + std::map::const_iterator mit = m_MessageTypeIdsByName.find(name); + if (mit == m_MessageTypeIdsByName.end()) { ScriptException::Raise(rq, "Registered component has unrecognized '%s' message handler method", it->c_str()); return; } if (isGlobal) - componentManager->SubscribeGloballyToMessageType(mit->second); + SubscribeGloballyToMessageType(mit->second); else - componentManager->SubscribeToMessageType(mit->second); + SubscribeToMessageType(mit->second); } - componentManager->m_CurrentComponent = CID__Invalid; + m_CurrentComponent = CID__Invalid; if (mustReloadComponents) { // For every script component with this cid, we need to switch its // prototype from the old constructor's prototype property to the new one's - const std::map& comps = componentManager->m_ComponentsByTypeId[cid]; + const std::map& comps = m_ComponentsByTypeId[cid]; std::map::const_iterator eit = comps.begin(); for (; eit != comps.end(); ++eit) { JS::RootedValue instance(rq.cx, eit->second->GetJSInstance()); if (!instance.isNull()) - componentManager->m_ScriptInterface.SetPrototype(instance, protoVal); + m_ScriptInterface.SetPrototype(instance, protoVal); } } } -void CComponentManager::Script_RegisterComponentType(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor) +void CComponentManager::Script_RegisterComponentType(int iid, const std::string& cname, JS::HandleValue ctor) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - componentManager->Script_RegisterComponentType_Common(pCmptPrivate, iid, cname, ctor, false, false); - componentManager->m_ScriptInterface.SetGlobal(cname.c_str(), ctor, componentManager->m_CurrentlyHotloading); + Script_RegisterComponentType_Common(iid, cname, ctor, false, false); + m_ScriptInterface.SetGlobal(cname.c_str(), ctor, m_CurrentlyHotloading); } -void CComponentManager::Script_RegisterSystemComponentType(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor) +void CComponentManager::Script_RegisterSystemComponentType(int iid, const std::string& cname, JS::HandleValue ctor) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - componentManager->Script_RegisterComponentType_Common(pCmptPrivate, iid, cname, ctor, false, true); - componentManager->m_ScriptInterface.SetGlobal(cname.c_str(), ctor, componentManager->m_CurrentlyHotloading); + Script_RegisterComponentType_Common(iid, cname, ctor, false, true); + m_ScriptInterface.SetGlobal(cname.c_str(), ctor, m_CurrentlyHotloading); } -void CComponentManager::Script_ReRegisterComponentType(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor) +void CComponentManager::Script_ReRegisterComponentType(int iid, const std::string& cname, JS::HandleValue ctor) { - Script_RegisterComponentType_Common(pCmptPrivate, iid, cname, ctor, true, false); + Script_RegisterComponentType_Common(iid, cname, ctor, true, false); } -void CComponentManager::Script_RegisterInterface(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name) +void CComponentManager::Script_RegisterInterface(const std::string& name) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - - std::map::iterator it = componentManager->m_InterfaceIdsByName.find(name); - if (it != componentManager->m_InterfaceIdsByName.end()) + std::map::iterator it = m_InterfaceIdsByName.find(name); + if (it != m_InterfaceIdsByName.end()) { // Redefinitions are fine (and just get ignored) when hotloading; otherwise // they're probably unintentional and should be reported - if (!componentManager->m_CurrentlyHotloading) + if (!m_CurrentlyHotloading) { - ScriptRequest rq(componentManager->m_ScriptInterface); + ScriptRequest rq(m_ScriptInterface); ScriptException::Raise(rq, "Registering interface with already-registered name '%s'", name.c_str()); } return; } // IIDs start at 1, so size+1 is the next unused one - size_t id = componentManager->m_InterfaceIdsByName.size() + 1; - componentManager->m_InterfaceIdsByName[name] = (InterfaceId)id; - componentManager->m_ComponentsByInterface.resize(id+1); // add one so we can index by InterfaceId - componentManager->m_ScriptInterface.SetGlobal(("IID_" + name).c_str(), (int)id); + size_t id = m_InterfaceIdsByName.size() + 1; + m_InterfaceIdsByName[name] = (InterfaceId)id; + m_ComponentsByInterface.resize(id+1); // add one so we can index by InterfaceId + m_ScriptInterface.SetGlobal(("IID_" + name).c_str(), (int)id); } -void CComponentManager::Script_RegisterMessageType(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name) +void CComponentManager::Script_RegisterMessageType(const std::string& name) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - - std::map::iterator it = componentManager->m_MessageTypeIdsByName.find(name); - if (it != componentManager->m_MessageTypeIdsByName.end()) + std::map::iterator it = m_MessageTypeIdsByName.find(name); + if (it != m_MessageTypeIdsByName.end()) { // Redefinitions are fine (and just get ignored) when hotloading; otherwise // they're probably unintentional and should be reported - if (!componentManager->m_CurrentlyHotloading) + if (!m_CurrentlyHotloading) { - ScriptRequest rq(componentManager->m_ScriptInterface); + ScriptRequest rq(m_ScriptInterface); ScriptException::Raise(rq, "Registering message type with already-registered name '%s'", name.c_str()); } return; } // MTIDs start at 1, so size+1 is the next unused one - size_t id = componentManager->m_MessageTypeIdsByName.size() + 1; - componentManager->RegisterMessageType((MessageTypeId)id, name.c_str()); - componentManager->m_ScriptInterface.SetGlobal(("MT_" + name).c_str(), (int)id); -} - -void CComponentManager::Script_RegisterGlobal(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name, JS::HandleValue value) -{ - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - componentManager->m_ScriptInterface.SetGlobal(name.c_str(), value, componentManager->m_CurrentlyHotloading); + size_t id = m_MessageTypeIdsByName.size() + 1; + RegisterMessageType((MessageTypeId)id, name.c_str()); + m_ScriptInterface.SetGlobal(("MT_" + name).c_str(), (int)id); } -IComponent* CComponentManager::Script_QueryInterface(ScriptInterface::CmptPrivate* pCmptPrivate, int ent, int iid) +void CComponentManager::Script_RegisterGlobal(const std::string& name, JS::HandleValue value) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - IComponent* component = componentManager->QueryInterface((entity_id_t)ent, iid); - return component; + m_ScriptInterface.SetGlobal(name.c_str(), value, m_CurrentlyHotloading); } -std::vector CComponentManager::Script_GetEntitiesWithInterface(ScriptInterface::CmptPrivate* pCmptPrivate, int iid) +std::vector CComponentManager::Script_GetEntitiesWithInterface(int iid) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - std::vector ret; - const InterfaceListUnordered& ents = componentManager->GetEntitiesWithInterfaceUnordered(iid); + const InterfaceListUnordered& ents = GetEntitiesWithInterfaceUnordered(iid); for (InterfaceListUnordered::const_iterator it = ents.begin(); it != ents.end(); ++it) if (!ENTITY_IS_LOCAL(it->first)) ret.push_back(it->first); std::sort(ret.begin(), ret.end()); return ret; } -std::vector CComponentManager::Script_GetComponentsWithInterface(ScriptInterface::CmptPrivate* pCmptPrivate, int iid) +std::vector CComponentManager::Script_GetComponentsWithInterface(int iid) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - std::vector ret; - InterfaceList ents = componentManager->GetEntitiesWithInterface(iid); + InterfaceList ents = GetEntitiesWithInterface(iid); for (InterfaceList::const_iterator it = ents.begin(); it != ents.end(); ++it) ret.push_back(it->second); // TODO: maybe we should exclude local entities return ret; } CMessage* CComponentManager::ConstructMessage(int mtid, JS::HandleValue data) { if (mtid == MT__Invalid || mtid > (int)m_MessageTypeIdsByName.size()) // (IDs start at 1 so use '>' here) LOGERROR("PostMessage with invalid message type ID '%d'", mtid); if (mtid < MT__LastNative) { return CMessageFromJSVal(mtid, m_ScriptInterface, data); } else { return new CMessageScripted(m_ScriptInterface, mtid, m_MessageTypeNamesById[mtid], data); } } -void CComponentManager::Script_PostMessage(ScriptInterface::CmptPrivate* pCmptPrivate, int ent, int mtid, JS::HandleValue data) +void CComponentManager::Script_PostMessage(int ent, int mtid, JS::HandleValue data) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - - CMessage* msg = componentManager->ConstructMessage(mtid, data); + CMessage* msg = ConstructMessage(mtid, data); if (!msg) return; // error - componentManager->PostMessage(ent, *msg); + PostMessage(ent, *msg); delete msg; } -void CComponentManager::Script_BroadcastMessage(ScriptInterface::CmptPrivate* pCmptPrivate, int mtid, JS::HandleValue data) +void CComponentManager::Script_BroadcastMessage(int mtid, JS::HandleValue data) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - - CMessage* msg = componentManager->ConstructMessage(mtid, data); + CMessage* msg = ConstructMessage(mtid, data); if (!msg) return; // error - componentManager->BroadcastMessage(*msg); + BroadcastMessage(*msg); delete msg; } -int CComponentManager::Script_AddEntity(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName) +int CComponentManager::Script_AddEntity(const std::wstring& templateName) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - - std::wstring name(templateName.begin(), templateName.end()); // TODO: should validate the string to make sure it doesn't contain scary characters // that will let it access non-component-template files - - entity_id_t ent = componentManager->AddEntity(name, componentManager->AllocateNewEntity()); - return (int)ent; + return AddEntity(templateName, AllocateNewEntity()); } -int CComponentManager::Script_AddLocalEntity(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName) +int CComponentManager::Script_AddLocalEntity(const std::wstring& templateName) { - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - - std::wstring name(templateName.begin(), templateName.end()); // TODO: should validate the string to make sure it doesn't contain scary characters // that will let it access non-component-template files - - entity_id_t ent = componentManager->AddEntity(name, componentManager->AllocateNewLocalEntity()); - return (int)ent; -} - -void CComponentManager::Script_DestroyEntity(ScriptInterface::CmptPrivate* pCmptPrivate, int ent) -{ - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - - componentManager->DestroyComponentsSoon(ent); -} - -void CComponentManager::Script_FlushDestroyedEntities(ScriptInterface::CmptPrivate *pCmptPrivate) -{ - CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); - componentManager->FlushDestroyedComponents(); + return AddEntity(templateName, AllocateNewLocalEntity()); } void CComponentManager::ResetState() { // Delete all dynamic message subscriptions m_DynamicMessageSubscriptionsNonsync.clear(); m_DynamicMessageSubscriptionsNonsyncByComponent.clear(); // Delete all IComponents std::map >::iterator iit = m_ComponentsByTypeId.begin(); for (; iit != m_ComponentsByTypeId.end(); ++iit) { std::map::iterator eit = iit->second.begin(); for (; eit != iit->second.end(); ++eit) { eit->second->Deinit(); m_ComponentTypesById[iit->first].dealloc(eit->second); } } std::vector >::iterator ifcit = m_ComponentsByInterface.begin(); for (; ifcit != m_ComponentsByInterface.end(); ++ifcit) ifcit->clear(); m_ComponentsByTypeId.clear(); // Delete all SEntityComponentCaches std::unordered_map::iterator ccit = m_ComponentCaches.begin(); for (; ccit != m_ComponentCaches.end(); ++ccit) free(ccit->second); m_ComponentCaches.clear(); m_SystemEntity = CEntityHandle(); m_DestructionQueue.clear(); // Reset IDs m_NextEntityId = SYSTEM_ENTITY + 1; m_NextLocalEntityId = FIRST_LOCAL_ENTITY; } void CComponentManager::SetRNGSeed(u32 seed) { m_RNG.seed(seed); } void CComponentManager::RegisterComponentType(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc, const char* name, const std::string& schema) { ComponentType c{ CT_Native, iid, alloc, dealloc, name, schema, std::unique_ptr() }; m_ComponentTypesById.insert(std::make_pair(cid, std::move(c))); m_ComponentTypeIdsByName[name] = cid; } void CComponentManager::RegisterComponentTypeScriptWrapper(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc, const char* name, const std::string& schema) { ComponentType c{ CT_ScriptWrapper, iid, alloc, dealloc, name, schema, std::unique_ptr() }; m_ComponentTypesById.insert(std::make_pair(cid, std::move(c))); m_ComponentTypeIdsByName[name] = cid; // TODO: merge with RegisterComponentType } void CComponentManager::MarkScriptedComponentForSystemEntity(CComponentManager::ComponentTypeId cid) { m_ScriptedSystemComponents.push_back(cid); } void CComponentManager::RegisterMessageType(MessageTypeId mtid, const char* name) { m_MessageTypeIdsByName[name] = mtid; m_MessageTypeNamesById[mtid] = name; } void CComponentManager::SubscribeToMessageType(MessageTypeId mtid) { // TODO: verify mtid ENSURE(m_CurrentComponent != CID__Invalid); std::vector& types = m_LocalMessageSubscriptions[mtid]; types.push_back(m_CurrentComponent); std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents } void CComponentManager::SubscribeGloballyToMessageType(MessageTypeId mtid) { // TODO: verify mtid ENSURE(m_CurrentComponent != CID__Invalid); std::vector& types = m_GlobalMessageSubscriptions[mtid]; types.push_back(m_CurrentComponent); std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents } void CComponentManager::FlattenDynamicSubscriptions() { std::map::iterator it; for (it = m_DynamicMessageSubscriptionsNonsync.begin(); it != m_DynamicMessageSubscriptionsNonsync.end(); ++it) { it->second.Flatten(); } } void CComponentManager::DynamicSubscriptionNonsync(MessageTypeId mtid, IComponent* component, bool enable) { if (enable) { bool newlyInserted = m_DynamicMessageSubscriptionsNonsyncByComponent[component].insert(mtid).second; if (newlyInserted) m_DynamicMessageSubscriptionsNonsync[mtid].Add(component); } else { size_t numRemoved = m_DynamicMessageSubscriptionsNonsyncByComponent[component].erase(mtid); if (numRemoved) m_DynamicMessageSubscriptionsNonsync[mtid].Remove(component); } } void CComponentManager::RemoveComponentDynamicSubscriptions(IComponent* component) { std::map >::iterator it = m_DynamicMessageSubscriptionsNonsyncByComponent.find(component); if (it == m_DynamicMessageSubscriptionsNonsyncByComponent.end()) return; std::set::iterator mtit; for (mtit = it->second.begin(); mtit != it->second.end(); ++mtit) { m_DynamicMessageSubscriptionsNonsync[*mtit].Remove(component); // Need to flatten the subscription lists immediately to avoid dangling IComponent* references m_DynamicMessageSubscriptionsNonsync[*mtit].Flatten(); } m_DynamicMessageSubscriptionsNonsyncByComponent.erase(it); } CComponentManager::ComponentTypeId CComponentManager::LookupCID(const std::string& cname) const { std::map::const_iterator it = m_ComponentTypeIdsByName.find(cname); if (it == m_ComponentTypeIdsByName.end()) return CID__Invalid; return it->second; } std::string CComponentManager::LookupComponentTypeName(ComponentTypeId cid) const { std::map::const_iterator it = m_ComponentTypesById.find(cid); if (it == m_ComponentTypesById.end()) return ""; return it->second.name; } CComponentManager::ComponentTypeId CComponentManager::GetScriptWrapper(InterfaceId iid) { if (iid >= IID__LastNative && iid <= (int)m_InterfaceIdsByName.size()) // use <= since IDs start at 1 return CID_UnknownScript; std::map::const_iterator it = m_ComponentTypesById.begin(); for (; it != m_ComponentTypesById.end(); ++it) if (it->second.iid == iid && it->second.type == CT_ScriptWrapper) return it->first; std::map::const_iterator iiit = m_InterfaceIdsByName.begin(); for (; iiit != m_InterfaceIdsByName.end(); ++iiit) if (iiit->second == iid) { LOGERROR("No script wrapper found for interface id %d '%s'", iid, iiit->first.c_str()); return CID__Invalid; } LOGERROR("No script wrapper found for interface id %d", iid); return CID__Invalid; } entity_id_t CComponentManager::AllocateNewEntity() { entity_id_t id = m_NextEntityId++; // TODO: check for overflow return id; } entity_id_t CComponentManager::AllocateNewLocalEntity() { entity_id_t id = m_NextLocalEntityId++; // TODO: check for overflow return id; } entity_id_t CComponentManager::AllocateNewEntity(entity_id_t preferredId) { // TODO: ensure this ID hasn't been allocated before // (this might occur with broken map files) // Trying to actually add two entities with the same id will fail in AddEntitiy entity_id_t id = preferredId; // Ensure this ID won't be allocated again if (id >= m_NextEntityId) m_NextEntityId = id+1; // TODO: check for overflow return id; } bool CComponentManager::AddComponent(CEntityHandle ent, ComponentTypeId cid, const CParamNode& paramNode) { IComponent* component = ConstructComponent(ent, cid); if (!component) return false; component->Init(paramNode); return true; } void CComponentManager::AddSystemComponents(bool skipScriptedComponents, bool skipAI) { CParamNode noParam; AddComponent(m_SystemEntity, CID_TemplateManager, noParam); AddComponent(m_SystemEntity, CID_CinemaManager, noParam); AddComponent(m_SystemEntity, CID_CommandQueue, noParam); AddComponent(m_SystemEntity, CID_ObstructionManager, noParam); AddComponent(m_SystemEntity, CID_ParticleManager, noParam); AddComponent(m_SystemEntity, CID_Pathfinder, noParam); AddComponent(m_SystemEntity, CID_ProjectileManager, noParam); AddComponent(m_SystemEntity, CID_RangeManager, noParam); AddComponent(m_SystemEntity, CID_SoundManager, noParam); AddComponent(m_SystemEntity, CID_Terrain, noParam); AddComponent(m_SystemEntity, CID_TerritoryManager, noParam); AddComponent(m_SystemEntity, CID_UnitRenderer, noParam); AddComponent(m_SystemEntity, CID_WaterManager, noParam); // Add scripted system components: if (!skipScriptedComponents) { for (uint32_t i = 0; i < m_ScriptedSystemComponents.size(); ++i) AddComponent(m_SystemEntity, m_ScriptedSystemComponents[i], noParam); if (!skipAI) AddComponent(m_SystemEntity, CID_AIManager, noParam); } } IComponent* CComponentManager::ConstructComponent(CEntityHandle ent, ComponentTypeId cid) { ScriptRequest rq(m_ScriptInterface); std::map::const_iterator it = m_ComponentTypesById.find(cid); if (it == m_ComponentTypesById.end()) { LOGERROR("Invalid component id %d", cid); return NULL; } const ComponentType& ct = it->second; ENSURE((size_t)ct.iid < m_ComponentsByInterface.size()); std::unordered_map& emap1 = m_ComponentsByInterface[ct.iid]; if (emap1.find(ent.GetId()) != emap1.end()) { LOGERROR("Multiple components for interface %d", ct.iid); return NULL; } std::map& emap2 = m_ComponentsByTypeId[cid]; // If this is a scripted component, construct the appropriate JS object first JS::RootedValue obj(rq.cx); if (ct.type == CT_Script) { m_ScriptInterface.CallConstructor(*ct.ctor, JS::HandleValueArray::empty(), &obj); if (obj.isNull()) { LOGERROR("Script component constructor failed"); return NULL; } } // Construct the new component IComponent* component = ct.alloc(m_ScriptInterface, obj); ENSURE(component); component->SetEntityHandle(ent); component->SetSimContext(m_SimContext); // Store a reference to the new component emap1.insert(std::make_pair(ent.GetId(), component)); emap2.insert(std::make_pair(ent.GetId(), component)); // TODO: We need to more careful about this - if an entity is constructed by a component // while we're iterating over all components, this will invalidate the iterators and everything // will break. // We probably need some kind of delayed addition, so they get pushed onto a queue and then // inserted into the world later on. (Be careful about immediation deletion in that case, too.) SEntityComponentCache* cache = ent.GetComponentCache(); ENSURE(cache != NULL && ct.iid < (int)cache->numInterfaces && cache->interfaces[ct.iid] == NULL); cache->interfaces[ct.iid] = component; return component; } void CComponentManager::AddMockComponent(CEntityHandle ent, InterfaceId iid, IComponent& component) { // Just add it into the by-interface map, not the by-component-type map, // so it won't be considered for messages or deletion etc std::unordered_map& emap1 = m_ComponentsByInterface.at(iid); if (emap1.find(ent.GetId()) != emap1.end()) debug_warn(L"Multiple components for interface"); emap1.insert(std::make_pair(ent.GetId(), &component)); SEntityComponentCache* cache = ent.GetComponentCache(); ENSURE(cache != NULL && iid < (int)cache->numInterfaces && cache->interfaces[iid] == NULL); cache->interfaces[iid] = &component; } CEntityHandle CComponentManager::AllocateEntityHandle(entity_id_t ent) { ENSURE(!EntityExists(ent)); // Interface IDs start at 1, and SEntityComponentCache is defined with a 1-sized array, // so we need space for an extra m_InterfaceIdsByName.size() items SEntityComponentCache* cache = (SEntityComponentCache*)calloc(1, sizeof(SEntityComponentCache) + sizeof(IComponent*) * m_InterfaceIdsByName.size()); ENSURE(cache != NULL); cache->numInterfaces = m_InterfaceIdsByName.size() + 1; m_ComponentCaches[ent] = cache; return CEntityHandle(ent, cache); } CEntityHandle CComponentManager::LookupEntityHandle(entity_id_t ent, bool allowCreate) { std::unordered_map::iterator it; it = m_ComponentCaches.find(ent); if (it == m_ComponentCaches.end()) { if (allowCreate) return AllocateEntityHandle(ent); else return CEntityHandle(ent, NULL); } else return CEntityHandle(ent, it->second); } void CComponentManager::InitSystemEntity() { ENSURE(m_SystemEntity.GetId() == INVALID_ENTITY); m_SystemEntity = AllocateEntityHandle(SYSTEM_ENTITY); m_SimContext.SetSystemEntity(m_SystemEntity); } entity_id_t CComponentManager::AddEntity(const std::wstring& templateName, entity_id_t ent) { ICmpTemplateManager *cmpTemplateManager = static_cast (QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)); if (!cmpTemplateManager) { debug_warn(L"No ICmpTemplateManager loaded"); return INVALID_ENTITY; } const CParamNode* tmpl = cmpTemplateManager->LoadTemplate(ent, utf8_from_wstring(templateName)); if (!tmpl) return INVALID_ENTITY; // LoadTemplate will have reported the error // This also ensures that ent does not exist CEntityHandle handle = AllocateEntityHandle(ent); // Construct a component for each child of the root element const CParamNode::ChildrenMap& tmplChilds = tmpl->GetChildren(); for (CParamNode::ChildrenMap::const_iterator it = tmplChilds.begin(); it != tmplChilds.end(); ++it) { // Ignore attributes on the root element if (it->first.length() && it->first[0] == '@') continue; CComponentManager::ComponentTypeId cid = LookupCID(it->first); if (cid == CID__Invalid) { LOGERROR("Unrecognized component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); return INVALID_ENTITY; } if (!AddComponent(handle, cid, it->second)) { LOGERROR("Failed to construct component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); return INVALID_ENTITY; } // TODO: maybe we should delete already-constructed components if one of them fails? } CMessageCreate msg(ent); PostMessage(ent, msg); return ent; } bool CComponentManager::EntityExists(entity_id_t ent) const { return m_ComponentCaches.find(ent) != m_ComponentCaches.end(); } void CComponentManager::DestroyComponentsSoon(entity_id_t ent) { m_DestructionQueue.push_back(ent); } void CComponentManager::FlushDestroyedComponents() { PROFILE2("Flush Destroyed Components"); while (!m_DestructionQueue.empty()) { // Make a copy of the destruction queue, so that the iterators won't be invalidated if the // CMessageDestroy handlers try to destroy more entities themselves std::vector queue; queue.swap(m_DestructionQueue); for (std::vector::iterator it = queue.begin(); it != queue.end(); ++it) { entity_id_t ent = *it; // Do nothing if invalid, destroyed, etc. if (!EntityExists(ent)) continue; CEntityHandle handle = LookupEntityHandle(ent); CMessageDestroy msg(ent); PostMessage(ent, msg); // Flatten all the dynamic subscriptions to ensure there are no dangling // references in the 'removed' lists to components we're going to delete // Some components may have dynamically unsubscribed following the Destroy message FlattenDynamicSubscriptions(); // Destroy the components, and remove from m_ComponentsByTypeId: std::map >::iterator iit = m_ComponentsByTypeId.begin(); for (; iit != m_ComponentsByTypeId.end(); ++iit) { std::map::iterator eit = iit->second.find(ent); if (eit != iit->second.end()) { eit->second->Deinit(); RemoveComponentDynamicSubscriptions(eit->second); m_ComponentTypesById[iit->first].dealloc(eit->second); iit->second.erase(ent); handle.GetComponentCache()->interfaces[m_ComponentTypesById[iit->first].iid] = NULL; } } free(handle.GetComponentCache()); m_ComponentCaches.erase(ent); // Remove from m_ComponentsByInterface std::vector >::iterator ifcit = m_ComponentsByInterface.begin(); for (; ifcit != m_ComponentsByInterface.end(); ++ifcit) { ifcit->erase(ent); } } } } IComponent* CComponentManager::QueryInterface(entity_id_t ent, InterfaceId iid) const { if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return NULL; } std::unordered_map::const_iterator eit = m_ComponentsByInterface[iid].find(ent); if (eit == m_ComponentsByInterface[iid].end()) { // This entity doesn't implement this interface return NULL; } return eit->second; } CComponentManager::InterfaceList CComponentManager::GetEntitiesWithInterface(InterfaceId iid) const { std::vector > ret; if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return ret; } ret.reserve(m_ComponentsByInterface[iid].size()); std::unordered_map::const_iterator it = m_ComponentsByInterface[iid].begin(); for (; it != m_ComponentsByInterface[iid].end(); ++it) ret.push_back(*it); std::sort(ret.begin(), ret.end()); // lexicographic pair comparison means this'll sort by entity ID return ret; } static CComponentManager::InterfaceListUnordered g_EmptyEntityMap; const CComponentManager::InterfaceListUnordered& CComponentManager::GetEntitiesWithInterfaceUnordered(InterfaceId iid) const { if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return g_EmptyEntityMap; } return m_ComponentsByInterface[iid]; } void CComponentManager::PostMessage(entity_id_t ent, const CMessage& msg) { PROFILE2_IFSPIKE("Post Message", 0.0005); PROFILE2_ATTR("%s", msg.GetScriptHandlerName()); // Send the message to components of ent, that subscribed locally to this message std::map >::const_iterator it; it = m_LocalMessageSubscriptions.find(msg.GetType()); if (it != m_LocalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.find(ent); if (eit != emap->second.end()) eit->second->HandleMessage(msg, false); } } SendGlobalMessage(ent, msg); } void CComponentManager::BroadcastMessage(const CMessage& msg) { // Send the message to components of all entities that subscribed locally to this message std::map >::const_iterator it; it = m_LocalMessageSubscriptions.find(msg.GetType()); if (it != m_LocalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.begin(); for (; eit != emap->second.end(); ++eit) eit->second->HandleMessage(msg, false); } } SendGlobalMessage(INVALID_ENTITY, msg); } void CComponentManager::SendGlobalMessage(entity_id_t ent, const CMessage& msg) { PROFILE2_IFSPIKE("SendGlobalMessage", 0.001); PROFILE2_ATTR("%s", msg.GetScriptHandlerName()); // (Common functionality for PostMessage and BroadcastMessage) // Send the message to components of all entities that subscribed globally to this message std::map >::const_iterator it; it = m_GlobalMessageSubscriptions.find(msg.GetType()); if (it != m_GlobalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Special case: Messages for local entities shouldn't be sent to script // components that subscribed globally, so that we don't have to worry about // them accidentally picking up non-network-synchronised data. if (ENTITY_IS_LOCAL(ent)) { std::map::const_iterator cit = m_ComponentTypesById.find(*ctit); if (cit != m_ComponentTypesById.end() && cit->second.type == CT_Script) continue; } // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.begin(); for (; eit != emap->second.end(); ++eit) eit->second->HandleMessage(msg, true); } } // Send the message to component instances that dynamically subscribed to this message std::map::iterator dit = m_DynamicMessageSubscriptionsNonsync.find(msg.GetType()); if (dit != m_DynamicMessageSubscriptionsNonsync.end()) { dit->second.Flatten(); const std::vector& dynamic = dit->second.GetComponents(); for (size_t i = 0; i < dynamic.size(); i++) dynamic[i]->HandleMessage(msg, false); } } std::string CComponentManager::GenerateSchema() const { std::string schema = "" "" "" "" "" "0" "" "" "0" "" "" "" "" "" "" "" "" "" "" "" "" ""; std::map > interfaceComponentTypes; std::vector componentTypes; for (std::map::const_iterator it = m_ComponentTypesById.begin(); it != m_ComponentTypesById.end(); ++it) { schema += "" "" "" + it->second.schema + "" "" ""; interfaceComponentTypes[it->second.iid].push_back(it->second.name); componentTypes.push_back(it->second.name); } // Declare the implementation of each interface, for documentation for (std::map::const_iterator it = m_InterfaceIdsByName.begin(); it != m_InterfaceIdsByName.end(); ++it) { schema += ""; std::vector& cts = interfaceComponentTypes[it->second]; for (size_t i = 0; i < cts.size(); ++i) schema += ""; schema += ""; } // List all the component types, in alphabetical order (to match the reordering performed by CParamNode). // (We do it this way, rather than ing all the interface definitions (which would additionally perform // a check that we don't use multiple component types of the same interface in one file), because libxml2 gives // useless error messages in the latter case; this way lets it report the real error.) std::sort(componentTypes.begin(), componentTypes.end()); schema += "" "" ""; for (std::vector::const_iterator it = componentTypes.begin(); it != componentTypes.end(); ++it) schema += ""; schema += "" ""; schema += ""; return schema; } Index: ps/trunk/source/simulation2/system/ComponentManager.h =================================================================== --- ps/trunk/source/simulation2/system/ComponentManager.h (revision 24968) +++ ps/trunk/source/simulation2/system/ComponentManager.h (revision 24969) @@ -1,342 +1,339 @@ -/* 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 . */ #ifndef INCLUDED_COMPONENTMANAGER #define INCLUDED_COMPONENTMANAGER #include "ps/Filesystem.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/helpers/Player.h" #include "simulation2/system/Components.h" #include "simulation2/system/Entity.h" #include #include #include #include class IComponent; class CParamNode; class CMessage; class CSimContext; class CDynamicSubscription; class CComponentManager { NONCOPYABLE(CComponentManager); public: // We can't use EInterfaceId/etc directly, since scripts dynamically generate new IDs // and casting arbitrary ints to enums is undefined behaviour, so use 'int' typedefs typedef int InterfaceId; typedef int ComponentTypeId; typedef int MessageTypeId; private: // Component allocation types typedef IComponent* (*AllocFunc)(const ScriptInterface& scriptInterface, JS::HandleValue ctor); typedef void (*DeallocFunc)(IComponent*); // ComponentTypes come in three types: // Native: normal C++ component // ScriptWrapper: C++ component that wraps a JS component implementation // Script: a ScriptWrapper linked to a specific JS component implementation enum EComponentTypeType { CT_Native, CT_ScriptWrapper, CT_Script }; // Representation of a component type, to be used when instantiating components struct ComponentType { EComponentTypeType type; InterfaceId iid; AllocFunc alloc; DeallocFunc dealloc; std::string name; std::string schema; // RelaxNG fragment std::unique_ptr ctor; // only valid if type == CT_Script }; public: CComponentManager(CSimContext&, shared_ptr cx, bool skipScriptFunctions = false); ~CComponentManager(); void LoadComponentTypes(); /** * Load a script and execute it in a new function scope. * @param filename VFS path to load * @param hotload set to true if this script has been loaded before, and redefinitions of * existing components should not be considered errors */ bool LoadScript(const VfsPath& filename, bool hotload = false); void RegisterMessageType(MessageTypeId mtid, const char* name); void RegisterComponentType(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema); void RegisterComponentTypeScriptWrapper(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema); void MarkScriptedComponentForSystemEntity(CComponentManager::ComponentTypeId cid); /** * Subscribe the current component type to the given message type. * Each component's HandleMessage will be called on any BroadcastMessage of this message type, * or on any PostMessage of this type targeted at the component's entity. * Must only be called by a component type's ClassInit. */ void SubscribeToMessageType(MessageTypeId mtid); /** * Subscribe the current component type to all messages of the given message type. * Each component's HandleMessage will be called on any BroadcastMessage or PostMessage of this message type, * regardless of the entity. * Must only be called by a component type's ClassInit. */ void SubscribeGloballyToMessageType(MessageTypeId mtid); /** * Subscribe the given component instance to all messages of the given message type. * The component's HandleMessage will be called on any BroadcastMessage or PostMessage of * this message type, regardless of the entity. * * This can be called at any time (including inside the HandleMessage callback for this message type). * * The component type must not have statically subscribed to this message type in its ClassInit. * * The subscription status is not saved or network-synchronised. Components must remember to * resubscribe in their Deserialize methods if they still want the message. * * This is primarily intended for Interpolate and RenderSubmit messages, to avoid the cost of * sending the message to components that do not currently need to do any rendering. */ void DynamicSubscriptionNonsync(MessageTypeId mtid, IComponent* component, bool enabled); /** * @param cname Requested component type name (not including any "CID" or "CCmp" prefix) * @return The component type id, or CID__Invalid if not found */ ComponentTypeId LookupCID(const std::string& cname) const; /** * @return The name of the given component type, or "" if not found */ std::string LookupComponentTypeName(ComponentTypeId cid) const; /** * Set up an empty SYSTEM_ENTITY. Must be called after ResetState() and before GetSystemEntity(). */ void InitSystemEntity(); /** * Returns a CEntityHandle with id SYSTEM_ENTITY. */ CEntityHandle GetSystemEntity() { ASSERT(m_SystemEntity.GetId() == SYSTEM_ENTITY); return m_SystemEntity; } /** * Returns a CEntityHandle with id @p ent. * If @p allowCreate is true and there is no existing CEntityHandle, a new handle will be allocated. */ CEntityHandle LookupEntityHandle(entity_id_t ent, bool allowCreate = false); /** * Returns true if the entity with id @p ent exists. */ bool EntityExists(entity_id_t ent) const; /** * Returns a new entity ID that has never been used before. * This affects the simulation state so it must only be called in network-synchronised ways. */ entity_id_t AllocateNewEntity(); /** * Returns a new local entity ID that has never been used before. * This entity will not be synchronised over the network, stored in saved games, etc. */ entity_id_t AllocateNewLocalEntity(); /** * Returns a new entity ID that has never been used before. * If possible, returns preferredId, and ensures this ID won't be allocated again. * This affects the simulation state so it must only be called in network-synchronised ways. */ entity_id_t AllocateNewEntity(entity_id_t preferredId); /** * Constructs a component of type 'cid', initialised with data 'paramNode', * and attaches it to entity 'ent'. * * @return true on success; false on failure, and logs an error message */ bool AddComponent(CEntityHandle ent, ComponentTypeId cid, const CParamNode& paramNode); /** * Add all system components to the system entity (skip the scripted components or the AI components on demand) */ void AddSystemComponents(bool skipScriptedComponents, bool skipAI); /** * Adds an externally-created component, so that it is returned by QueryInterface * but does not get destroyed and does not receive messages from the component manager. * (This is intended for unit tests that need to add mock objects the tested components * expect to exist.) */ void AddMockComponent(CEntityHandle ent, InterfaceId iid, IComponent& component); /** * Allocates a component object of type 'cid', and attaches it to entity 'ent'. * (The component's Init is not called here - either Init or Deserialize must be called * before using the returned object.) */ IComponent* ConstructComponent(CEntityHandle ent, ComponentTypeId cid); /** * Constructs an entity based on the given template, and adds it the world with * entity ID @p ent. There should not be any existing components with that entity ID. * @return ent, or INVALID_ENTITY on error */ entity_id_t AddEntity(const std::wstring& templateName, entity_id_t ent); /** * Destroys all the components belonging to the specified entity when FlushDestroyedComponents is called. * Has no effect if the entity does not exist, or has already been added to the destruction queue. */ void DestroyComponentsSoon(entity_id_t ent); /** * Does the actual destruction of components from DestroyComponentsSoon. * This must not be called if the component manager is on the call stack (since it * will break internal iterators). */ void FlushDestroyedComponents(); IComponent* QueryInterface(entity_id_t ent, InterfaceId iid) const; using InterfacePair = std::pair; using InterfaceList = std::vector; using InterfaceListUnordered = std::unordered_map; InterfaceList GetEntitiesWithInterface(InterfaceId iid) const; const InterfaceListUnordered& GetEntitiesWithInterfaceUnordered(InterfaceId iid) const; /** * Send a message, targeted at a particular entity. The message will be received by any * components of that entity which subscribed to the message type, and by any other components * that subscribed globally to the message type. */ void PostMessage(entity_id_t ent, const CMessage& msg); /** * Send a message, not targeted at any particular entity. The message will be received by any * components that subscribed (either globally or not) to the message type. */ void BroadcastMessage(const CMessage& msg); /** * Resets the dynamic simulation state (deletes all entities, resets entity ID counters; * doesn't unload/reload component scripts). */ void ResetState(); /** * Initializes the random number generator with a seed determined by the host. */ void SetRNGSeed(u32 seed); // Various state serialization functions: bool ComputeStateHash(std::string& outHash, bool quick) const; bool DumpDebugState(std::ostream& stream, bool includeDebugInfo) const; // FlushDestroyedComponents must be called before SerializeState (since the destruction queue // won't get serialized) bool SerializeState(std::ostream& stream) const; bool DeserializeState(std::istream& stream); std::string GenerateSchema() const; ScriptInterface& GetScriptInterface() { return m_ScriptInterface; } private: // Implementations of functions exposed to scripts - static void Script_RegisterComponentType_Common(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor, bool reRegister, bool systemComponent); - static void Script_RegisterComponentType(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor); - static void Script_RegisterSystemComponentType(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor); - static void Script_ReRegisterComponentType(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor); - static void Script_RegisterInterface(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name); - static void Script_RegisterMessageType(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name); - static void Script_RegisterGlobal(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name, JS::HandleValue value); - static IComponent* Script_QueryInterface(ScriptInterface::CmptPrivate* pCmptPrivate, int ent, int iid); - static std::vector Script_GetEntitiesWithInterface(ScriptInterface::CmptPrivate* pCmptPrivate, int iid); - static std::vector Script_GetComponentsWithInterface(ScriptInterface::CmptPrivate* pCmptPrivate, int iid); - static void Script_PostMessage(ScriptInterface::CmptPrivate* pCmptPrivate, int ent, int mtid, JS::HandleValue data); - static void Script_BroadcastMessage(ScriptInterface::CmptPrivate* pCmptPrivate, int mtid, JS::HandleValue data); - static int Script_AddEntity(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName); - static int Script_AddLocalEntity(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName); - static void Script_DestroyEntity(ScriptInterface::CmptPrivate* pCmptPrivate, int ent); - static void Script_FlushDestroyedEntities(ScriptInterface::CmptPrivate* pCmptPrivate); + void Script_RegisterComponentType_Common(int iid, const std::string& cname, JS::HandleValue ctor, bool reRegister, bool systemComponent); + void Script_RegisterComponentType(int iid, const std::string& cname, JS::HandleValue ctor); + void Script_RegisterSystemComponentType(int iid, const std::string& cname, JS::HandleValue ctor); + void Script_ReRegisterComponentType(int iid, const std::string& cname, JS::HandleValue ctor); + void Script_RegisterInterface(const std::string& name); + void Script_RegisterMessageType(const std::string& name); + void Script_RegisterGlobal(const std::string& name, JS::HandleValue value); + std::vector Script_GetEntitiesWithInterface(int iid); + std::vector Script_GetComponentsWithInterface(int iid); + void Script_PostMessage(int ent, int mtid, JS::HandleValue data); + void Script_BroadcastMessage(int mtid, JS::HandleValue data); + int Script_AddEntity(const std::wstring& templateName); + int Script_AddLocalEntity(const std::wstring& templateName); CMessage* ConstructMessage(int mtid, JS::HandleValue data); void SendGlobalMessage(entity_id_t ent, const CMessage& msg); void FlattenDynamicSubscriptions(); void RemoveComponentDynamicSubscriptions(IComponent* component); ComponentTypeId GetScriptWrapper(InterfaceId iid); CEntityHandle AllocateEntityHandle(entity_id_t ent); ScriptInterface m_ScriptInterface; CSimContext& m_SimContext; CEntityHandle m_SystemEntity; ComponentTypeId m_CurrentComponent; // used when loading component types bool m_CurrentlyHotloading; // TODO: some of these should be vectors std::map m_ComponentTypesById; std::vector m_ScriptedSystemComponents; std::vector > m_ComponentsByInterface; // indexed by InterfaceId std::map > m_ComponentsByTypeId; std::map > m_LocalMessageSubscriptions; std::map > m_GlobalMessageSubscriptions; std::map m_ComponentTypeIdsByName; std::map m_MessageTypeIdsByName; std::map m_MessageTypeNamesById; std::map m_InterfaceIdsByName; std::map m_DynamicMessageSubscriptionsNonsync; std::map > m_DynamicMessageSubscriptionsNonsyncByComponent; std::unordered_map m_ComponentCaches; // TODO: maintaining both ComponentsBy* is nasty; can we get rid of one, // while keeping QueryInterface and PostMessage sufficiently efficient? std::vector m_DestructionQueue; ComponentTypeId m_NextScriptComponentTypeId; entity_id_t m_NextEntityId; entity_id_t m_NextLocalEntityId; boost::rand48 m_RNG; friend class TestComponentManager; }; #endif // INCLUDED_COMPONENTMANAGER Index: ps/trunk/source/test_setup.cpp =================================================================== --- ps/trunk/source/test_setup.cpp (revision 24968) +++ ps/trunk/source/test_setup.cpp (revision 24969) @@ -1,158 +1,160 @@ /* 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 . */ // Got to be consistent with what the rest of the source files do before // including precompiled.h, so that the PCH works correctly #ifndef CXXTEST_RUNNING #define CXXTEST_RUNNING #endif #define _CXXTEST_HAVE_STD #include "precompiled.h" #include #include "lib/self_test.h" #include #if OS_WIN #include "lib/sysdep/os/win/wdbg_heap.h" #endif #include "lib/timer.h" #include "lib/sysdep/sysdep.h" #include "ps/Profiler2.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptEngine.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" class LeakReporter : public CxxTest::GlobalFixture { virtual bool tearDownWorld() { // Enable leak reporting on exit. // (This is done in tearDownWorld so that it doesn't report 'leaks' // if the program is aborted before finishing cleanly.) #if OS_WIN wdbg_heap_Enable(true); #endif return true; } virtual bool setUpWorld() { #if MSC_VERSION // (Warning: the allocation numbers seem to differ by 3 when you // run in the build process vs the debugger) // _CrtSetBreakAlloc(1952); #endif return true; } }; class MiscSetup : public CxxTest::GlobalFixture { virtual bool setUpWorld() { // Timer must be initialised, else things will break when tests do IO timer_Init(); #if OS_MACOSX || OS_BSD // See comment in GameSetup.cpp FixLocales setlocale(LC_CTYPE, "UTF-8"); #endif Threading::SetMainThread(); g_Profiler2.Initialise(); m_ScriptEngine = new ScriptEngine; g_ScriptContext = ScriptContext::CreateContext(); return true; } virtual bool tearDownWorld() { g_ScriptContext.reset(); SAFE_DELETE(m_ScriptEngine); g_Profiler2.Shutdown(); return true; } virtual bool setUp() { // Clean up any JS leftover between tests. g_ScriptContext->ShrinkingGC(); return true; } private: // We're doing the initialization and shutdown of the ScriptEngine explicitly here // to make sure it's only initialized when setUpWorld is called. ScriptEngine* m_ScriptEngine; }; static LeakReporter leakReporter; static MiscSetup miscSetup; // Definition of functions from lib/self_test.h bool ts_str_contains(const std::string& str1, const std::string& str2) { return str1.find(str2) != str1.npos; } bool ts_str_contains(const std::wstring& str1, const std::wstring& str2) { return str1.find(str2) != str1.npos; } // we need the (version-controlled) binaries/data directory because it // contains input files (it is assumed that developer's machines have // write access to those directories). note that argv0 isn't // available, so we use sys_ExecutablePathname. OsPath DataDir() { return sys_ExecutablePathname().Parent()/".."/"data"; } // Script-based testing setup: namespace { - void script_TS_FAIL(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& msg) + void script_TS_FAIL(const std::wstring& msg) { TS_FAIL(utf8_from_wstring(msg).c_str()); } } -void ScriptTestSetup(const ScriptInterface& scriptinterface) +void ScriptTestSetup(const ScriptInterface& scriptInterface) { - scriptinterface.RegisterFunction("TS_FAIL"); + ScriptRequest rq(scriptInterface); + ScriptFunction::Register(rq, "TS_FAIL"); // Load the TS_* function definitions // (We don't use VFS because tests might not have the normal VFS paths loaded) OsPath path = DataDir()/"tests"/"test_setup.js"; std::ifstream ifs(OsString(path).c_str()); ENSURE(ifs.good()); std::string content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); - ENSURE(scriptinterface.LoadScript(L"test_setup.js", content)); + ENSURE(scriptInterface.LoadScript(L"test_setup.js", content)); }