/* Copyright (C) 2010 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 . */ // This module defines the table of all functions callable from JS. // it's required by the interpreter; we make use of the opportunity to // document them all in one spot. we thus obviate having to dig through // all the other headers. most of the functions are implemented here; // as for the rest, we only link to their docs (duplication is bad). #include "precompiled.h" #include "ScriptGlue.h" #include "JSConversions.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/MapWriter.h" #include "graphics/Unit.h" #include "graphics/UnitManager.h" #include "graphics/scripting/JSInterface_Camera.h" #include "graphics/scripting/JSInterface_LightEnv.h" #include "gui/GUIManager.h" #include "gui/IGUIObject.h" #include "lib/frequency_filter.h" #include "lib/svn_revision.h" #include "lib/timer.h" #include "lib/utf8.h" #include "maths/scripting/JSInterface_Vector3D.h" #include "network/NetServer.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Game.h" #include "ps/Globals.h" // g_frequencyFilter #include "ps/GameSetup/GameSetup.h" #include "ps/Hotkey.h" #include "ps/ProfileViewer.h" #include "ps/World.h" #include "ps/scripting/JSInterface_Console.h" #include "ps/scripting/JSInterface_VFS.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #define LOG_CATEGORY L"script" // rationale: the function table is now at the end of the source file to // avoid the need for forward declarations for every function. // all normal function wrappers have the following signature: // JSBool func(JSContext* cx, JSObject* globalObject, uintN argc, jsval* argv, jsval* rval); // all property accessors have the following signature: // JSBool accessor(JSContext* cx, JSObject* globalObject, jsval id, jsval* vp); //----------------------------------------------------------------------------- // Timer //----------------------------------------------------------------------------- // Script profiling functions: Begin timing a piece of code with StartJsTimer(num) // and stop timing with StopJsTimer(num). The results will be printed to stdout // when the game exits. static const size_t MAX_JS_TIMERS = 20; static TimerUnit js_start_times[MAX_JS_TIMERS]; static TimerUnit js_timer_overhead; static TimerClient js_timer_clients[MAX_JS_TIMERS]; static wchar_t js_timer_descriptions_buf[MAX_JS_TIMERS * 12]; // depends on MAX_JS_TIMERS and format string below static void InitJsTimers() { wchar_t* pos = js_timer_descriptions_buf; for(size_t i = 0; i < MAX_JS_TIMERS; i++) { const wchar_t* description = pos; pos += swprintf_s(pos, 12, L"js_timer %d", (int)i)+1; timer_AddClient(&js_timer_clients[i], description); } // call several times to get a good approximation of 'hot' performance. // note: don't use a separate timer slot to warm up and then judge // overhead from another: that causes worse results (probably some // caching effects inside JS, but I don't entirely understand why). static const char* calibration_script = "startXTimer(0);\n" "stopXTimer (0);\n" "\n"; g_ScriptingHost.RunMemScript(calibration_script, strlen(calibration_script)); // slight hack: call RunMemScript twice because we can't average several // TimerUnit values because there's no operator/. this way is better anyway // because it hopefully avoids the one-time JS init overhead. g_ScriptingHost.RunMemScript(calibration_script, strlen(calibration_script)); js_timer_overhead = js_timer_clients[0].sum; js_timer_clients[0].sum.SetToZero(); } JSBool StartJsTimer(JSContext* cx, JSObject*, uintN argc, jsval* argv, jsval* rval) { ONCE(InitJsTimers()); JSU_REQUIRE_PARAMS(1); size_t slot = ToPrimitive(argv[0]); if (slot >= MAX_JS_TIMERS) return JS_FALSE; js_start_times[slot].SetFromTimer(); return JS_TRUE; } JSBool StopJsTimer(JSContext* cx, JSObject*, uintN argc, jsval* argv, jsval* rval) { JSU_REQUIRE_PARAMS(1); size_t slot = ToPrimitive(argv[0]); if (slot >= MAX_JS_TIMERS) return JS_FALSE; TimerUnit now; now.SetFromTimer(); now.Subtract(js_timer_overhead); BillingPolicy_Default()(&js_timer_clients[slot], js_start_times[slot], now); js_start_times[slot].SetToZero(); return JS_TRUE; } //----------------------------------------------------------------------------- // Game Setup //----------------------------------------------------------------------------- // Immediately ends the current game (if any). // params: // returns: JSBool EndGame(JSContext* cx, JSObject*, uintN argc, jsval* argv, jsval* rval) { JSU_REQUIRE_NO_PARAMS(); EndGame(); return JS_TRUE; } //----------------------------------------------------------------------------- // Misc. Engine Interface //----------------------------------------------------------------------------- // Return the global frames-per-second value. // params: // returns: FPS [int] // notes: // - This value is recalculated once a frame. We take special care to // filter it, so it is both accurate and free of jitter. JSBool GetFps( JSContext* cx, JSObject*, uintN argc, jsval* argv, jsval* rval ) { JSU_REQUIRE_NO_PARAMS(); int freq = 0; if (g_frequencyFilter) freq = g_frequencyFilter->StableFrequency(); *rval = INT_TO_JSVAL(freq); return JS_TRUE; } // Cause the game to exit gracefully. // params: // returns: // notes: // - Exit happens after the current main loop iteration ends // (since this only sets a flag telling it to end) JSBool ExitProgram( JSContext* cx, JSObject*, uintN argc, jsval* argv, jsval* rval ) { JSU_REQUIRE_NO_PARAMS(); kill_mainloop(); return JS_TRUE; } // Write an indication of total video RAM to console. // params: // returns: // notes: // - May not be supported on all platforms. // - Only a rough approximation; do not base low-level decisions // ("should I allocate one more texture?") on this. JSBool WriteVideoMemToConsole( JSContext* cx, JSObject*, uintN argc, jsval* argv, jsval* rval ) { JSU_REQUIRE_NO_PARAMS(); const SDL_VideoInfo* videoInfo = SDL_GetVideoInfo(); g_Console->InsertMessage(L"VRAM: total %d", videoInfo->video_mem); return JS_TRUE; } // Change the mouse cursor. // params: cursor name [string] (i.e. basename of definition file and texture) // returns: // notes: // - Cursors are stored in "art\textures\cursors" JSBool SetCursor( JSContext* cx, JSObject*, uintN argc, jsval* argv, jsval* rval ) { JSU_REQUIRE_PARAMS(1); g_CursorName = g_ScriptingHost.ValueToUCString(argv[0]); return JS_TRUE; } // Change the LOD bias. // params: LOD bias [float] // returns: // notes: // - value is as required by GL_TEXTURE_LOD_BIAS. // - useful for adjusting image "sharpness" (since it affects which mipmap level is chosen) JSBool _LodBias( JSContext* cx, JSObject*, uintN argc, jsval* argv, jsval* rval ) { JSU_REQUIRE_PARAMS(1); g_Renderer.SetOptionFloat(CRenderer::OPT_LODBIAS, ToPrimitive(argv[0])); return JS_TRUE; } JSBool GetGUIObjectByName(JSContext* cx, JSObject* UNUSED(obj), uintN UNUSED(argc), jsval* argv, jsval* rval) { try { CStr name = ToPrimitive(cx, argv[0]); IGUIObject* guiObj = g_GUI->FindObjectByName(name); if (guiObj) *rval = OBJECT_TO_JSVAL(guiObj->GetJSObject()); else *rval = JSVAL_NULL; return JS_TRUE; } catch (PSERROR_Scripting&) { return JS_FALSE; } } //----------------------------------------------------------------------------- // Miscellany //----------------------------------------------------------------------------- // Return the date/time at which the current executable was compiled. // params: none (-> "date time (svn revision)") OR an integer specifying // what to display: 0 for date, 1 for time, 2 for svn revision // returns: string with the requested timestamp info // notes: // - Displayed on main menu screen; tells non-programmers which auto-build // they are running. Could also be determined via .EXE file properties, // but that's a bit more trouble. // - To be exact, the date/time returned is when scriptglue.cpp was // last compiled, but the auto-build does full rebuilds. // - svn revision is generated by calling svnversion and cached in // lib/svn_revision.cpp. it is useful to know when attempting to // reproduce bugs (the main EXE and PDB should be temporarily reverted to // that revision so that they match user-submitted crashdumps). JSBool GetBuildTimestamp( JSContext* cx, JSObject*, uintN argc, jsval* argv, jsval* rval ) { JSU_REQUIRE_MAX_PARAMS(1); char buf[200]; // see function documentation const int mode = argc? JSVAL_TO_INT(argv[0]) : -1; switch(mode) { case -1: sprintf_s(buf, ARRAY_SIZE(buf), "%s %s (%ls)", __DATE__, __TIME__, svn_revision); break; case 0: sprintf_s(buf, ARRAY_SIZE(buf), "%s", __DATE__); break; case 1: sprintf_s(buf, ARRAY_SIZE(buf), "%s", __TIME__); break; case 2: sprintf_s(buf, ARRAY_SIZE(buf), "%ls", svn_revision); break; } *rval = STRING_TO_JSVAL(JS_NewStringCopyZ(cx, buf)); return JS_TRUE; } #ifdef DEBUG void DumpHeap(const char* name, int idx, JSContext* cx) { wchar_t buf[64]; swprintf_s(buf, ARRAY_SIZE(buf), L"%hs.%03d.txt", name, idx); fs::wpath path(psLogDir()/buf); FILE* f = fopen(utf8_from_wstring(path.string()).c_str(), "w"); debug_assert(f); JS_DumpHeap(cx, f, NULL, 0, NULL, (size_t)-1, NULL); fclose(f); } #endif JSBool DumpHeaps(JSContext* UNUSED(cx), JSObject* UNUSED(globalObject), uintN UNUSED(argc), jsval* UNUSED(argv), jsval* UNUSED(rval) ) { #ifdef DEBUG static int i = 0; if (ScriptingHost::IsInitialised()) DumpHeap("gui", i, g_ScriptingHost.GetContext()); if (g_Game) DumpHeap("sim", i, g_Game->GetSimulation2()->GetScriptInterface().GetContext()); ++i; #else debug_warn(L"DumpHeaps only available in DEBUG mode"); #endif return JS_TRUE; } //----------------------------------------------------------------------------- // Is the game paused? JSBool IsPaused( JSContext* cx, JSObject* UNUSED(globalObject), uintN argc, jsval* argv, jsval* rval ) { JSU_REQUIRE_NO_PARAMS(); if( !g_Game ) { JS_ReportError( cx, "Game is not started" ); return JS_FALSE; } *rval = g_Game->m_Paused ? JSVAL_TRUE : JSVAL_FALSE; return JS_TRUE ; } // Pause/unpause the game JSBool SetPaused( JSContext* cx, JSObject* UNUSED(globalObject), uintN argc, jsval* argv, jsval* rval ) { JSU_REQUIRE_PARAMS( 1 ); if( !g_Game ) { JS_ReportError( cx, "Game is not started" ); return JS_FALSE; } try { g_Game->m_Paused = ToPrimitive( argv[0] ); } catch( PSERROR_Scripting_ConversionFailed ) { JS_ReportError( cx, "Invalid parameter to SetPaused" ); } return JS_TRUE; } //----------------------------------------------------------------------------- // function table //----------------------------------------------------------------------------- // the JS interpreter expects the table to contain 5-tuples as follows: // - name the function will be called as from script; // - function which will be called; // - number of arguments this function expects // - Flags (deprecated, always zero) // - Extra (reserved for future use, always zero) // // we simplify this a bit with a macro: #define JS_FUNC(script_name, cpp_function, min_params) { script_name, cpp_function, min_params, 0, 0 }, JSFunctionSpec ScriptFunctionTable[] = { // Profiling JS_FUNC("startXTimer", StartJsTimer, 1) JS_FUNC("stopXTimer", StopJsTimer, 1) // Game Setup JS_FUNC("endGame", EndGame, 0) // VFS (external) JS_FUNC("buildDirEntList", JSI_VFS::BuildDirEntList, 1) JS_FUNC("getFileMTime", JSI_VFS::GetFileMTime, 1) JS_FUNC("getFileSize", JSI_VFS::GetFileSize, 1) JS_FUNC("readFile", JSI_VFS::ReadFile, 1) JS_FUNC("readFileLines", JSI_VFS::ReadFileLines, 1) JS_FUNC("archiveBuilderCancel", JSI_VFS::ArchiveBuilderCancel, 1) // Misc. Engine Interface JS_FUNC("exit", ExitProgram, 0) JS_FUNC("isPaused", IsPaused, 0) JS_FUNC("setPaused", SetPaused, 1) JS_FUNC("vmem", WriteVideoMemToConsole, 0) JS_FUNC("_lodBias", _LodBias, 0) JS_FUNC("setCursor", SetCursor, 1) JS_FUNC("getFPS", GetFps, 0) JS_FUNC("getGUIObjectByName", GetGUIObjectByName, 1) // Miscellany JS_FUNC("buildTime", GetBuildTimestamp, 0) JS_FUNC("dumpHeaps", DumpHeaps, 0) // end of table marker {0, 0, 0, 0, 0} }; #undef JS_FUNC //----------------------------------------------------------------------------- // property accessors //----------------------------------------------------------------------------- JSBool GetGameView( JSContext* UNUSED(cx), JSObject* UNUSED(obj), jsval UNUSED(id), jsval* vp ) { if (g_Game) *vp = OBJECT_TO_JSVAL( g_Game->GetView()->GetScript() ); else *vp = JSVAL_NULL; return( JS_TRUE ); } JSBool GetRenderer( JSContext* UNUSED(cx), JSObject* UNUSED(obj), jsval UNUSED(id), jsval* vp ) { if (CRenderer::IsInitialised()) *vp = OBJECT_TO_JSVAL( g_Renderer.GetScript() ); else *vp = JSVAL_NULL; return( JS_TRUE ); } enum ScriptGlobalTinyIDs { GLOBAL_SELECTION, GLOBAL_GROUPSARRAY, GLOBAL_CAMERA, GLOBAL_CONSOLE, GLOBAL_LIGHTENV }; JSPropertySpec ScriptGlobalTable[] = { { "camera" , GLOBAL_CAMERA, JSPROP_PERMANENT, JSI_Camera::getCamera, JSI_Camera::setCamera }, { "console" , GLOBAL_CONSOLE, JSPROP_PERMANENT|JSPROP_READONLY, JSI_Console::getConsole, 0 }, { "lightenv" , GLOBAL_LIGHTENV, JSPROP_PERMANENT, JSI_LightEnv::getLightEnv, JSI_LightEnv::setLightEnv }, { "gameView" , 0, JSPROP_PERMANENT|JSPROP_READONLY, GetGameView, 0 }, { "renderer" , 0, JSPROP_PERMANENT|JSPROP_READONLY, GetRenderer, 0 }, // end of table marker { 0, 0, 0, 0, 0 }, };