Correctly serialize/deserialize user-defined JS objects.
Generalize component/AI serialization system to any user-defined JS object. This includes Vector2D/3D, fixing an old issue. As with components/AI, JS Objects may implement a Serialize/Deserialize function to store custom data instead of the default, which attemps to serialize all enumerable properties. Fixes #4698 Differential Revision: https://code.wildfiregames.com/D2746 This was SVN commit r24462.
This commit is contained in:
parent
1432343eeb
commit
9fc6c3c897
@ -3,7 +3,7 @@ function TestScript1_values() {}
|
||||
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 @@ TestScript1_entity.prototype.GetX = function() {
|
||||
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 @@ function TestScript1_nontree() {}
|
||||
|
||||
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.Init = function() {
|
||||
};
|
||||
|
||||
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 @@ function TestScript1_getter() {}
|
||||
|
||||
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);
|
||||
|
@ -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();
|
@ -618,13 +618,12 @@ Trigger.prototype.InitDanubius = function()
|
||||
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();
|
||||
|
@ -36,6 +36,12 @@ Player.prototype.Serialize = function()
|
||||
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.
|
||||
*/
|
||||
|
@ -48,7 +48,7 @@ function LoadMapSettings(settings)
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -644,16 +644,16 @@ bool ScriptInterface::SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleV
|
||||
return JS_DefinePropertyById(rq.cx, object, id, value, attrs);
|
||||
}
|
||||
|
||||
bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const
|
||||
{
|
||||
return GetProperty_(obj, name, out);
|
||||
}
|
||||
|
||||
bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const
|
||||
{
|
||||
ScriptRequest rq(this);
|
||||
return GetProperty(rq, obj, name, out);
|
||||
}
|
||||
|
||||
bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleObject out)
|
||||
{
|
||||
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 @@ bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::Mut
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const
|
||||
{
|
||||
return GetPropertyInt_(obj, name, out);
|
||||
}
|
||||
|
||||
bool ScriptInterface::GetProperty_(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const
|
||||
bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const
|
||||
{
|
||||
ScriptRequest rq(this);
|
||||
return GetProperty(rq, obj, name, out);
|
||||
}
|
||||
|
||||
bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleValue out)
|
||||
{
|
||||
if (!obj.isObject())
|
||||
return false;
|
||||
JS::RootedObject object(rq.cx, &obj.toObject());
|
||||
@ -680,9 +680,14 @@ bool ScriptInterface::GetProperty_(JS::HandleValue obj, const char* name, JS::Mu
|
||||
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;
|
||||
|
@ -210,23 +210,26 @@ public:
|
||||
*/
|
||||
template<typename T>
|
||||
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<typename T>
|
||||
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<typename T>
|
||||
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<typename T>
|
||||
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 @@ private:
|
||||
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 @@ template<typename T>
|
||||
bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, T& out) const
|
||||
{
|
||||
ScriptRequest rq(this);
|
||||
return GetProperty(rq, obj, name, out);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
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 @@ template<typename T>
|
||||
bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, T& out) const
|
||||
{
|
||||
ScriptRequest rq(this);
|
||||
return GetPropertyInt(rq, obj, name, out);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
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);
|
||||
}
|
||||
|
@ -669,12 +669,7 @@ public:
|
||||
|
||||
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 @@ public:
|
||||
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 @@ public:
|
||||
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 @@ public:
|
||||
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
|
||||
|
@ -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 @@ public:
|
||||
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 @@ public:
|
||||
CSimContext context;
|
||||
CComponentManager componentManager(context, g_ScriptContext, true);
|
||||
ScriptTestSetup(componentManager.GetScriptInterface());
|
||||
|
||||
componentManager.GetScriptInterface().RegisterFunction<JS::Value, JS::HandleValue, Script_SerializationRoundTrip> ("SerializationRoundTrip");
|
||||
|
||||
load_script(componentManager.GetScriptInterface(), path);
|
||||
}
|
||||
}
|
||||
@ -100,6 +120,7 @@ public:
|
||||
|
||||
componentManager.GetScriptInterface().RegisterFunction<void, VfsPath, Script_LoadComponentScript> ("LoadComponentScript");
|
||||
componentManager.GetScriptInterface().RegisterFunction<void, VfsPath, Script_LoadHelperScript> ("LoadHelperScript");
|
||||
componentManager.GetScriptInterface().RegisterFunction<JS::Value, JS::HandleValue, Script_SerializationRoundTrip> ("SerializationRoundTrip");
|
||||
|
||||
componentManager.LoadComponentTypes();
|
||||
|
||||
|
@ -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::HandleMessage(const CMessage& msg, bool global)
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 @@ public:
|
||||
private:
|
||||
const ScriptInterface& m_ScriptInterface;
|
||||
JS::PersistentRootedValue m_Instance;
|
||||
bool m_HasCustomSerialize;
|
||||
bool m_HasCustomDeserialize;
|
||||
bool m_HasNullSerialize;
|
||||
};
|
||||
|
||||
#endif // INCLUDED_SCRIPTCOMPONENT
|
||||
|
@ -219,8 +219,36 @@ void CBinarySerializerScriptImpl::HandleScriptVal(JS::HandleValue val)
|
||||
|
||||
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 @@ i32 CBinarySerializerScriptImpl::GetScriptBackrefTag(JS::HandleObject obj)
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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::PutString(const char* name, const std::string& value)
|
||||
|
||||
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)
|
||||
|
@ -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 @@ public:
|
||||
/// 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;
|
||||
|
@ -31,7 +31,7 @@ enum
|
||||
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 @@ enum
|
||||
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
|
||||
|
@ -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 @@ void CStdDeserializer::GetScriptBackref(size_t tag, JS::MutableHandleObject ret)
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
|
||||
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 @@ JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleOb
|
||||
|
||||
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 @@ void CStdDeserializer::ScriptVal(const char* name, JS::MutableHandleValue out)
|
||||
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);
|
||||
|
||||
|
@ -33,7 +33,7 @@ public:
|
||||
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 @@ protected:
|
||||
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<JS::Latin1Char>& str);
|
||||
void ReadStringUTF16(const char* name, utf16string& str);
|
||||
|
||||
|
@ -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 @@ public:
|
||||
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 @@ entities:\n\
|
||||
\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"
|
||||
);
|
||||
|
||||
|
@ -324,6 +324,14 @@ public:
|
||||
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 @@ public:
|
||||
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})");
|
||||
|
Loading…
Reference in New Issue
Block a user