diff --git a/source/scriptinterface/ScriptConversions.cpp b/source/scriptinterface/ScriptConversions.cpp index 6bc3706517..d55165ce5b 100644 --- a/source/scriptinterface/ScriptConversions.cpp +++ b/source/scriptinterface/ScriptConversions.cpp @@ -49,6 +49,16 @@ template<> bool ScriptInterface::FromJSVal(JSContext* cx, jsval v, float& return true; } +template<> bool ScriptInterface::FromJSVal(JSContext* cx, jsval v, double& out) +{ + jsdouble ret; + WARN_IF_NOT(JSVAL_IS_NUMBER(v)); + if (!JS_ValueToNumber(cx, v, &ret)) + return false; + out = ret; + return true; +} + template<> bool ScriptInterface::FromJSVal(JSContext* cx, jsval v, i32& out) { int32 ret; @@ -146,6 +156,13 @@ template<> jsval ScriptInterface::ToJSVal(JSContext* cx, const float& val return rval; } +template<> jsval ScriptInterface::ToJSVal(JSContext* cx, const double& val) +{ + jsval rval = JSVAL_VOID; + JS_NewNumberValue(cx, val, &rval); // ignore return value + return rval; +} + template<> jsval ScriptInterface::ToJSVal(JSContext* cx, const i32& val) { if (INT_FITS_IN_JSVAL(val)) diff --git a/source/scriptinterface/ScriptInterface.cpp b/source/scriptinterface/ScriptInterface.cpp index 72cab52897..33c9f24d82 100644 --- a/source/scriptinterface/ScriptInterface.cpp +++ b/source/scriptinterface/ScriptInterface.cpp @@ -29,6 +29,9 @@ #include #include +#include +#include +#include #include "valgrind.h" @@ -110,6 +113,28 @@ JSBool print(JSContext* cx, JSObject* UNUSED(obj), uintN argc, jsval* argv, jsva return JS_TRUE; } +// Math override functions: + +JSBool Math_random(JSContext* cx, uintN UNUSED(argc), jsval* vp) +{ + // Grab the RNG that was hidden in our slot + jsval rngp; + if (!JS_GetReservedSlot(cx, JSVAL_TO_OBJECT(JS_CALLEE(cx, vp)), 0, &rngp)) + return JS_FALSE; + boost::rand48* rng = static_cast(JSVAL_TO_PRIVATE(rngp)); + + // TODO: is the double generation sufficiently deterministic for us? + boost::uniform_real dist; + boost::variate_generator > gen(*rng, dist); + double r = gen(); + + jsval rv; + if (!JS_NewNumberValue(cx, r, &rv)) + return JS_FALSE; + JS_SET_RVAL(cx, vp, rv); + return JS_TRUE; +} + } // anonymous namespace #if ENABLE_SCRIPT_PROFILING @@ -233,6 +258,26 @@ void* ScriptInterface::GetCallbackData(JSContext* cx) return JS_GetContextPrivate(cx); } +void ScriptInterface::ReplaceNondeterministicFunctions(boost::rand48& rng) +{ + jsval math; + if (!JS_GetProperty(m->m_cx, m->m_glob, "Math", &math) || !JSVAL_IS_OBJECT(math)) + { + LOGERROR(L"ReplaceNondeterministicFunctions: failed to get Math"); + return; + } + + JSFunction* random = JS_DefineFunction(m->m_cx, JSVAL_TO_OBJECT(math), "random", (JSNative)Math_random, 0, + JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT | JSFUN_FAST_NATIVE); + if (!random) + { + LOGERROR(L"ReplaceNondeterministicFunctions: failed to replace Math.random"); + return; + } + // Store the RNG in a slot which is sort-of-guaranteed to be unused by the JS engine + JS_SetReservedSlot(m->m_cx, JS_GetFunctionObject(random), 0, PRIVATE_TO_JSVAL(&rng)); +} + void ScriptInterface::Register(const char* name, JSNative fptr, size_t nargs) { m->Register(name, fptr, (uintN)nargs); diff --git a/source/scriptinterface/ScriptInterface.h b/source/scriptinterface/ScriptInterface.h index a1926b60d1..89506a87da 100644 --- a/source/scriptinterface/ScriptInterface.h +++ b/source/scriptinterface/ScriptInterface.h @@ -29,6 +29,8 @@ #include "ScriptTypes.h" #include "ScriptVal.h" +namespace boost { class rand48; } + // 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) @@ -62,6 +64,8 @@ public: JSContext* GetContext() const; + void ReplaceNondeterministicFunctions(boost::rand48& rng); + /** * Call a constructor function, roughly equivalent to JS "new ctor". * diff --git a/source/scriptinterface/tests/test_ScriptInterface.h b/source/scriptinterface/tests/test_ScriptInterface.h index f19ee0e8b2..984b476f04 100644 --- a/source/scriptinterface/tests/test_ScriptInterface.h +++ b/source/scriptinterface/tests/test_ScriptInterface.h @@ -21,6 +21,8 @@ #include "ps/CLogger.h" +#include + class TestScriptInterface : public CxxTest::TestSuite { public: @@ -103,4 +105,24 @@ public: TS_ASSERT(script2.CallFunction(obj2.get(), "toSource", source)); TS_ASSERT_STR_EQUALS(source, "({a:#1=[#1#], b:#1#})"); } + + void test_random() + { + ScriptInterface script("Test"); + + double d1, d2; + TS_ASSERT(script.Eval("Math.random()", d1)); + TS_ASSERT(script.Eval("Math.random()", d2)); + TS_ASSERT_DIFFERS(d1, d2); + + boost::rand48 rng; + script.ReplaceNondeterministicFunctions(rng); + rng.seed(0); + TS_ASSERT(script.Eval("Math.random()", d1)); + TS_ASSERT(script.Eval("Math.random()", d2)); + TS_ASSERT_DIFFERS(d1, d2); + rng.seed(0); + TS_ASSERT(script.Eval("Math.random()", d2)); + TS_ASSERT_EQUALS(d1, d2); + } }; diff --git a/source/simulation2/system/ComponentManager.cpp b/source/simulation2/system/ComponentManager.cpp index 9ab72f3f8e..b40f2f0d0f 100644 --- a/source/simulation2/system/ComponentManager.cpp +++ b/source/simulation2/system/ComponentManager.cpp @@ -36,6 +36,9 @@ CComponentManager::CComponentManager(CSimContext& context, bool skipScriptFuncti m_ScriptInterface.SetCallbackData(static_cast (this)); + // TODO: ought to seed the RNG (in a network-synchronised way) before we use it + m_ScriptInterface.ReplaceNondeterministicFunctions(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) diff --git a/source/simulation2/system/ComponentManager.h b/source/simulation2/system/ComponentManager.h index 9e1f557b16..d19e842b68 100644 --- a/source/simulation2/system/ComponentManager.h +++ b/source/simulation2/system/ComponentManager.h @@ -22,6 +22,8 @@ #include "Components.h" #include "scriptinterface/ScriptInterface.h" +#include + #include class IComponent; @@ -250,6 +252,8 @@ private: entity_id_t m_NextEntityId; entity_id_t m_NextLocalEntityId; + boost::rand48 m_RNG; + friend class TestComponentManager; };