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,9 @@ +function test_serialization() +{ + 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); +} + +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/maps/random/danubius_triggers.js =================================================================== --- binaries/data/mods/public/maps/random/danubius_triggers.js +++ binaries/data/mods/public/maps/random/danubius_triggers.js @@ -618,13 +618,12 @@ this.fillShipsTimer = undefined; // Be able to distinguish between the left and right riverside - // TODO: The Vector2D types don't survive deserialization, so use an object with x and y properties only! let mapSize = TriggerHelper.GetMapSizeTerrain(); - this.mapCenter = clone(new Vector2D(mapSize / 2, mapSize / 2)); + this.mapCenter = new Vector2D(mapSize / 2, mapSize / 2); - this.riverDirection = clone(Vector2D.sub( + this.riverDirection = Vector2D.sub( TriggerHelper.GetEntityPosition2D(this.GetTriggerPoints(triggerPointRiverDirection)[0]), - this.mapCenter)); + this.mapCenter); this.StartCelticRitual(); this.GarrisonAllGallicBuildings(); 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/CCmpAIManager.cpp =================================================================== --- source/simulation2/components/CCmpAIManager.cpp +++ source/simulation2/components/CCmpAIManager.cpp @@ -216,7 +216,6 @@ m_CommandsComputed(true), m_HasLoadedEntityTemplates(false), m_HasSharedComponent(false), - m_SerializablePrototypes(new ObjectIdCache(g_ScriptRuntime)), m_EntityTemplates(g_ScriptRuntime->m_rt), m_SharedAIObj(g_ScriptRuntime->m_rt), m_PassabilityMapVal(g_ScriptRuntime->m_rt), @@ -227,7 +226,6 @@ m_ScriptInterface->SetCallbackData(static_cast (this)); - m_SerializablePrototypes->init(); JS_AddExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), Trace, this); m_ScriptInterface->RegisterFunction("PostCommand"); @@ -660,8 +658,6 @@ else { CStdSerializer serializer(*m_ScriptInterface, stream); - // TODO: see comment in Deserialize() - serializer.SetSerializablePrototypes(m_SerializablePrototypes); SerializeState(serializer); } } @@ -782,12 +778,6 @@ m_Players.back()->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(val)); } - // TODO: this is yucky but necessary while the AIs are sharing data between contexts; - // ideally a new (de)serializer instance would be created for each player - // so they would have a single, consistent script context to use and serializable - // prototypes could be stored in their ScriptInterface - deserializer.SetSerializablePrototypes(m_DeserializablePrototypes); - bool hasCustomDeserialize = m_ScriptInterface->HasProperty(m_Players.back()->m_Obj, "Deserialize"); if (hasCustomDeserialize) { @@ -826,25 +816,6 @@ return m_Players.size(); } - void RegisterSerializablePrototype(std::wstring name, JS::HandleValue proto) - { - // Require unique prototype and name (for reverse lookup) - // TODO: this is yucky - see comment in Deserialize() - ENSURE(proto.isObject() && "A serializable prototype has to be an object!"); - - JSContext* cx = m_ScriptInterface->GetContext(); - JSAutoRequest rq(cx); - - JS::RootedObject obj(cx, &proto.toObject()); - if (m_SerializablePrototypes->has(obj) || m_DeserializablePrototypes.find(name) != m_DeserializablePrototypes.end()) - { - LOGERROR("RegisterSerializablePrototype called with same prototype multiple times: p=%p n='%s'", (void *)obj.get(), utf8_from_wstring(name)); - return; - } - m_SerializablePrototypes->add(cx, obj, name); - m_DeserializablePrototypes[name] = JS::Heap(obj); - } - private: static void Trace(JSTracer *trc, void *data) { @@ -853,8 +824,6 @@ void TraceMember(JSTracer *trc) { - for (std::pair>& prototype : m_DeserializablePrototypes) - JS_CallObjectTracer(trc, &prototype.second, "CAIWorker::m_DeserializablePrototypes"); for (std::pair>& metadata : m_PlayerMetadata) JS_CallValueTracer(trc, &metadata.second, "CAIWorker::m_PlayerMetadata"); } @@ -943,8 +912,6 @@ bool m_CommandsComputed; - shared_ptr > m_SerializablePrototypes; - std::map > m_DeserializablePrototypes; CTemplateLoader m_TemplateLoader; }; 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/EngineScriptConversions.cpp =================================================================== --- source/simulation2/scripting/EngineScriptConversions.cpp +++ source/simulation2/scripting/EngineScriptConversions.cpp @@ -169,7 +169,7 @@ ScriptInterface::CxPrivate* pCxPrivate = ScriptInterface::GetScriptInterfaceAndCBData(cx); JS::RootedObject global(cx, &pCxPrivate->pScriptInterface->GetGlobalObject().toObject()); JS::RootedValue valueVector3D(cx); - if (!JS_GetProperty(cx, global, "Vector3D", &valueVector3D)) + if (!pCxPrivate->pScriptInterface->GetConstructor("Vector3D", &valueVector3D)) FAIL_VOID("Failed to get Vector3D constructor"); JS::AutoValueArray<3> args(cx); @@ -206,7 +206,7 @@ ScriptInterface::CxPrivate* pCxPrivate = ScriptInterface::GetScriptInterfaceAndCBData(cx); JS::RootedObject global(cx, &pCxPrivate->pScriptInterface->GetGlobalObject().toObject()); JS::RootedValue valueVector2D(cx); - if (!JS_GetProperty(cx, global, "Vector2D", &valueVector2D)) + if (!pCxPrivate->pScriptInterface->GetConstructor("Vector2D", &valueVector2D)) FAIL_VOID("Failed to get Vector2D constructor"); JS::AutoValueArray<2> args(cx); Index: source/simulation2/scripting/ScriptComponent.h =================================================================== --- source/simulation2/scripting/ScriptComponent.h +++ source/simulation2/scripting/ScriptComponent.h @@ -70,9 +70,6 @@ private: const ScriptInterface& m_ScriptInterface; JS::PersistentRootedValue m_Instance; - bool m_HasCustomSerialize; - bool m_HasCustomDeserialize; - bool m_HasNullSerialize; }; #endif // INCLUDED_SCRIPTCOMPONENT Index: source/simulation2/scripting/ScriptComponent.cpp =================================================================== --- source/simulation2/scripting/ScriptComponent.cpp +++ source/simulation2/scripting/ScriptComponent.cpp @@ -24,22 +24,7 @@ CComponentTypeScript::CComponentTypeScript(const ScriptInterface& scriptInterface, JS::HandleValue instance) : m_ScriptInterface(scriptInterface), m_Instance(scriptInterface.GetJSRuntime(), instance) -{ - // Cache the property detection for efficiency - JSContext* cx = m_ScriptInterface.GetContext(); - JSAutoRequest rq(cx); - - m_HasCustomSerialize = m_ScriptInterface.HasProperty(m_Instance, "Serialize"); - m_HasCustomDeserialize = m_ScriptInterface.HasProperty(m_Instance, "Deserialize"); - - m_HasNullSerialize = false; - if (m_HasCustomSerialize) - { - JS::RootedValue val(cx); - if (m_ScriptInterface.GetProperty(m_Instance, "Serialize", &val) && val.isNull()) - m_HasNullSerialize = true; - } -} +{} void CComponentTypeScript::Init(const CParamNode& paramNode, entity_id_t ent) { @@ -68,26 +53,10 @@ void CComponentTypeScript::Serialize(ISerializer& serialize) { - // If the component set Serialize = null, then do no work here - 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, true); } void CComponentTypeScript::Deserialize(const CParamNode& paramNode, IDeserializer& deserialize, entity_id_t ent) @@ -95,29 +64,8 @@ JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); + deserialize.ScriptObjectAppendProperties("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.h =================================================================== --- source/simulation2/serialization/BinarySerializer.h +++ source/simulation2/serialization/BinarySerializer.h @@ -86,8 +86,7 @@ CBinarySerializerScriptImpl(const ScriptInterface& scriptInterface, ISerializer& serializer); void ScriptString(const char* name, JS::HandleString string); - void HandleScriptVal(JS::HandleValue val); - void SetSerializablePrototypes(shared_ptr > prototypes); + void HandleScriptVal(JS::HandleValue val, bool only_props = false); private: const ScriptInterface& m_ScriptInterface; ISerializer& m_Serializer; @@ -96,10 +95,15 @@ u32 m_ScriptBackrefsNext; u32 GetScriptBackrefTag(JS::HandleObject obj); - shared_ptr > m_SerializablePrototypes; + struct SSerializablePrototype + { + std::wstring name = L""; + bool hasCustomSerialize = false; + bool hasNullSerialize = false; + }; + shared_ptr> m_SerializablePrototypes; - bool IsSerializablePrototype(JS::HandleObject prototype); - std::wstring GetPrototypeName(JS::HandleObject prototype); + SSerializablePrototype GetPrototypeData(JS::HandleObject prototype); }; /** @@ -127,11 +131,6 @@ { } - virtual void SetSerializablePrototypes(shared_ptr >& prototypes) - { - m_ScriptImpl->SetSerializablePrototypes(prototypes); - } - protected: /* The Put* implementations here are designed for subclasses @@ -206,9 +205,9 @@ m_Impl.Put(name, (u8*)value.data(), value.length()); } - virtual void PutScriptVal(const char* UNUSED(name), JS::MutableHandleValue value) + virtual void PutScriptVal(const char* UNUSED(name), JS::MutableHandleValue value, bool only_props) { - m_ScriptImpl->HandleScriptVal(value); + m_ScriptImpl->HandleScriptVal(value, only_props); } virtual void PutRaw(const char* name, const u8* data, size_t len) Index: source/simulation2/serialization/BinarySerializer.cpp =================================================================== --- source/simulation2/serialization/BinarySerializer.cpp +++ source/simulation2/serialization/BinarySerializer.cpp @@ -56,13 +56,13 @@ CBinarySerializerScriptImpl::CBinarySerializerScriptImpl(const ScriptInterface& scriptInterface, ISerializer& serializer) : m_ScriptInterface(scriptInterface), m_Serializer(serializer), m_ScriptBackrefs(scriptInterface.GetRuntime()), - m_SerializablePrototypes(new ObjectIdCache(scriptInterface.GetRuntime())), m_ScriptBackrefsNext(1) + m_SerializablePrototypes(new ObjectIdCache(scriptInterface.GetRuntime())), m_ScriptBackrefsNext(1) { m_ScriptBackrefs.init(); m_SerializablePrototypes->init(); } -void CBinarySerializerScriptImpl::HandleScriptVal(JS::HandleValue val) +void CBinarySerializerScriptImpl::HandleScriptVal(JS::HandleValue val, bool only_props) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); @@ -155,45 +155,32 @@ { // 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 + SSerializablePrototype prototype = GetPrototypeData(proto); + if (prototype.name == 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 - m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_PROTOTYPE); - - const std::wstring prototypeName = GetPrototypeName(proto); - m_Serializer.String("proto name", prototypeName, 0, 256); + // Component serialization can skip this part as we deserialize with an already-created object. + if (!only_props) + { + m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_PROTOTYPE); + m_Serializer.String("proto", prototype.name, 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)) - throw PSERROR_Serialize_ScriptError("JS_HasProperty failed"); - - if (hasCustomSerialize) + if (prototype.hasCustomSerialize) { - JS::RootedValue serialize(cx); - if (!JS_GetProperty(cx, obj, "Serialize", &serialize)) - throw PSERROR_Serialize_ScriptError("JS_GetProperty failed"); - // If serialize is null, so don't serialize anything more - if (!serialize.isNull()) + if (!prototype.hasNullSerialize) { 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; } @@ -475,20 +462,49 @@ return 0; } -bool CBinarySerializerScriptImpl::IsSerializablePrototype(JS::HandleObject prototype) +CBinarySerializerScriptImpl::SSerializablePrototype CBinarySerializerScriptImpl::GetPrototypeData(JS::HandleObject prototype) { - return m_SerializablePrototypes->has(prototype); -} - -std::wstring CBinarySerializerScriptImpl::GetPrototypeName(JS::HandleObject prototype) -{ - std::wstring ret; + SSerializablePrototype ret; bool found = m_SerializablePrototypes->find(prototype, ret); - ENSURE(found); + if (!found) + { + JSContext* cx = m_ScriptInterface.GetContext(); + JSAutoRequest rq(cx); + + JS::RootedValue constructor(cx, JS::ObjectOrNullValue(JS_GetConstructor(cx, prototype))); + if (!m_ScriptInterface.GetProperty(constructor, "name", ret.name)) + throw PSERROR_Serialize_ScriptError("Could not get constructor name."); + + // Nothing to do for basic Object objects. + if (ret.name == L"Object") + { + m_SerializablePrototypes->add(cx, prototype, ret); + return ret; + } + + 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."); + + bool hasCustomDeserialize; + if (!JS_HasProperty(cx, prototype, "Serialize", &ret.hasCustomSerialize) || + !JS_HasProperty(cx, prototype, "Deserialize", &hasCustomDeserialize)) + throw PSERROR_Serialize_ScriptError("JS_HasProperty failed"); + + if (ret.hasCustomSerialize) + { + JS::RootedValue serialize(cx); + if (!JS_GetProperty(cx, prototype, "Serialize", &serialize)) + throw PSERROR_Serialize_ScriptError("JS_GetProperty failed"); + + if (serialize.isNull()) + ret.hasNullSerialize = true; + else if (!hasCustomDeserialize) + throw PSERROR_Serialize_ScriptError("Cannot serialize script with non-null Serialize but no Deserialize."); + } + m_SerializablePrototypes->add(cx, prototype, ret); + } return ret; } - -void CBinarySerializerScriptImpl::SetSerializablePrototypes(shared_ptr > prototypes) -{ - m_SerializablePrototypes = prototypes; -} Index: source/simulation2/serialization/DebugSerializer.h =================================================================== --- source/simulation2/serialization/DebugSerializer.h +++ source/simulation2/serialization/DebugSerializer.h @@ -54,7 +54,7 @@ virtual void PutNumber(const char* name, fixed value); virtual void PutBool(const char* name, bool value); virtual void PutString(const char* name, const std::string& value); - virtual void PutScriptVal(const char* name, JS::MutableHandleValue value); + virtual void PutScriptVal(const char* name, JS::MutableHandleValue value, bool only_props); virtual void PutRaw(const char* name, const u8* data, size_t len); private: Index: source/simulation2/serialization/DebugSerializer.cpp =================================================================== --- source/simulation2/serialization/DebugSerializer.cpp +++ source/simulation2/serialization/DebugSerializer.cpp @@ -147,11 +147,27 @@ m_Stream << INDENT << name << ": " << "\"" << escaped << "\"\n"; } -void CDebugSerializer::PutScriptVal(const char* name, JS::MutableHandleValue value) +void CDebugSerializer::PutScriptVal(const char* name, JS::MutableHandleValue value, bool UNUSED(only_props)) { - std::string source = m_ScriptInterface.ToString(value, true); + JSContext* cx = m_ScriptInterface.GetContext(); + JSAutoRequest rq(cx); - m_Stream << INDENT << name << ": " << source << "\n"; + + JS::RootedValue serialize(cx); + if (m_ScriptInterface.GetProperty(value, "Serialize", &serialize) && !serialize.isNullOrUndefined()) + { + // If the value has a Serialize property, pretty-parse that and return the value as a raw string. + // This gives more debug data for components in case of OOS. + m_ScriptInterface.CallFunction(value, "Serialize", &serialize); + std::string serialized_source = m_ScriptInterface.ToString(&serialize, true); + std::string source = m_ScriptInterface.ToString(value, false); + m_Stream << INDENT << name << ": " << serialized_source << " (raw: " << source << ")\n"; + } + else + { + std::string source = m_ScriptInterface.ToString(value, true); + m_Stream << INDENT << name << ": " << source << "\n"; + } } void CDebugSerializer::PutRaw(const char* name, const u8* data, size_t len) Index: source/simulation2/serialization/IDeserializer.h =================================================================== --- source/simulation2/serialization/IDeserializer.h +++ source/simulation2/serialization/IDeserializer.h @@ -58,8 +58,8 @@ /// Deserialize a JS::Value, replacing 'out' 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; + /// Deserialize properties of an already-created object 'objToAppendTo'. + virtual void ScriptObjectAppendProperties(const char* name, JS::MutableHandleValue objToAppendTo) = 0; /// Deserialize a JSString virtual void ScriptString(const char* name, JS::MutableHandleString out) = 0; Index: source/simulation2/serialization/ISerializer.h =================================================================== --- source/simulation2/serialization/ISerializer.h +++ source/simulation2/serialization/ISerializer.h @@ -219,8 +219,10 @@ * Serialize a JS::MutableHandleValue. * The value must not contain any unserializable values (like functions). * NOTE: We have to use a mutable handle because JS_Stringify requires that for unknown reasons. + * If only_props is true, only serialize properties of value (this is a specific optimization for components, + * since components are recreated from C++ and we do not need to serialize their type). */ - void ScriptVal(const char* name, JS::MutableHandleValue value); + void ScriptVal(const char* name, JS::MutableHandleValue value, bool only_props = false); /** * Serialize a stream of bytes. @@ -257,7 +259,7 @@ virtual void PutBool(const char* name, bool value) = 0; virtual void PutString(const char* name, const std::string& value) = 0; // We have to use a mutable handle because JS_Stringify requires that for unknown reasons. - virtual void PutScriptVal(const char* name, JS::MutableHandleValue value) = 0; + virtual void PutScriptVal(const char* name, JS::MutableHandleValue value, bool only_props) = 0; virtual void PutRaw(const char* name, const u8* data, size_t len) = 0; }; Index: source/simulation2/serialization/ISerializer.cpp =================================================================== --- source/simulation2/serialization/ISerializer.cpp +++ source/simulation2/serialization/ISerializer.cpp @@ -92,9 +92,9 @@ PutString(name, str); } -void ISerializer::ScriptVal(const char* name, JS::MutableHandleValue value) +void ISerializer::ScriptVal(const char* name, JS::MutableHandleValue value, bool only_props) { - PutScriptVal(name, value); + PutScriptVal(name, value, only_props); } void ISerializer::RawBytes(const char* name, const u8* data, size_t len) Index: source/simulation2/serialization/StdDeserializer.h =================================================================== --- source/simulation2/serialization/StdDeserializer.h +++ source/simulation2/serialization/StdDeserializer.h @@ -21,6 +21,7 @@ #include "IDeserializer.h" #include "ps/utf16string.h" +#include "scriptinterface/third_party/ObjectToIDMap.h" #include @@ -32,14 +33,13 @@ 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::MutableHandleValue objToAppendTo); virtual void ScriptString(const char* name, JS::MutableHandleString out); virtual std::istream& GetStream(); virtual void RequireBytesInStream(size_t numBytes); - virtual void SetSerializablePrototypes(std::map >& prototypes); - static void Trace(JSTracer *trc, void *data); void TraceMember(JSTracer *trc); @@ -48,7 +48,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); @@ -61,10 +61,13 @@ std::istream& m_Stream; - std::map > m_SerializablePrototypes; - - bool IsSerializablePrototype(const std::wstring& name); - void GetSerializablePrototype(const std::wstring& name, JS::MutableHandleObject ret); + struct SPrototypeInfo + { + bool hasCustomDeserialize = false; + bool hasNullSerialize = false; + }; + ObjectIdCache m_PrototypeInfo; + SPrototypeInfo GetPrototypeInfo(JS::HandleObject prototype); }; #endif // INCLUDED_STDDESERIALIZER Index: source/simulation2/serialization/StdDeserializer.cpp =================================================================== --- source/simulation2/serialization/StdDeserializer.cpp +++ source/simulation2/serialization/StdDeserializer.cpp @@ -28,7 +28,7 @@ #include "lib/byte_order.h" CStdDeserializer::CStdDeserializer(const ScriptInterface& scriptInterface, std::istream& stream) : - m_ScriptInterface(scriptInterface), m_Stream(stream), + m_ScriptInterface(scriptInterface), m_Stream(stream), m_PrototypeInfo(scriptInterface.GetRuntime()), m_dummyObject(scriptInterface.GetJSRuntime()) { JSContext* cx = m_ScriptInterface.GetContext(); @@ -36,6 +36,8 @@ JS_AddExtraGCRootsTracer(m_ScriptInterface.GetJSRuntime(), CStdDeserializer::Trace, this); + m_PrototypeInfo.init(); + // Add a dummy tag because the serializer uses the tag 0 to indicate that a value // needs to be serialized and then tagged m_dummyObject = JS_NewPlainObject(cx); @@ -56,9 +58,6 @@ { for (size_t i=0; i>& proto : m_SerializablePrototypes) - JS_CallObjectTracer(trc, &proto.second, "StdDeserializer::m_SerializablePrototypes"); } void CStdDeserializer::Get(const char* name, u8* data, size_t len) @@ -122,13 +121,16 @@ //////////////////////////////////////////////////////////////// -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); uint8_t type; - NumberU8_Unbounded("type", type); + if (customObject != nullptr) + type = SCRIPT_TYPE_OBJECT_PROTOTYPE; + else + NumberU8_Unbounded("type", type); switch (type) { case SCRIPT_TYPE_VOID: @@ -142,11 +144,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,37 +156,35 @@ } else // SCRIPT_TYPE_OBJECT_PROTOTYPE { - std::wstring prototypeName; - String("proto name", prototypeName, 0, 256); + // If an object was passed, no need to construct a new one. + if (customObject != nullptr) + obj.set(customObject); + else + { + 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 - bool hasCustomDeserialize, hasCustomSerialize; - if (!JS_HasProperty(cx, obj, "Serialize", &hasCustomSerialize) || !JS_HasProperty(cx, obj, "Deserialize", &hasCustomDeserialize)) - throw PSERROR_Serialize_ScriptError("JS_HasProperty failed"); + JS::RootedObject prototype(cx); + JS_GetPrototype(cx, obj, &prototype); + SPrototypeInfo info = GetPrototypeInfo(prototype); - if (hasCustomDeserialize) + if (info.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) + if (!info.hasNullSerialize) ScriptVal("data", &data); JS::RootedValue objVal(cx, JS::ObjectValue(*obj)); @@ -196,6 +192,13 @@ return JS::ObjectValue(*obj); } + else if (info.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 +216,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 +227,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 +336,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 +408,8 @@ for (u32 i=0; i >& prototypes) +CStdDeserializer::SPrototypeInfo CStdDeserializer::GetPrototypeInfo(JS::HandleObject prototype) { - m_SerializablePrototypes = prototypes; -} + SPrototypeInfo ret; + bool found = m_PrototypeInfo.find(prototype, ret); + if (!found) + { + JSContext* cx = m_ScriptInterface.GetContext(); + JSAutoRequest rq(cx); -bool CStdDeserializer::IsSerializablePrototype(const std::wstring& name) -{ - return m_SerializablePrototypes.find(name) != m_SerializablePrototypes.end(); -} + // Does it have custom Deserialize function? + // if so, we let it handle the deserialized data, rather than adding properties directly + bool hasCustomSerialize; + if (!JS_HasProperty(cx, prototype, "Serialize", &hasCustomSerialize) || + !JS_HasProperty(cx, prototype, "Deserialize", &ret.hasCustomDeserialize)) + throw PSERROR_Serialize_ScriptError("JS_HasProperty failed"); -void CStdDeserializer::GetSerializablePrototype(const std::wstring& name, JS::MutableHandleObject ret) -{ - std::map >::iterator it = m_SerializablePrototypes.find(name); - if (it != m_SerializablePrototypes.end()) - ret.set(it->second); - else - ret.set(NULL); + JS::RootedValue serialize(cx); + if (!JS_GetProperty(cx, prototype, "Serialize", &serialize)) + throw PSERROR_Serialize_ScriptError("JS_GetProperty failed"); + ret.hasNullSerialize = hasCustomSerialize && serialize.isNull(); + m_PrototypeInfo.add(cx, prototype, ret); + } + return ret; } Index: source/simulation2/tests/test_ComponentManager.h =================================================================== --- source/simulation2/tests/test_ComponentManager.h +++ source/simulation2/tests/test_ComponentManager.h @@ -775,7 +775,7 @@ entities:\n\ - id: 1\n\ TestScript1_values:\n\ - object: {\n\ + comp: {\n\ \"x\": 1234,\n\ \"str\": \"this is a string\",\n\ \"things\": {\n\ @@ -794,17 +794,17 @@ \n\ - id: 2\n\ TestScript1_entity:\n\ - object: {}\n\ + comp: {}\n\ \n\ - id: 3\n\ TestScript1_nontree:\n\ - object: ({x:[[2], [2], [], {y:[2]}]})\n\ + comp: ({x:[[2], [2], [], {y:[2]}]})\n\ \n\ - id: 4\n\ TestScript1_custom:\n\ - object: {\n\ + comp: {\n\ \"c\": 1\n\ -}\n\ +} (raw: ({y:2}))\n\ \n" );