Index: binaries/data/mods/_test.sim/simulation/components/test-modding1.js =================================================================== --- /dev/null +++ binaries/data/mods/_test.sim/simulation/components/test-modding1.js @@ -0,0 +1,13 @@ +function Modding() {} + +Modding.prototype.Schema = ""; + +Modding.prototype.Init = function() { + this.x = +this.template.x; +}; + +Modding.prototype.GetX = function() { + return this.x; +}; + +Engine.RegisterComponentType(IID_Test1, "Modding", Modding); Index: binaries/data/mods/_test.sim/simulation/components/test-modding2.js =================================================================== --- /dev/null +++ binaries/data/mods/_test.sim/simulation/components/test-modding2.js @@ -0,0 +1,5 @@ +Modding.prototype.GetX = function() { + return this.x*10; +}; + +Engine.ReRegisterComponentType(IID_Test1, "Modding", Modding); Index: source/ps/GameSetup/GameSetup.cpp =================================================================== --- source/ps/GameSetup/GameSetup.cpp +++ source/ps/GameSetup/GameSetup.cpp @@ -183,8 +183,8 @@ // display progress / description in loading screen void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task) { - g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_Progress", percent, true, false, true); - g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_LoadDescription", pending_task, true, false, true); + g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_Progress", percent, false, true); + g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_LoadDescription", pending_task, false, true); g_GUI->GetActiveGUI()->SendEventToAll("progress"); } Index: source/scriptinterface/ScriptInterface.h =================================================================== --- source/scriptinterface/ScriptInterface.h +++ source/scriptinterface/ScriptInterface.h @@ -138,11 +138,12 @@ /** * Set the named property on the global object. - * If @p replace is true, an existing property will be overwritten; otherwise attempts - * to set an already-defined value will fail. + * Optionally makes it {!ReadOnly, !DontDelete, DontEnum}. + * If @p hotloadable is true and @p constant is true, we will not make it DontDelete. + * This will allow us to override its value during hotload (i.e. when @p currentlyHotloading is true). */ template - bool SetGlobal(const char* name, const T& value, bool replace = false, bool constant = true, bool enumerate = true); + bool SetGlobal(const char* name, const T& value, bool constant = true, bool enumerate = true, bool hotloadable = false, bool currentlyHotloading = false); /** * Set the named property on the given object. @@ -362,7 +363,7 @@ bool CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) const; bool Eval_(const char* code, JS::MutableHandleValue ret) const; bool Eval_(const wchar_t* code, JS::MutableHandleValue ret) const; - bool SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate); + bool SetGlobal_(const char* name, JS::HandleValue value, bool constant, bool enumerate, bool hotloadable, bool currentlyHotloading); bool SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool readonly, bool enumerate) const; bool SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool readonly, bool enumerate) const; bool SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool readonly, bool enumerate) const; @@ -488,12 +489,12 @@ } template -bool ScriptInterface::SetGlobal(const char* name, const T& value, bool replace, bool constant, bool enumerate) +bool ScriptInterface::SetGlobal(const char* name, const T& value, bool constant, bool enumerate, bool hotloadable, bool currentlyHotloading) { JSAutoRequest rq(GetContext()); JS::RootedValue val(GetContext()); AssignOrToJSVal(GetContext(), &val, value); - return SetGlobal_(name, val, replace, constant, enumerate); + return SetGlobal_(name, val, constant, enumerate, hotloadable, currentlyHotloading); } template Index: source/scriptinterface/ScriptInterface.cpp =================================================================== --- source/scriptinterface/ScriptInterface.cpp +++ source/scriptinterface/ScriptInterface.cpp @@ -602,25 +602,47 @@ return JS::ObjectValue(*JS::CurrentGlobalOrNull(m->m_cx)); } -bool ScriptInterface::SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate) +bool ScriptInterface::SetGlobal_(const char* name, JS::HandleValue value, bool constant, bool enumerate, bool hotloadable, bool currentlyHotloading) { JSAutoRequest rq(m->m_cx); JS::RootedObject global(m->m_cx, m->m_glob); - if (!replace) + + bool found; + if (!JS_HasProperty(m->m_cx, global, name, &found)) + return false; + if (found) { - bool found; - if (!JS_HasProperty(m->m_cx, global, name, &found)) + JS::Rooted desc(m->m_cx); + if (!JS_GetOwnPropertyDescriptor(m->m_cx, global, name, &desc)) return false; - if (found) + + if (desc.isReadonly()) { - JS_ReportError(m->m_cx, "SetGlobal \"%s\" called multiple times", name); - return false; + if (!currentlyHotloading) + { + JS_ReportError(m->m_cx, "SetGlobal \"%s\" called multiple times", name); + return false; + } + + if (desc.isPermanent()) + { + JS_ReportError(m->m_cx, "The global \"%s\" cannot be modified during hotload. Please restart the engine.", name); + return false; + } + + LOGMESSAGE("Hotloading new value for global \"%s\".", name); + ENSURE(JS_DeleteProperty(m->m_cx, global, name)); } } uint attrs = 0; if (constant) - attrs |= JSPROP_READONLY | JSPROP_PERMANENT; + { + if (hotloadable) + attrs |= JSPROP_READONLY; + else + attrs |= JSPROP_READONLY | JSPROP_PERMANENT; + } if (enumerate) attrs |= JSPROP_ENUMERATE; Index: source/scriptinterface/tests/test_ScriptInterface.h =================================================================== --- source/scriptinterface/tests/test_ScriptInterface.h +++ source/scriptinterface/tests/test_ScriptInterface.h @@ -254,4 +254,27 @@ TS_ASSERT(script.ParseJSON(stringified, &val)); TS_ASSERT_STR_EQUALS(script.ToString(&val), "({x:1, z:[2, \"3\\u263A\\uFFFD\"], y:true})"); } + + // This function tests a common way to mod functions, by crating a wrapper that + // extends the functionality and is then assigned to the name of the function. + void test_function_override() + { + ScriptInterface script("Test", "Test", g_ScriptRuntime); + JSContext* cx = script.GetContext(); + JSAutoRequest rq(cx); + + TS_ASSERT(script.Eval( + "function f() { return 1; }" + "f = (function (originalFunction) {" + "return function () { return originalFunction() + 1; }" + "})(f);" + )); + + JS::RootedValue out(cx); + TS_ASSERT(script.Eval("f()", &out)); + + int outNbr = 0; + ScriptInterface::FromJSVal(cx, out, outNbr); + TS_ASSERT_EQUALS(2, outNbr); + } }; Index: source/simulation2/system/ComponentManager.cpp =================================================================== --- source/simulation2/system/ComponentManager.cpp +++ source/simulation2/system/ComponentManager.cpp @@ -145,6 +145,8 @@ return false; std::string content = file.DecodeUTF8(); // assume it's UTF-8 bool ok = m_ScriptInterface.LoadScript(filename, content); + + m_CurrentlyHotloading = false; return ok; } @@ -322,14 +324,14 @@ { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); componentManager->Script_RegisterComponentType_Common(pCxPrivate, iid, cname, ctor, false, false); - componentManager->m_ScriptInterface.SetGlobal(cname.c_str(), ctor, componentManager->m_CurrentlyHotloading); + componentManager->m_ScriptInterface.SetGlobal(cname.c_str(), ctor, true, true, true, componentManager->m_CurrentlyHotloading); } void CComponentManager::Script_RegisterSystemComponentType(ScriptInterface::CxPrivate* pCxPrivate, int iid, const std::string& cname, JS::HandleValue ctor) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); componentManager->Script_RegisterComponentType_Common(pCxPrivate, iid, cname, ctor, false, true); - componentManager->m_ScriptInterface.SetGlobal(cname.c_str(), ctor, componentManager->m_CurrentlyHotloading); + componentManager->m_ScriptInterface.SetGlobal(cname.c_str(), ctor, true, true, true, componentManager->m_CurrentlyHotloading); } void CComponentManager::Script_ReRegisterComponentType(ScriptInterface::CxPrivate* pCxPrivate, int iid, const std::string& cname, JS::HandleValue ctor) @@ -387,10 +389,7 @@ void CComponentManager::Script_RegisterGlobal(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name, JS::HandleValue value) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); - - // Set the value, and accept duplicates only if hotloading (otherwise it's an error, - // in order to detect accidental duplicate definitions of globals) - componentManager->m_ScriptInterface.SetGlobal(name.c_str(), value, componentManager->m_CurrentlyHotloading); + componentManager->m_ScriptInterface.SetGlobal(name.c_str(), value, true, true, true, componentManager->m_CurrentlyHotloading); } IComponent* CComponentManager::Script_QueryInterface(ScriptInterface::CxPrivate* pCxPrivate, int ent, int iid) Index: source/simulation2/tests/test_ComponentManager.h =================================================================== --- source/simulation2/tests/test_ComponentManager.h +++ source/simulation2/tests/test_ComponentManager.h @@ -617,6 +617,31 @@ TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 200); } + void test_script_modding() + { + CSimContext context; + CComponentManager man(context, g_ScriptRuntime); + man.LoadComponentTypes(); + + CParamNode testParam; + TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "100"), PSRETURN_OK); + + entity_id_t ent1 = 1, ent2 = 2; + CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); + CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); + + TS_ASSERT(man.LoadScript(L"simulation/components/test-modding1.js")); + + man.AddComponent(hnd1, man.LookupCID("Modding"), testParam); + TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); + + TS_ASSERT(man.LoadScript(L"simulation/components/test-modding2.js")); + + TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1000); + man.AddComponent(hnd2, man.LookupCID("Modding"), testParam); + TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1000); + } + void test_serialization() { CSimContext context;