Index: binaries/data/mods/_test.sim/simulation/components/test-serialize.js =================================================================== --- binaries/data/mods/_test.sim/simulation/components/test-serialize.js +++ binaries/data/mods/_test.sim/simulation/components/test-serialize.js @@ -7,7 +7,6 @@ }; TestScript1_values.prototype.GetX = function() { -// print(uneval(this)); return this.x; }; @@ -64,6 +63,10 @@ return {c:1}; }; +TestScript1_custom.prototype.Deserialize = function(data) { + this.c = data.c; +}; + Engine.RegisterComponentType(IID_Test1, "TestScript1_custom", TestScript1_custom); // -------- // Index: binaries/data/mods/public/globalscripts/tests/test_vector.js =================================================================== --- /dev/null +++ binaries/data/mods/public/globalscripts/tests/test_vector.js @@ -0,0 +1,11 @@ +function test_serialization() +{ + warn("start vec"); + let test_val = new Vector2D(1, 2); + let rt = Engine.SerializationRoundTrip(test_val); + TS_ASSERT_EQUALS(test_val.constructor, rt.constructor); + TS_ASSERT_EQUALS(rt.add(test_val).x, 2); + warn("end vec"); +} + +test_serialization(); Index: binaries/data/mods/public/globalscripts/vector.js =================================================================== --- binaries/data/mods/public/globalscripts/vector.js +++ binaries/data/mods/public/globalscripts/vector.js @@ -18,6 +18,7 @@ return new Vector2D(this.x, this.y); }; + // Mutating 2D functions // // These functions modify the current object, Index: binaries/data/mods/public/simulation/components/Player.js =================================================================== --- binaries/data/mods/public/simulation/components/Player.js +++ binaries/data/mods/public/simulation/components/Player.js @@ -36,6 +36,12 @@ return state; }; +Player.prototype.Deserialize = function(state) +{ + for (let prop in state) + this[prop] = state[prop]; +}; + /** * Which units will be shown with special icons at the top. */ Index: source/scriptinterface/ScriptInterface.h =================================================================== --- source/scriptinterface/ScriptInterface.h +++ source/scriptinterface/ScriptInterface.h @@ -119,6 +119,13 @@ */ bool ReplaceNondeterministicRNG(boost::rand48& rng); + /** + * Get the object constructor from the global scope or any lexical scope. + * @param constructorName Name of the constructor. + * @param out The constructor object or null. + */ + bool GetConstructor(const std::string& constructorName, JS::MutableHandleValue out) const; + /** * Call a constructor function, equivalent to JS "new ctor(arg)". * @param ctor An object that can be used as constructor Index: source/scriptinterface/ScriptInterface.cpp =================================================================== --- source/scriptinterface/ScriptInterface.cpp +++ source/scriptinterface/ScriptInterface.cpp @@ -497,6 +497,38 @@ return m->m_runtime; } +bool ScriptInterface::GetConstructor(const std::string& constructorName, JS::MutableHandleValue out) const +{ + JSAutoRequest rq(m->m_cx); + + // Try to get the object as a property of the global object. + JS::RootedObject global(m->m_cx, &GetGlobalObject().toObject()); + if (!JS_GetProperty(m->m_cx, global, constructorName.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_scopes(m->m_cx, JS_GlobalLexicalScope(global.get())); + if (!JS_GetProperty(m->m_cx, lexical_scopes, constructorName.c_str(), out)) + { + out.set(JS::NullHandleValue); + return false; + } + + if (!out.isNullOrUndefined()) + return true; + + out.set(JS::NullHandleValue); + return false; +} + void ScriptInterface::CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const { JSAutoRequest rq(m->m_cx); Index: source/simulation2/components/tests/test_scripts.h =================================================================== --- source/simulation2/components/tests/test_scripts.h +++ source/simulation2/components/tests/test_scripts.h @@ -16,6 +16,8 @@ */ #include "simulation2/system/ComponentTest.h" +#include "simulation2/serialization/StdDeserializer.h" +#include "simulation2/serialization/StdSerializer.h" #include "ps/Filesystem.h" @@ -56,6 +58,21 @@ TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname)); } + static JS::Value Script_SerializationRoundTrip(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue value) + { + JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); + JSAutoRequest rq(cx); + + JS::RootedValue val(cx); + val = value; + std::stringstream stream; + CStdSerializer serializer(*pCxPrivate->pScriptInterface, stream); + serializer.ScriptVal("", &val); + CStdDeserializer deserializer(*pCxPrivate->pScriptInterface, stream); + deserializer.ScriptVal("", &val); + return val; + } + void test_global_scripts() { if (!VfsDirectoryExists(L"globalscripts/tests/")) @@ -71,6 +88,9 @@ CSimContext context; CComponentManager componentManager(context, g_ScriptRuntime, true); ScriptTestSetup(componentManager.GetScriptInterface()); + + componentManager.GetScriptInterface().RegisterFunction ("SerializationRoundTrip"); + load_script(componentManager.GetScriptInterface(), path); } } @@ -96,6 +116,7 @@ componentManager.GetScriptInterface().RegisterFunction ("LoadComponentScript"); componentManager.GetScriptInterface().RegisterFunction ("LoadHelperScript"); + componentManager.GetScriptInterface().RegisterFunction ("SerializationRoundTrip"); componentManager.LoadComponentTypes(); Index: source/simulation2/scripting/ScriptComponent.cpp =================================================================== --- source/simulation2/scripting/ScriptComponent.cpp +++ source/simulation2/scripting/ScriptComponent.cpp @@ -69,25 +69,13 @@ void CComponentTypeScript::Serialize(ISerializer& serialize) { // If the component set Serialize = null, then do no work here - if (m_HasNullSerialize) - return; + //if (m_HasNullSerialize) + // return; JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); - // Support a custom "Serialize" function, which returns a new object that will be - // serialized instead of the component itself - if (m_HasCustomSerialize) - { - JS::RootedValue val(cx); - if (!m_ScriptInterface.CallFunction(m_Instance, "Serialize", &val)) - LOGERROR("Script Serialize call failed"); - serialize.ScriptVal("object", &val); - } - else - { - serialize.ScriptVal("object", &m_Instance); - } + serialize.ScriptVal("comp", &m_Instance); } void CComponentTypeScript::Deserialize(const CParamNode& paramNode, IDeserializer& deserialize, entity_id_t ent) @@ -95,29 +83,9 @@ JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); + //if (!m_HasNullSerialize) + deserialize.ScriptVal("comp", &m_Instance); + m_ScriptInterface.SetProperty(m_Instance, "entity", (int)ent, true, false); m_ScriptInterface.SetProperty(m_Instance, "template", paramNode, true, false); - - // Support a custom "Deserialize" function, to which we pass the deserialized data - // instead of automatically adding the deserialized properties onto the object - if (m_HasCustomDeserialize) - { - JS::RootedValue val(cx); - - // If Serialize = null, we'll still call Deserialize but with undefined argument - if (!m_HasNullSerialize) - deserialize.ScriptVal("object", &val); - - if (!m_ScriptInterface.CallFunctionVoid(m_Instance, "Deserialize", val)) - LOGERROR("Script Deserialize call failed"); - } - else - { - if (!m_HasNullSerialize) - { - // Use ScriptObjectAppend so we don't lose the carefully-constructed - // prototype/parent of this object - deserialize.ScriptObjectAppend("object", m_Instance); - } - } } Index: source/simulation2/serialization/BinarySerializer.cpp =================================================================== --- source/simulation2/serialization/BinarySerializer.cpp +++ source/simulation2/serialization/BinarySerializer.cpp @@ -155,30 +155,31 @@ { // Object class - check for user-defined prototype JS::RootedObject proto(cx); - JS_GetPrototype(cx, obj, &proto); - if (!proto) + if (!JS_GetPrototype(cx, obj, &proto)) throw PSERROR_Serialize_ScriptError("JS_GetPrototype failed"); - if (m_SerializablePrototypes->empty() || !IsSerializablePrototype(proto)) - { - // Standard Object prototype + CStrW protoName; + JS::RootedValue constructor(cx, JS::ObjectOrNullValue(JS_GetConstructor(cx, proto))); + if (!m_ScriptInterface.GetProperty(constructor, "name", protoName)) + throw PSERROR_Serialize_ScriptError("Could not get constructor name."); + + if (protoName == L"Object") m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT); - - // TODO: maybe we should throw an error for unrecognized non-Object prototypes? - // (requires fixing AI serialization first and excluding component scripts) - } else { - // User-defined custom prototype + u32 nbArgs; + m_ScriptInterface.GetProperty(constructor, "length", nbArgs); + // We cannot recreate objects if their constructor takes mandatory arguments. + if (nbArgs != 0) + throw PSERROR_Serialize_ScriptError("Cannot serialize objects with no default constructors."); + m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_PROTOTYPE); - - const std::wstring prototypeName = GetPrototypeName(proto); - m_Serializer.String("proto name", prototypeName, 0, 256); + m_Serializer.String("proto", protoName, 0, 256); // Does it have custom Serialize function? // if so, we serialize the data it returns, rather than the object's properties directly - bool hasCustomSerialize; - if (!JS_HasProperty(cx, obj, "Serialize", &hasCustomSerialize)) + bool hasCustomDeserialize, hasCustomSerialize; + if (!JS_HasProperty(cx, obj, "Serialize", &hasCustomSerialize) || !JS_HasProperty(cx, obj, "Deserialize", &hasCustomDeserialize)) throw PSERROR_Serialize_ScriptError("JS_HasProperty failed"); if (hasCustomSerialize) @@ -190,10 +191,13 @@ // If serialize is null, so don't serialize anything more if (!serialize.isNull()) { + if (!hasCustomDeserialize) + throw PSERROR_Serialize_ScriptError("Cannot serialize script with non-null Serialize but no Deserialize."); + JS::RootedValue data(cx); if (!m_ScriptInterface.CallFunction(val, "Serialize", &data)) throw PSERROR_Serialize_ScriptError("Prototype Serialize function failed"); - HandleScriptVal(data); + m_Serializer.ScriptVal("ser_obj", &data); } break; } Index: source/simulation2/serialization/IDeserializer.h =================================================================== --- source/simulation2/serialization/IDeserializer.h +++ source/simulation2/serialization/IDeserializer.h @@ -59,7 +59,7 @@ virtual void ScriptVal(const char* name, JS::MutableHandleValue out) = 0; /// Deserialize an object value, appending properties to object 'objVal' - virtual void ScriptObjectAppend(const char* name, JS::HandleValue objVal) = 0; + virtual void ScriptObjectAppendProperties(const char* name, JS::HandleValue objToAppendTo) = 0; /// Deserialize a JSString virtual void ScriptString(const char* name, JS::MutableHandleString out) = 0; Index: source/simulation2/serialization/StdDeserializer.h =================================================================== --- source/simulation2/serialization/StdDeserializer.h +++ source/simulation2/serialization/StdDeserializer.h @@ -32,7 +32,8 @@ virtual ~CStdDeserializer(); virtual void ScriptVal(const char* name, JS::MutableHandleValue out); - virtual void ScriptObjectAppend(const char* name, JS::HandleValue objVal); + // + virtual void ScriptObjectAppendProperties(const char* name, JS::HandleValue objToAppendTo); virtual void ScriptString(const char* name, JS::MutableHandleString out); virtual std::istream& GetStream(); @@ -48,7 +49,7 @@ virtual void Get(const char* name, u8* data, size_t len); private: - JS::Value ReadScriptVal(const char* name, JS::HandleObject appendParent); + JS::Value ReadScriptVal(const char* name, JS::HandleObject customObject = nullptr); void ReadStringLatin1(const char* name, std::vector& str); void ReadStringUTF16(const char* name, utf16string& str); Index: source/simulation2/serialization/StdDeserializer.cpp =================================================================== --- source/simulation2/serialization/StdDeserializer.cpp +++ source/simulation2/serialization/StdDeserializer.cpp @@ -122,7 +122,7 @@ //////////////////////////////////////////////////////////////// -JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleObject appendParent) +JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleObject customObject) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); @@ -142,11 +142,7 @@ case SCRIPT_TYPE_OBJECT_PROTOTYPE: { JS::RootedObject obj(cx); - if (appendParent) - { - obj.set(appendParent); - } - else if (type == SCRIPT_TYPE_ARRAY) + if (type == SCRIPT_TYPE_ARRAY) { u32 length; NumberU32_Unbounded("array length", length); @@ -158,18 +154,17 @@ } else // SCRIPT_TYPE_OBJECT_PROTOTYPE { - std::wstring prototypeName; - String("proto name", prototypeName, 0, 256); + CStrW prototypeName; + String("proto", prototypeName, 0, 256); - // Get constructor object - JS::RootedObject proto(cx); - GetSerializablePrototype(prototypeName, &proto); - if (!proto) - throw PSERROR_Deserialize_ScriptError("Failed to find serializable prototype for object"); + JS::RootedValue constructor(cx); + if (!m_ScriptInterface.GetConstructor(prototypeName.ToUTF8(), &constructor)) + throw PSERROR_Deserialize_ScriptError("Deserializer failed to get constructor object"); - obj.set(JS_NewObjectWithGivenProto(cx, nullptr, proto)); - if (!obj) - throw PSERROR_Deserialize_ScriptError("JS_NewObject failed"); + JS::RootedValue objVal(cx); + if (!JS::Construct(cx, constructor, JS::HandleValueArray::empty(), &objVal)) + throw PSERROR_Deserialize_ScriptError("Deserializer failed to construct object"); + obj.set(objVal.toObjectOrNull()); // Does it have custom Deserialize function? // if so, we let it handle the deserialized data, rather than adding properties directly @@ -177,15 +172,15 @@ if (!JS_HasProperty(cx, obj, "Serialize", &hasCustomSerialize) || !JS_HasProperty(cx, obj, "Deserialize", &hasCustomDeserialize)) throw PSERROR_Serialize_ScriptError("JS_HasProperty failed"); + JS::RootedValue serialize(cx); + if (!JS_GetProperty(cx, obj, "Serialize", &serialize)) + throw PSERROR_Serialize_ScriptError("JS_GetProperty failed"); + bool hasNullSerialize = hasCustomSerialize && serialize.isNull(); + if (hasCustomDeserialize) { AddScriptBackref(obj); - JS::RootedValue serialize(cx); - if (!JS_GetProperty(cx, obj, "Serialize", &serialize)) - throw PSERROR_Serialize_ScriptError("JS_GetProperty failed"); - bool hasNullSerialize = hasCustomSerialize && serialize.isNull(); - // If Serialize is null, we'll still call Deserialize but with undefined argument JS::RootedValue data(cx); if (!hasNullSerialize) @@ -196,6 +191,13 @@ return JS::ObjectValue(*obj); } + else if (hasCustomSerialize && hasNullSerialize) + { + // If we serialized null, this means we're pretty much a default-constructed object. + // Nothing to do. + AddScriptBackref(obj); + return JS::ObjectValue(*obj); + } } if (!obj) @@ -213,7 +215,7 @@ { std::vector propname; ReadStringLatin1("prop name", propname); - JS::RootedValue propval(cx, ReadScriptVal("prop value", nullptr)); + JS::RootedValue propval(cx, ReadScriptVal("prop value")); utf16string prp(propname.begin(), propname.end());; // TODO: Should ask upstream about getting a variant of JS_SetProperty with a length param. @@ -224,7 +226,7 @@ { utf16string propname; ReadStringUTF16("prop name", propname); - JS::RootedValue propval(cx, ReadScriptVal("prop value", nullptr)); + JS::RootedValue propval(cx, ReadScriptVal("prop value")); if (!JS_SetUCProperty(cx, obj, (const char16_t*)propname.data(), propname.length(), propval)) throw PSERROR_Deserialize_ScriptError(); @@ -333,7 +335,7 @@ AddScriptBackref(arrayObj); // Get buffer object - JS::RootedValue bufferVal(cx, ReadScriptVal("buffer", nullptr)); + JS::RootedValue bufferVal(cx, ReadScriptVal("buffer")); if (!bufferVal.isObject()) throw PSERROR_Deserialize_ScriptError(); @@ -405,8 +407,8 @@ for (u32 i=0; i