Index: ps/trunk/binaries/data/mods/_test.sim/simulation/components/test-serialize.js =================================================================== --- ps/trunk/binaries/data/mods/_test.sim/simulation/components/test-serialize.js +++ ps/trunk/binaries/data/mods/_test.sim/simulation/components/test-serialize.js @@ -3,7 +3,7 @@ TestScript1_values.prototype.Init = function() { this.x = +this.template.x; this.str = "this is a string"; - this.things = { a: 1, b: "2", c: [3, "4", [5, []]] }; + this.things = { "a": 1, "b": "2", "c": [3, "4", [5, []]] }; }; TestScript1_values.prototype.GetX = function() { @@ -22,11 +22,11 @@ try { delete this.entity; Engine.TS_FAIL("Missed exception"); - } catch (e) { } + } catch (e) { /* OK */ } try { this.entity = -1; Engine.TS_FAIL("Missed exception"); - } catch (e) { } + } catch (e) { /* OK */ } // and return the value return this.entity; @@ -40,7 +40,7 @@ TestScript1_nontree.prototype.Init = function() { var n = [1]; - this.x = [n, n, null, { y: n }]; + this.x = [n, n, null, { "y": n }]; this.x[2] = this.x; }; @@ -61,7 +61,11 @@ }; TestScript1_custom.prototype.Serialize = function() { - return {c:1}; + return { "c": 1 }; +}; + +TestScript1_custom.prototype.Deserialize = function(data) { + this.c = data.c; }; Engine.RegisterComponentType(IID_Test1, "TestScript1_custom", TestScript1_custom); @@ -72,7 +76,7 @@ TestScript1_getter.prototype.Init = function() { this.x = 100; - this.__defineGetter__('x', function () { print("FAIL\n"); die(); return 200; }); + this.__defineGetter__('x', function() { print("FAIL\n"); die(); return 200; }); }; Engine.RegisterComponentType(IID_Test1, "TestScript1_getter", TestScript1_getter); Index: ps/trunk/binaries/data/mods/public/globalscripts/tests/test_vector.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/tests/test_vector.js +++ ps/trunk/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: ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js +++ ps/trunk/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: ps/trunk/binaries/data/mods/public/simulation/components/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Player.js +++ ps/trunk/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: ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js @@ -48,7 +48,7 @@ } let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); - let gameSettings = { "victoryConditions": settings.VictoryConditions }; + let gameSettings = { "victoryConditions": clone(settings.VictoryConditions) }; if (gameSettings.victoryConditions.indexOf("capture_the_relic") != -1) { gameSettings.relicCount = settings.RelicCount; Index: ps/trunk/source/scriptinterface/ScriptInterface.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.h +++ ps/trunk/source/scriptinterface/ScriptInterface.h @@ -210,23 +210,26 @@ */ template bool GetProperty(JS::HandleValue obj, const char* name, T& out) const; - - /** - * Get the named property of the given object. - */ bool GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const; bool GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const; + template + static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, T& out); + static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleValue out); + static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleObject out); + /** * Get the integer-named property on the given object. */ template bool GetPropertyInt(JS::HandleValue obj, int name, T& out) const; - - /** - * Get the named property of the given object. - */ bool GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const; + bool GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleObject out) const; + + template + static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, T& out); + static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleValue out); + static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleObject out); /** * Check the named property has been defined on the given object. @@ -444,8 +447,6 @@ bool SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const; bool SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const; bool SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const; - bool GetProperty_(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const; - bool GetPropertyInt_(JS::HandleValue obj, int name, JS::MutableHandleValue value) const; struct CustomType { @@ -589,8 +590,14 @@ bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, T& out) const { ScriptRequest rq(this); + return GetProperty(rq, obj, name, out); +} + +template +bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, T& out) +{ JS::RootedValue val(rq.cx); - if (!GetProperty_(obj, name, &val)) + if (!GetProperty(rq, obj, name, &val)) return false; return FromJSVal(rq, val, out); } @@ -599,8 +606,14 @@ bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, T& out) const { ScriptRequest rq(this); + return GetPropertyInt(rq, obj, name, out); +} + +template +bool ScriptInterface::GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, T& out) +{ JS::RootedValue val(rq.cx); - if (!GetPropertyInt_(obj, name, &val)) + if (!GetPropertyInt(rq, obj, name, &val)) return false; return FromJSVal(rq, val, out); } Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp @@ -644,16 +644,16 @@ return JS_DefinePropertyById(rq.cx, object, id, value, attrs); } -bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const +bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const { - return GetProperty_(obj, name, out); + ScriptRequest rq(this); + return GetProperty(rq, obj, name, out); } -bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const +bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleObject out) { - ScriptRequest rq(this); JS::RootedValue val(rq.cx); - if (!GetProperty_(obj, name, &val)) + if (!GetProperty(rq, obj, name, &val)) return false; if (!val.isObject()) { @@ -665,14 +665,14 @@ return true; } -bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const +bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const { - return GetPropertyInt_(obj, name, out); + ScriptRequest rq(this); + return GetProperty(rq, obj, name, out); } -bool ScriptInterface::GetProperty_(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const +bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleValue out) { - ScriptRequest rq(this); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); @@ -680,9 +680,14 @@ return JS_GetProperty(rq.cx, object, name, out); } -bool ScriptInterface::GetPropertyInt_(JS::HandleValue obj, int name, JS::MutableHandleValue out) const +bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const { ScriptRequest rq(this); + return GetPropertyInt(rq,obj, name, out); +} + +bool ScriptInterface::GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleValue out) +{ JS::RootedId nameId(rq.cx, INT_TO_JSID(name)); if (!obj.isObject()) return false; Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpAIManager.cpp +++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp @@ -669,12 +669,7 @@ serializer.Bool("useSharedScript", m_HasSharedComponent); if (m_HasSharedComponent) - { - JS::RootedValue sharedData(rq.cx); - if (!m_ScriptInterface->CallFunction(m_SharedAIObj, "Serialize", &sharedData)) - LOGERROR("AI shared script Serialize call failed"); - serializer.ScriptVal("sharedData", &sharedData); - } + serializer.ScriptVal("sharedData", &m_SharedAIObj); for (size_t i = 0; i < m_Players.size(); ++i) { serializer.String("name", m_Players[i]->m_AIName, 1, 256); @@ -690,18 +685,7 @@ serializer.ScriptVal("command", &val); } - bool hasCustomSerialize = m_ScriptInterface->HasProperty(m_Players[i]->m_Obj, "Serialize"); - if (hasCustomSerialize) - { - JS::RootedValue scriptData(rq.cx); - if (!m_ScriptInterface->CallFunction(m_Players[i]->m_Obj, "Serialize", &scriptData)) - LOGERROR("AI script Serialize call failed"); - serializer.ScriptVal("data", &scriptData); - } - else - { - serializer.ScriptVal("data", &m_Players[i]->m_Obj); - } + serializer.ScriptVal("data", &m_Players[i]->m_Obj); } // AI pathfinder @@ -739,10 +723,7 @@ if (m_HasSharedComponent) { TryLoadSharedComponent(); - JS::RootedValue sharedData(rq.cx); - deserializer.ScriptVal("sharedData", &sharedData); - if (!m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "Deserialize", sharedData)) - LOGERROR("AI shared script Deserialize call failed"); + deserializer.ScriptObjectAssign("sharedData", m_SharedAIObj); } for (size_t i = 0; i < numAis; ++i) @@ -768,25 +749,7 @@ m_Players.back()->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(val)); } - bool hasCustomDeserialize = m_ScriptInterface->HasProperty(m_Players.back()->m_Obj, "Deserialize"); - if (hasCustomDeserialize) - { - JS::RootedValue scriptData(rq.cx); - deserializer.ScriptVal("data", &scriptData); - if (m_Players[i]->m_UseSharedComponent) - { - if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData, m_SharedAIObj)) - LOGERROR("AI script Deserialize call failed"); - } - else if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData)) - { - LOGERROR("AI script deserialize() call failed"); - } - } - else - { - deserializer.ScriptVal("data", &m_Players.back()->m_Obj); - } + deserializer.ScriptObjectAssign("data", m_Players.back()->m_Obj); } // AI pathfinder Index: ps/trunk/source/simulation2/components/tests/test_scripts.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_scripts.h +++ ps/trunk/source/simulation2/components/tests/test_scripts.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -16,6 +16,8 @@ */ #include "simulation2/system/ComponentTest.h" +#include "simulation2/serialization/StdDeserializer.h" +#include "simulation2/serialization/StdSerializer.h" #include "ps/Filesystem.h" @@ -58,6 +60,21 @@ TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname)); } + static JS::Value Script_SerializationRoundTrip(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue value) + { + ScriptInterface& scriptInterface = *(pCmptPrivate->pScriptInterface); + ScriptRequest rq(scriptInterface); + + JS::RootedValue val(rq.cx); + val = value; + std::stringstream stream; + CStdSerializer serializer(scriptInterface, stream); + serializer.ScriptVal("", &val); + CStdDeserializer deserializer(scriptInterface, stream); + deserializer.ScriptVal("", &val); + return val; + } + void test_global_scripts() { if (!VfsDirectoryExists(L"globalscripts/tests/")) @@ -73,6 +90,9 @@ CSimContext context; CComponentManager componentManager(context, g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); + + componentManager.GetScriptInterface().RegisterFunction ("SerializationRoundTrip"); + load_script(componentManager.GetScriptInterface(), path); } } @@ -100,6 +120,7 @@ componentManager.GetScriptInterface().RegisterFunction ("LoadComponentScript"); componentManager.GetScriptInterface().RegisterFunction ("LoadHelperScript"); + componentManager.GetScriptInterface().RegisterFunction ("SerializationRoundTrip"); componentManager.LoadComponentTypes(); Index: ps/trunk/source/simulation2/scripting/ScriptComponent.h =================================================================== --- ps/trunk/source/simulation2/scripting/ScriptComponent.h +++ ps/trunk/source/simulation2/scripting/ScriptComponent.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -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: ps/trunk/source/simulation2/scripting/ScriptComponent.cpp =================================================================== --- ps/trunk/source/simulation2/scripting/ScriptComponent.cpp +++ ps/trunk/source/simulation2/scripting/ScriptComponent.cpp @@ -25,19 +25,6 @@ CComponentTypeScript::CComponentTypeScript(const ScriptInterface& scriptInterface, JS::HandleValue instance) : m_ScriptInterface(scriptInterface), m_Instance(scriptInterface.GetGeneralJSContext(), instance) { - // Cache the property detection for efficiency - ScriptRequest rq(m_ScriptInterface); - - 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(rq.cx); - if (m_ScriptInterface.GetProperty(m_Instance, "Serialize", &val) && val.isNull()) - m_HasNullSerialize = true; - } } void CComponentTypeScript::Init(const CParamNode& paramNode, entity_id_t ent) @@ -66,54 +53,18 @@ void CComponentTypeScript::Serialize(ISerializer& serialize) { - // If the component set Serialize = null, then do no work here - if (m_HasNullSerialize) - return; - ScriptRequest rq(m_ScriptInterface); - // 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(rq.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) { ScriptRequest rq(m_ScriptInterface); + deserialize.ScriptObjectAssign("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(rq.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: ps/trunk/source/simulation2/serialization/BinarySerializer.cpp =================================================================== --- ps/trunk/source/simulation2/serialization/BinarySerializer.cpp +++ ps/trunk/source/simulation2/serialization/BinarySerializer.cpp @@ -219,8 +219,36 @@ if (protokey == JSProto_Object) { - // Standard Object prototype - m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT); + // Object class - check for user-defined prototype + JS::RootedObject proto(rq.cx); + if (!JS_GetPrototype(rq.cx, obj, &proto)) + throw PSERROR_Serialize_ScriptError("JS_GetPrototype failed"); + + SPrototypeSerialization protoInfo = GetPrototypeInfo(rq, proto); + + if (protoInfo.name == "Object") + m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT); + else + { + m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_PROTOTYPE); + m_Serializer.String("proto", wstring_from_utf8(protoInfo.name), 0, 256); + + // Does it have custom Serialize function? + // if so, we serialize the data it returns, rather than the object's properties directly + if (protoInfo.hasCustomSerialize) + { + // If serialize is null, don't serialize anything more + if (!protoInfo.hasNullSerialize) + { + JS::RootedValue data(rq.cx); + if (!m_ScriptInterface.CallFunction(val, "Serialize", &data)) + throw PSERROR_Serialize_ScriptError("Prototype Serialize function failed"); + m_Serializer.ScriptVal("data", &data); + } + // Break here to skip the custom object property serialization logic below. + break; + } + } } else if (protokey == JSProto_Number) { @@ -438,7 +466,14 @@ } tagValue = JS::Int32Value(m_ScriptBackrefsNext); - JS_SetPropertyById(rq.cx, obj, symbolId, tagValue); + // TODO: this fails if the object cannot be written to. + // This means we could end up in an infinite loop... + if (!JS_DefinePropertyById(rq.cx, obj, symbolId, tagValue, JSPROP_READONLY)) + { + // For now just warn, this should be user-fixable and may not actually error out. + JS::RootedValue objVal(rq.cx, JS::ObjectValue(*obj.get())); + LOGWARNING("Serialization symbol cannot be written on object %s", m_ScriptInterface.ToString(&objVal)); + } ++m_ScriptBackrefsNext; // Return a non-tag number so callers know they need to serialize the object Index: ps/trunk/source/simulation2/serialization/DebugSerializer.cpp =================================================================== --- ps/trunk/source/simulation2/serialization/DebugSerializer.cpp +++ ps/trunk/source/simulation2/serialization/DebugSerializer.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -149,9 +149,23 @@ void CDebugSerializer::PutScriptVal(const char* name, JS::MutableHandleValue value) { - std::string source = m_ScriptInterface.ToString(value, true); + ScriptRequest rq(m_ScriptInterface); - m_Stream << INDENT << name << ": " << source << "\n"; + JS::RootedValue serialize(rq.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: ps/trunk/source/simulation2/serialization/IDeserializer.h =================================================================== --- ps/trunk/source/simulation2/serialization/IDeserializer.h +++ ps/trunk/source/simulation2/serialization/IDeserializer.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -58,8 +58,11 @@ /// 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 an object and assign its properties to objVal + * (Essentially equivalent to Object.assign(objVal, serialized)) + */ + virtual void ScriptObjectAssign(const char* name, JS::HandleValue objVal) = 0; /// Deserialize a JSString virtual void ScriptString(const char* name, JS::MutableHandleString out) = 0; Index: ps/trunk/source/simulation2/serialization/SerializedScriptTypes.h =================================================================== --- ps/trunk/source/simulation2/serialization/SerializedScriptTypes.h +++ ps/trunk/source/simulation2/serialization/SerializedScriptTypes.h @@ -31,7 +31,7 @@ SCRIPT_TYPE_BACKREF = 8, SCRIPT_TYPE_TYPED_ARRAY = 9, // ArrayBufferView subclasses - see below SCRIPT_TYPE_ARRAY_BUFFER = 10, // ArrayBuffer containing actual typed array data (may be shared by multiple views) - SCRIPT_TYPE_OBJECT_PROTOTYPE = 11, // user-defined prototype - currently unused + SCRIPT_TYPE_OBJECT_PROTOTYPE = 11, // User-defined prototype - see GetPrototypeInfo SCRIPT_TYPE_OBJECT_NUMBER = 12, // standard Number class SCRIPT_TYPE_OBJECT_STRING = 13, // standard String class SCRIPT_TYPE_OBJECT_BOOLEAN = 14, // standard Boolean class @@ -53,4 +53,42 @@ SCRIPT_TYPED_ARRAY_UINT8_CLAMPED = 8 }; +struct SPrototypeSerialization +{ + std::string name = ""; + bool hasCustomSerialize = false; + bool hasCustomDeserialize = false; + bool hasNullSerialize = false; +}; + +inline SPrototypeSerialization GetPrototypeInfo(const ScriptRequest& rq, JS::HandleObject prototype) +{ + SPrototypeSerialization ret; + + JS::RootedValue constructor(rq.cx, JS::ObjectOrNullValue(JS_GetConstructor(rq.cx, prototype))); + if (!ScriptInterface::GetProperty(rq, constructor, "name", ret.name)) + throw PSERROR_Serialize_ScriptError("Could not get constructor name."); + + // Nothing to do for basic Object objects. + if (ret.name == "Object") + return ret; + + if (!JS_HasProperty(rq.cx, prototype, "Serialize", &ret.hasCustomSerialize) || + !JS_HasProperty(rq.cx, prototype, "Deserialize", &ret.hasCustomDeserialize)) + throw PSERROR_Serialize_ScriptError("JS_HasProperty failed"); + + if (ret.hasCustomSerialize) + { + JS::RootedValue serialize(rq.cx); + if (!JS_GetProperty(rq.cx, prototype, "Serialize", &serialize)) + throw PSERROR_Serialize_ScriptError("JS_GetProperty failed"); + + if (serialize.isNull()) + ret.hasNullSerialize = true; + else if (!ret.hasCustomDeserialize) + throw PSERROR_Serialize_ScriptError("Cannot serialize script with non-null Serialize but no Deserialize."); + } + return ret; +} + #endif // INCLUDED_SERIALIZEDSCRIPTTYPES Index: ps/trunk/source/simulation2/serialization/StdDeserializer.h =================================================================== --- ps/trunk/source/simulation2/serialization/StdDeserializer.h +++ ps/trunk/source/simulation2/serialization/StdDeserializer.h @@ -33,7 +33,7 @@ virtual ~CStdDeserializer(); virtual void ScriptVal(const char* name, JS::MutableHandleValue out); - virtual void ScriptObjectAppend(const char* name, JS::HandleValue objVal); + virtual void ScriptObjectAssign(const char* name, JS::HandleValue objVal); virtual void ScriptString(const char* name, JS::MutableHandleString out); virtual std::istream& GetStream(); @@ -47,7 +47,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 preexistingObject); void ReadStringLatin1(const char* name, std::vector& str); void ReadStringUTF16(const char* name, utf16string& str); Index: ps/trunk/source/simulation2/serialization/StdDeserializer.cpp =================================================================== --- ps/trunk/source/simulation2/serialization/StdDeserializer.cpp +++ ps/trunk/source/simulation2/serialization/StdDeserializer.cpp @@ -19,13 +19,13 @@ #include "StdDeserializer.h" -#include "SerializedScriptTypes.h" -#include "StdSerializer.h" // for DEBUG_SERIALIZER_ANNOTATE - +#include "lib/byte_order.h" +#include "ps/CStr.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptExtraHeaders.h" // For typed arrays and ArrayBuffer - -#include "lib/byte_order.h" +#include "simulation2/serialization/ISerializer.h" +#include "simulation2/serialization/SerializedScriptTypes.h" +#include "simulation2/serialization/StdSerializer.h" // for DEBUG_SERIALIZER_ANNOTATE CStdDeserializer::CStdDeserializer(const ScriptInterface& scriptInterface, std::istream& stream) : m_ScriptInterface(scriptInterface), m_Stream(stream) @@ -110,7 +110,7 @@ //////////////////////////////////////////////////////////////// -JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleObject appendParent) +JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleObject preexistingObject) { ScriptRequest rq(m_ScriptInterface); @@ -126,22 +126,69 @@ case SCRIPT_TYPE_ARRAY: case SCRIPT_TYPE_OBJECT: + case SCRIPT_TYPE_OBJECT_PROTOTYPE: { JS::RootedObject obj(rq.cx); - if (appendParent) - { - obj.set(appendParent); - } - else if (type == SCRIPT_TYPE_ARRAY) + if (type == SCRIPT_TYPE_ARRAY) { u32 length; NumberU32_Unbounded("array length", length); obj.set(JS::NewArrayObject(rq.cx, length)); } - else // SCRIPT_TYPE_OBJECT + else if (type == SCRIPT_TYPE_OBJECT) { obj.set(JS_NewPlainObject(rq.cx)); } + else // SCRIPT_TYPE_OBJECT_PROTOTYPE + { + CStrW prototypeName; + String("proto", prototypeName, 0, 256); + + // If an object was passed, no need to construct a new one. + if (preexistingObject != nullptr) + obj.set(preexistingObject); + else + { + JS::RootedValue constructor(rq.cx); + if (!ScriptInterface::GetGlobalProperty(rq, prototypeName.ToUTF8(), &constructor)) + throw PSERROR_Deserialize_ScriptError("Deserializer failed to get constructor object"); + + JS::RootedObject newObj(rq.cx); + if (!JS::Construct(rq.cx, constructor, JS::HandleValueArray::empty(), &newObj)) + throw PSERROR_Deserialize_ScriptError("Deserializer failed to construct object"); + obj.set(newObj); + } + + JS::RootedObject prototype(rq.cx); + JS_GetPrototype(rq.cx, obj, &prototype); + SPrototypeSerialization info = GetPrototypeInfo(rq, prototype); + + if (preexistingObject != nullptr && prototypeName != wstring_from_utf8(info.name)) + throw PSERROR_Deserialize_ScriptError("Deserializer failed: incorrect pre-existing object"); + + + if (info.hasCustomDeserialize) + { + AddScriptBackref(obj); + + // If Serialize is null, we'll still call Deserialize but with undefined argument + JS::RootedValue data(rq.cx); + if (!info.hasNullSerialize) + ScriptVal("data", &data); + + JS::RootedValue objVal(rq.cx, JS::ObjectValue(*obj)); + m_ScriptInterface.CallFunctionVoid(objVal, "Deserialize", data); + + 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) throw PSERROR_Deserialize_ScriptError("Deserializer failed to create new object"); @@ -431,7 +478,7 @@ out.set(ReadScriptVal(name, nullptr)); } -void CStdDeserializer::ScriptObjectAppend(const char* name, JS::HandleValue objVal) +void CStdDeserializer::ScriptObjectAssign(const char* name, JS::HandleValue objVal) { ScriptRequest rq(m_ScriptInterface); Index: ps/trunk/source/simulation2/tests/test_ComponentManager.h =================================================================== --- ps/trunk/source/simulation2/tests/test_ComponentManager.h +++ ps/trunk/source/simulation2/tests/test_ComponentManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -773,7 +773,7 @@ entities:\n\ - id: 1\n\ TestScript1_values:\n\ - object: {\n\ + comp: {\n\ \"x\": 1234,\n\ \"str\": \"this is a string\",\n\ \"things\": {\n\ @@ -792,17 +792,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" ); Index: ps/trunk/source/simulation2/tests/test_Serializer.h =================================================================== --- ps/trunk/source/simulation2/tests/test_Serializer.h +++ ps/trunk/source/simulation2/tests/test_Serializer.h @@ -324,6 +324,14 @@ TSM_ASSERT(msg, !stream.bad() && !stream.fail()); TSM_ASSERT_EQUALS(msg, stream.peek(), EOF); + std::stringstream stream2; + CStdSerializer serialize2(script, stream2); + CStdDeserializer deserialize2(script, stream2); + + // Round-trip the deserialized value again. This helps ensure prototypes are correctly deserialized. + serialize2.ScriptVal("script2", &newobj); + deserialize2.ScriptVal("script2", &newobj); + std::string source; TSM_ASSERT(msg, script.CallFunction(newobj, "toSource", source)); TS_ASSERT_STR_EQUALS(source, expected); @@ -411,6 +419,44 @@ helper_script_roundtrip("Boolean with props", "var b=new Boolean('true'); b.foo='bar'; b", "(new Boolean(true))"); } + void test_script_fancy_objects() + { + // This asserts that objects are deserialized with their correct prototypes. + helper_script_roundtrip("Custom Object", "" + "function customObj() { this.a = this.customFunc.name; };" + "customObj.prototype.customFunc = function customFunc(){};" + "new customObj();", "({a:\"customFunc\"})"); + + helper_script_roundtrip("Custom Class", "" + "class customObj {" + " constructor() { this.a = this.customFunc.name; }" + " customFunc(){};" + "}; new customObj();", "({a:\"customFunc\"})"); + + helper_script_roundtrip("Custom Class with Serialize/Deserialize()", "" + "class customObj {" + " constructor() { this.a = this.customFunc.name; }" + " Serialize() { return { 'foo': 'bar' }; }" + " Deserialize(data) { this.foo = data.foo; }" + " customFunc(){};" + "}; new customObj();", "({a:\"customFunc\", foo:\"bar\"})"); + + helper_script_roundtrip("Custom Class with null serialize & deserialize()", "" + "class customObj {" + " constructor() { this.a = this.customFunc.name; }" + " Deserialize(data) { this.test = 'test'; };" + " customFunc(){};" + "}; customObj.prototype.Serialize=null;" + "new customObj();", "({a:\"customFunc\", test:\"test\"})"); + + helper_script_roundtrip("Custom Class with arguments but still works", "" + "class customObj {" + " constructor(test) { this.a = test; }" + " Serialize() { return { 'data': this.a }; };" + " Deserialize(data) { this.a = data.data; };" + "}; new customObj(4);", "({a:4})"); + } + void test_script_objects_properties() { helper_script_roundtrip("Object with null in prop name", "({\"foo\\0bar\":1})", "({\'foo\\x00bar\':1})");