2137 lines
78 KiB
C++
2137 lines
78 KiB
C++
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
|
|
|
#include "VisualScript.h"
|
|
#include "Engine/Core/Log.h"
|
|
#include "Engine/Core/Types/DataContainer.h"
|
|
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
|
#include "Engine/Scripting/MException.h"
|
|
#include "Engine/Scripting/Scripting.h"
|
|
#include "Engine/Scripting/ManagedCLR/MClass.h"
|
|
#include "Engine/Scripting/ManagedCLR/MMethod.h"
|
|
#include "Engine/Scripting/ManagedCLR/MField.h"
|
|
#include "Engine/Scripting/ManagedCLR/MUtils.h"
|
|
#include "Engine/Scripting/ManagedCLR/MType.h"
|
|
#include "Engine/Serialization/MemoryReadStream.h"
|
|
#include "Engine/Serialization/MemoryWriteStream.h"
|
|
#include "Engine/Serialization/Serialization.h"
|
|
#include "Engine/Serialization/JsonWriter.h"
|
|
#include "Engine/Utilities/StringConverter.h"
|
|
#include "FlaxEngine.Gen.h"
|
|
|
|
namespace
|
|
{
|
|
struct VisualScriptThread
|
|
{
|
|
uint32 StackFramesCount;
|
|
VisualScripting::StackFrame* Stack;
|
|
};
|
|
|
|
ThreadLocal<VisualScriptThread, 32> ThreadStacks;
|
|
VisualScriptingBinaryModule VisualScriptingModule;
|
|
VisualScriptExecutor VisualScriptingExecutor;
|
|
|
|
void PrintStack(LogType type)
|
|
{
|
|
const String stack = VisualScripting::GetStackTrace();
|
|
Log::Logger::Write(type, TEXT("Visual Script stack trace:"));
|
|
Log::Logger::Write(type, stack);
|
|
Log::Logger::Write(type, TEXT(""));
|
|
}
|
|
}
|
|
|
|
#if VISUAL_SCRIPT_DEBUGGING
|
|
Action VisualScripting::DebugFlow;
|
|
#endif
|
|
|
|
static_assert(TIsPODType<VisualScripting::StackFrame>::Value, "VisualScripting::StackFrame must be POD type.");
|
|
static_assert(TIsPODType<VisualScriptThread>::Value, "VisualScriptThread must be POD type.");
|
|
|
|
bool VisualScriptGraph::onNodeLoaded(Node* n)
|
|
{
|
|
switch (n->GroupID)
|
|
{
|
|
// Function
|
|
case 16:
|
|
switch (n->TypeID)
|
|
{
|
|
// Invoke Method
|
|
case 4:
|
|
n->Data.InvokeMethod.Method = nullptr;
|
|
break;
|
|
// Get/Set Field
|
|
case 7:
|
|
case 8:
|
|
n->Data.GetSetField.Field = nullptr;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Base
|
|
return VisjectGraph<VisualScriptGraphNode, VisjectGraphBox, VisjectGraphParameter>::onNodeLoaded(n);
|
|
}
|
|
|
|
VisualScriptExecutor::VisualScriptExecutor()
|
|
{
|
|
_perGroupProcessCall[2] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupConstants;
|
|
_perGroupProcessCall[4] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupPacking;
|
|
_perGroupProcessCall[6] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupParameters;
|
|
_perGroupProcessCall[7] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupTools;
|
|
_perGroupProcessCall[16] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupFunction;
|
|
_perGroupProcessCall[17] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupFlow;
|
|
}
|
|
|
|
VisjectExecutor::Value VisualScriptExecutor::eatBox(Node* caller, Box* box)
|
|
{
|
|
// Check if graph is looped or is too deep
|
|
auto& stack = ThreadStacks.Get();
|
|
if (stack.StackFramesCount >= VISUAL_SCRIPT_GRAPH_MAX_CALL_STACK)
|
|
{
|
|
OnError(caller, box, TEXT("Graph is looped or too deep!"));
|
|
return Value::Zero;
|
|
}
|
|
#if !BUILD_RELEASE
|
|
if (box == nullptr)
|
|
{
|
|
OnError(caller, box, TEXT("Null graph box!"));
|
|
return Value::Zero;
|
|
}
|
|
#endif
|
|
const auto parentNode = box->GetParent<Node>();
|
|
|
|
// Add to the calling stack
|
|
VisualScripting::StackFrame frame = *stack.Stack;
|
|
frame.Node = parentNode;
|
|
frame.Box = box;
|
|
frame.PreviousFrame = stack.Stack;
|
|
stack.Stack = &frame;
|
|
stack.StackFramesCount++;
|
|
|
|
#if VISUAL_SCRIPT_DEBUGGING
|
|
// Debugger event
|
|
VisualScripting::DebugFlow();
|
|
#endif
|
|
|
|
// Call per group custom processing event
|
|
Value value;
|
|
const ProcessBoxHandler func = _perGroupProcessCall[parentNode->GroupID];
|
|
(this->*func)(box, parentNode, value);
|
|
|
|
// Remove from the calling stack
|
|
stack.StackFramesCount--;
|
|
stack.Stack = frame.PreviousFrame;
|
|
|
|
return value;
|
|
}
|
|
|
|
VisjectExecutor::Graph* VisualScriptExecutor::GetCurrentGraph() const
|
|
{
|
|
auto& stack = ThreadStacks.Get();
|
|
return stack.Stack && stack.Stack->Script ? &stack.Stack->Script->Graph : nullptr;
|
|
}
|
|
|
|
void VisualScriptExecutor::ProcessGroupConstants(Box* box, Node* node, Value& value)
|
|
{
|
|
switch (node->TypeID)
|
|
{
|
|
// Enum
|
|
case 11:
|
|
{
|
|
value = node->Values[0];
|
|
break;
|
|
}
|
|
default:
|
|
VisjectExecutor::ProcessGroupConstants(box, node, value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void VisualScriptExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value)
|
|
{
|
|
switch (node->TypeID)
|
|
{
|
|
// Pack Structure
|
|
case 26:
|
|
{
|
|
// Find type
|
|
const StringView typeName(node->Values[0]);
|
|
const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
|
|
const StringAnsiView typeNameAnsiView(typeNameAnsi.Get(), typeName.Length());
|
|
const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(typeNameAnsiView);
|
|
if (!typeHandle)
|
|
{
|
|
const auto mclass = Scripting::FindClass(typeNameAnsiView);
|
|
if (mclass)
|
|
{
|
|
// Fallback to C#-only types
|
|
bool failed = false;
|
|
auto instance = mclass->CreateInstance();
|
|
value = instance;
|
|
auto& layoutCache = node->Values[1];
|
|
CHECK(layoutCache.Type.Type == VariantType::Blob);
|
|
MemoryReadStream stream((byte*)layoutCache.AsBlob.Data, layoutCache.AsBlob.Length);
|
|
const byte version = stream.ReadByte();
|
|
if (version == 1)
|
|
{
|
|
int32 fieldsCount;
|
|
stream.ReadInt32(&fieldsCount);
|
|
for (int32 boxId = 1; boxId < node->Boxes.Count(); boxId++)
|
|
{
|
|
box = &node->Boxes[boxId];
|
|
String fieldName;
|
|
stream.ReadString(&fieldName, 11);
|
|
VariantType fieldType;
|
|
stream.ReadVariantType(&fieldType);
|
|
if (box && box->HasConnection())
|
|
{
|
|
StringAsANSI<40> fieldNameAnsi(*fieldName, fieldName.Length());
|
|
auto field = mclass->GetField(fieldNameAnsi.Get());
|
|
if (field)
|
|
{
|
|
Variant fieldValue = eatBox(node, box->FirstConnection());
|
|
field->SetValue(instance, MUtils::VariantToManagedArgPtr(fieldValue, field->GetType(), failed));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (typeName.HasChars())
|
|
{
|
|
LOG(Error, "Missing type '{0}'", typeName);
|
|
PrintStack(LogType::Error);
|
|
}
|
|
return;
|
|
}
|
|
const ScriptingType& type = typeHandle.GetType();
|
|
|
|
// Allocate structure data and initialize it with native constructor
|
|
value.SetType(VariantType(VariantType::Structure, typeNameAnsiView));
|
|
|
|
// Setup structure fields
|
|
auto& layoutCache = node->Values[1];
|
|
CHECK(layoutCache.Type.Type == VariantType::Blob);
|
|
MemoryReadStream stream((byte*)layoutCache.AsBlob.Data, layoutCache.AsBlob.Length);
|
|
const byte version = stream.ReadByte();
|
|
if (version == 1)
|
|
{
|
|
int32 fieldsCount;
|
|
stream.ReadInt32(&fieldsCount);
|
|
for (int32 boxId = 1; boxId < node->Boxes.Count(); boxId++)
|
|
{
|
|
box = &node->Boxes[boxId];
|
|
String fieldName;
|
|
stream.ReadString(&fieldName, 11);
|
|
VariantType fieldType;
|
|
stream.ReadVariantType(&fieldType);
|
|
if (box && box->HasConnection())
|
|
{
|
|
const Variant fieldValue = eatBox(node, box->FirstConnection());
|
|
type.Struct.SetField(value.AsBlob.Data, fieldName, fieldValue);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
// Unpack Structure
|
|
case 36:
|
|
{
|
|
// Get value with structure data
|
|
const Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection());
|
|
if (!node->GetBox(0)->HasConnection())
|
|
return;
|
|
|
|
// Find type
|
|
const StringView typeName(node->Values[0]);
|
|
const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
|
|
const StringAnsiView typeNameAnsiView(typeNameAnsi.Get(), typeName.Length());
|
|
const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(typeNameAnsiView);
|
|
if (!typeHandle)
|
|
{
|
|
const auto mclass = Scripting::FindClass(typeNameAnsiView);
|
|
if (mclass)
|
|
{
|
|
// Fallback to C#-only types
|
|
auto instance = mono_gchandle_get_target(structureValue.AsUint);
|
|
CHECK(instance);
|
|
if (structureValue.Type.Type != VariantType::ManagedObject || mono_object_get_class(instance) != mclass->GetNative())
|
|
{
|
|
LOG(Error, "Cannot unpack value of type {0} to structure of type {1}", String(MUtils::GetClassFullname(instance)), typeName);
|
|
PrintStack(LogType::Error);
|
|
return;
|
|
}
|
|
auto& layoutCache = node->Values[1];
|
|
CHECK(layoutCache.Type.Type == VariantType::Blob);
|
|
MemoryReadStream stream((byte*)layoutCache.AsBlob.Data, layoutCache.AsBlob.Length);
|
|
const byte version = stream.ReadByte();
|
|
if (version == 1)
|
|
{
|
|
int32 fieldsCount;
|
|
stream.ReadInt32(&fieldsCount);
|
|
for (int32 boxId = 1; boxId < node->Boxes.Count(); boxId++)
|
|
{
|
|
String fieldName;
|
|
stream.ReadString(&fieldName, 11);
|
|
VariantType fieldType;
|
|
stream.ReadVariantType(&fieldType);
|
|
if (box->ID == boxId)
|
|
{
|
|
StringAsANSI<40> fieldNameAnsi(*fieldName, fieldName.Length());
|
|
auto field = mclass->GetField(fieldNameAnsi.Get());
|
|
if (field)
|
|
value = MUtils::UnboxVariant(field->GetValueBoxed(instance));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (typeName.HasChars())
|
|
{
|
|
LOG(Error, "Missing type '{0}'", typeName);
|
|
PrintStack(LogType::Error);
|
|
}
|
|
return;
|
|
}
|
|
const ScriptingType& type = typeHandle.GetType();
|
|
if (structureValue.Type.Type != VariantType::Structure || StringUtils::Compare(typeNameAnsi.Get(), structureValue.Type.TypeName) != 0)
|
|
{
|
|
LOG(Error, "Cannot unpack value of type {0} to structure of type {1}", structureValue.Type, typeName);
|
|
PrintStack(LogType::Error);
|
|
return;
|
|
}
|
|
|
|
// Read structure field
|
|
auto& layoutCache = node->Values[1];
|
|
CHECK(layoutCache.Type.Type == VariantType::Blob);
|
|
MemoryReadStream stream((byte*)layoutCache.AsBlob.Data, layoutCache.AsBlob.Length);
|
|
const byte version = stream.ReadByte();
|
|
if (version == 1)
|
|
{
|
|
int32 fieldsCount;
|
|
stream.ReadInt32(&fieldsCount);
|
|
for (int32 boxId = 1; boxId < node->Boxes.Count(); boxId++)
|
|
{
|
|
String fieldName;
|
|
stream.ReadString(&fieldName, 11);
|
|
VariantType fieldType;
|
|
stream.ReadVariantType(&fieldType);
|
|
if (box->ID == boxId)
|
|
{
|
|
type.Struct.GetField(structureValue.AsBlob.Data, fieldName, value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
VisjectExecutor::ProcessGroupPacking(box, node, value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void VisualScriptExecutor::ProcessGroupParameters(Box* box, Node* node, Value& value)
|
|
{
|
|
switch (node->TypeID)
|
|
{
|
|
// Get
|
|
case 3:
|
|
{
|
|
int32 paramIndex;
|
|
auto& stack = ThreadStacks.Get();
|
|
if (!stack.Stack->Instance)
|
|
{
|
|
LOG(Error, "Cannot access Visual Script parameter without instance.");
|
|
PrintStack(LogType::Error);
|
|
break;
|
|
}
|
|
const auto param = stack.Stack->Script->Graph.GetParameter((Guid)node->Values[0], paramIndex);
|
|
const auto instanceParams = stack.Stack->Script->_instances.Find(stack.Stack->Instance->GetID());
|
|
if (param && instanceParams)
|
|
{
|
|
value = instanceParams->Value[paramIndex];
|
|
}
|
|
else
|
|
{
|
|
LOG(Error, "Failed to access Visual Script parameter for {0}.", stack.Stack->Instance->ToString());
|
|
PrintStack(LogType::Error);
|
|
}
|
|
break;
|
|
}
|
|
// Set
|
|
case 4:
|
|
{
|
|
int32 paramIndex;
|
|
auto& stack = ThreadStacks.Get();
|
|
if (!stack.Stack->Instance)
|
|
{
|
|
LOG(Error, "Cannot access Visual Script parameter without instance.");
|
|
PrintStack(LogType::Error);
|
|
break;
|
|
}
|
|
const auto param = stack.Stack->Script->Graph.GetParameter((Guid)node->Values[0], paramIndex);
|
|
const auto instanceParams = stack.Stack->Script->_instances.Find(stack.Stack->Instance->GetID());
|
|
if (param && instanceParams)
|
|
{
|
|
instanceParams->Value[paramIndex] = tryGetValue(node->GetBox(1), 1, Value::Zero);
|
|
}
|
|
else
|
|
{
|
|
LOG(Error, "Failed to access Visual Script parameter for {0}.", stack.Stack->Instance->ToString());
|
|
PrintStack(LogType::Error);
|
|
}
|
|
if (node->Boxes[2].HasConnection())
|
|
eatBox(node, node->Boxes[2].FirstConnection());
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void VisualScriptExecutor::ProcessGroupTools(Box* box, Node* node, Value& value)
|
|
{
|
|
switch (node->TypeID)
|
|
{
|
|
// This Instance
|
|
case 19:
|
|
{
|
|
value = ThreadStacks.Get().Stack->Instance;
|
|
break;
|
|
}
|
|
// As
|
|
case 22:
|
|
{
|
|
value = Value::Null;
|
|
const auto obj = (ScriptingObject*)tryGetValue(node->GetBox(1), Value::Null);
|
|
if (obj)
|
|
{
|
|
const StringView typeName(node->Values[0]);
|
|
const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
|
|
const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
|
|
const auto objClass = obj->GetClass();
|
|
if (typeHandle && objClass && objClass->IsSubClassOf(typeHandle.GetType().ManagedClass))
|
|
value = obj;
|
|
}
|
|
break;
|
|
}
|
|
// Type Reference node
|
|
case 23:
|
|
{
|
|
const StringView typeName(node->Values[0]);
|
|
if (box->ID == 0)
|
|
value.SetTypename(typeName);
|
|
else
|
|
value = typeName;
|
|
break;
|
|
}
|
|
// Is
|
|
case 24:
|
|
{
|
|
value = Value::False;
|
|
const auto obj = (ScriptingObject*)tryGetValue(node->GetBox(1), Value::Null);
|
|
if (obj)
|
|
{
|
|
const StringView typeName(node->Values[0]);
|
|
const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
|
|
const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
|
|
const auto objClass = obj->GetClass();
|
|
value.AsBool = typeHandle && objClass && objClass->IsSubClassOf(typeHandle.GetType().ManagedClass);
|
|
}
|
|
break;
|
|
}
|
|
// Cast
|
|
case 25:
|
|
{
|
|
if (box->ID == 0)
|
|
{
|
|
// Get object and try to cast it
|
|
auto obj = (ScriptingObject*)tryGetValue(node->GetBox(1), Value::Null);
|
|
if (obj)
|
|
{
|
|
const StringView typeName(node->Values[0]);
|
|
const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
|
|
const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
|
|
const auto objClass = obj->GetClass();
|
|
if (!typeHandle || !objClass || !objClass->IsSubClassOf(typeHandle.GetType().ManagedClass))
|
|
obj = nullptr;
|
|
}
|
|
|
|
// Cache cast object value (only if it's valid)
|
|
const bool isValid = obj != nullptr;
|
|
if (isValid)
|
|
{
|
|
const auto scope = ThreadStacks.Get().Stack->Scope;
|
|
int32 returnedIndex = 0;
|
|
for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
|
|
{
|
|
const auto& e = scope->ReturnedValues[returnedIndex];
|
|
if (e.NodeId == node->ID && e.BoxId == 4)
|
|
break;
|
|
}
|
|
if (returnedIndex == scope->ReturnedValues.Count())
|
|
scope->ReturnedValues.AddOne();
|
|
auto& returnedValue = scope->ReturnedValues[returnedIndex];
|
|
returnedValue.NodeId = node->ID;
|
|
returnedValue.BoxId = 4;
|
|
returnedValue.Value = obj;
|
|
}
|
|
|
|
// Call graph further
|
|
const auto impulseBox = &node->Boxes[isValid ? 2 : 3];
|
|
if (impulseBox && impulseBox->HasConnection())
|
|
eatBox(node, impulseBox->FirstConnection());
|
|
}
|
|
else if (box->ID == 4)
|
|
{
|
|
// Find returned value inside the current scope
|
|
const auto scope = ThreadStacks.Get().Stack->Scope;
|
|
int32 returnedIndex = 0;
|
|
for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
|
|
{
|
|
const auto& e = scope->ReturnedValues[returnedIndex];
|
|
if (e.NodeId == node->ID && e.BoxId == 4)
|
|
break;
|
|
}
|
|
if (returnedIndex != scope->ReturnedValues.Count())
|
|
value = scope->ReturnedValues[returnedIndex].Value;
|
|
}
|
|
break;
|
|
}
|
|
// Cast Value
|
|
case 26:
|
|
{
|
|
if (box->ID == 0)
|
|
{
|
|
// Get object and try to cast it
|
|
auto obj = tryGetValue(node->GetBox(1), Value::Null);
|
|
if (obj)
|
|
{
|
|
const StringView typeName(node->Values[0]);
|
|
const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
|
|
if (StringUtils::Compare(typeNameAnsi.Get(), obj.Type.GetTypeName()) != 0)
|
|
{
|
|
MonoClass* klass = Scripting::FindClassNative(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
|
|
MonoClass* objKlass = MUtils::GetClass(obj);
|
|
if (!klass || !objKlass || mono_class_is_subclass_of(objKlass, klass, false) == 0)
|
|
obj = Value::Null;
|
|
}
|
|
}
|
|
|
|
// Cache cast object value (only if it's valid)
|
|
const bool isValid = obj != Value::Null;
|
|
if (isValid)
|
|
{
|
|
const auto scope = ThreadStacks.Get().Stack->Scope;
|
|
int32 returnedIndex = 0;
|
|
for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
|
|
{
|
|
const auto& e = scope->ReturnedValues[returnedIndex];
|
|
if (e.NodeId == node->ID && e.BoxId == 4)
|
|
break;
|
|
}
|
|
if (returnedIndex == scope->ReturnedValues.Count())
|
|
scope->ReturnedValues.AddOne();
|
|
auto& returnedValue = scope->ReturnedValues[returnedIndex];
|
|
returnedValue.NodeId = node->ID;
|
|
returnedValue.BoxId = 4;
|
|
returnedValue.Value = MoveTemp(obj);
|
|
}
|
|
|
|
// Call graph further
|
|
const auto impulseBox = &node->Boxes[isValid ? 2 : 3];
|
|
if (impulseBox && impulseBox->HasConnection())
|
|
eatBox(node, impulseBox->FirstConnection());
|
|
}
|
|
else if (box->ID == 4)
|
|
{
|
|
// Find returned value inside the current scope
|
|
const auto scope = ThreadStacks.Get().Stack->Scope;
|
|
int32 returnedIndex = 0;
|
|
for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
|
|
{
|
|
const auto& e = scope->ReturnedValues[returnedIndex];
|
|
if (e.NodeId == node->ID && e.BoxId == 4)
|
|
break;
|
|
}
|
|
if (returnedIndex != scope->ReturnedValues.Count())
|
|
value = scope->ReturnedValues[returnedIndex].Value;
|
|
}
|
|
break;
|
|
}
|
|
// Is Null
|
|
case 27:
|
|
value = tryGetValue(node->GetBox(1), Value::Null) == Value::Null;
|
|
break;
|
|
// Is Valid
|
|
case 28:
|
|
value = tryGetValue(node->GetBox(1), Value::Null) != Value::Null;
|
|
break;
|
|
default:
|
|
VisjectExecutor::ProcessGroupTools(box, node, value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void VisualScriptExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value& value)
|
|
{
|
|
switch (node->TypeID)
|
|
{
|
|
// Method Override
|
|
case 3:
|
|
{
|
|
if (boxBase->ID == 0)
|
|
{
|
|
// Call graph further
|
|
if (boxBase->HasConnection())
|
|
eatBox(node, boxBase->FirstConnection());
|
|
}
|
|
else
|
|
{
|
|
// Evaluate overriden method parameter value from the current scope
|
|
auto& scope = ThreadStacks.Get().Stack->Scope;
|
|
value = scope->Parameters[boxBase->ID - 1];
|
|
}
|
|
break;
|
|
}
|
|
// Invoke Method
|
|
case 4:
|
|
{
|
|
// Call Impulse or Pure Method
|
|
if (boxBase->ID == 0 || (bool)node->Values[3])
|
|
{
|
|
auto& cache = node->Data.InvokeMethod;
|
|
if (!cache.Method)
|
|
{
|
|
// Load method signature
|
|
const auto typeName = (StringView)node->Values[0];
|
|
const auto methodName = (StringView)node->Values[1];
|
|
const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
|
|
const StringAsANSI<60> methodNameAnsi(methodName.Get(), methodName.Length());
|
|
ScriptingTypeMethodSignature signature;
|
|
signature.Name = StringAnsiView(methodNameAnsi.Get(), methodName.Length());
|
|
auto& signatureCache = node->Values[4];
|
|
if (signatureCache.Type.Type != VariantType::Blob)
|
|
{
|
|
LOG(Error, "Missing method '{0}::{1}' signature data", typeName, methodName);
|
|
PrintStack(LogType::Error);
|
|
return;
|
|
}
|
|
MemoryReadStream stream((byte*)signatureCache.AsBlob.Data, signatureCache.AsBlob.Length);
|
|
const byte version = stream.ReadByte();
|
|
if (version == 4)
|
|
{
|
|
signature.IsStatic = stream.ReadBool();
|
|
stream.ReadVariantType(&signature.ReturnType);
|
|
int32 signatureParamsCount;
|
|
stream.ReadInt32(&signatureParamsCount);
|
|
signature.Params.Resize(signatureParamsCount);
|
|
for (int32 i = 0; i < signatureParamsCount; i++)
|
|
{
|
|
auto& param = signature.Params[i];
|
|
int32 parameterNameLength;
|
|
stream.ReadInt32(¶meterNameLength);
|
|
stream.SetPosition(stream.GetPosition() + parameterNameLength * sizeof(Char));
|
|
stream.ReadVariantType(¶m.Type);
|
|
param.IsOut = stream.ReadBool();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LOG(Error, "Unsupported method '{0}::{1}' signature data", typeName, methodName);
|
|
PrintStack(LogType::Error);
|
|
return;
|
|
}
|
|
void* method;
|
|
ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
|
|
if (typeHandle)
|
|
{
|
|
// Find method in the scripting type
|
|
method = typeHandle.Module->FindMethod(typeHandle, signature);
|
|
if (!method)
|
|
{
|
|
LOG(Error, "Missing method '{0}::{1}'", typeName, methodName);
|
|
PrintStack(LogType::Error);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fallback to C#-only types
|
|
const auto mclass = Scripting::FindClass(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
|
|
if (mclass)
|
|
{
|
|
method = ManagedBinaryModule::FindMethod(mclass, signature);
|
|
if (!method)
|
|
{
|
|
LOG(Error, "Missing method '{0}::{1}'", typeName, methodName);
|
|
PrintStack(LogType::Error);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (typeName.HasChars())
|
|
{
|
|
LOG(Error, "Missing type '{0}'", typeName);
|
|
PrintStack(LogType::Error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Mock the scripting type for C# method that doesn't exist in engine script types database
|
|
typeHandle = ScriptingTypeHandle(GetBinaryModuleFlaxEngine(), 0);
|
|
}
|
|
|
|
// Cache method data
|
|
cache.Method = method;
|
|
cache.Module = typeHandle.Module;
|
|
cache.ParamsCount = signature.Params.Count();
|
|
cache.IsStatic = signature.IsStatic;
|
|
cache.OutParamsMask = 0;
|
|
for (int32 paramIdx = 0; paramIdx < signature.Params.Count() && paramIdx < 32; paramIdx++)
|
|
cache.OutParamsMask |= signature.Params[paramIdx].IsOut ? (1 << static_cast<uint32>(paramIdx)) : 0;
|
|
}
|
|
|
|
// Evaluate object instance for non-static methods
|
|
Variant instance;
|
|
if (!cache.IsStatic)
|
|
{
|
|
// Evaluate object instance
|
|
const auto box = node->GetBox(1);
|
|
if (box->HasConnection())
|
|
{
|
|
instance = eatBox(node, box->FirstConnection());
|
|
}
|
|
else
|
|
{
|
|
// Unconnected instance box is treated as this script instance if type matches the member method class
|
|
auto& stack = ThreadStacks.Get();
|
|
instance.SetObject(stack.Stack->Instance);
|
|
}
|
|
}
|
|
|
|
// Evaluate parameter values
|
|
Variant* paramValues = (Variant*)alloca(cache.ParamsCount * sizeof(Variant));
|
|
bool hasOutParams = false;
|
|
for (int32 paramIdx = 0; paramIdx < cache.ParamsCount; paramIdx++)
|
|
{
|
|
auto& paramValue = paramValues[paramIdx];
|
|
Memory::ConstructItem(¶mValue);
|
|
const bool isOut = paramIdx < 32 && (cache.OutParamsMask & (1 << static_cast<uint32>(paramIdx))) != 0;
|
|
hasOutParams |= isOut;
|
|
const auto box = node->GetBox(paramIdx + 4);
|
|
if (box->HasConnection() && !isOut)
|
|
paramValue = eatBox(node, box->FirstConnection());
|
|
else if (node->Values.Count() > 5 + paramIdx)
|
|
paramValue = node->Values[5 + paramIdx];
|
|
}
|
|
|
|
// Invoke the method
|
|
Variant result;
|
|
if (cache.Module->InvokeMethod(cache.Method, instance, Span<Variant>(paramValues, cache.ParamsCount), result))
|
|
{
|
|
PrintStack(LogType::Error);
|
|
}
|
|
else
|
|
{
|
|
// Cache returned value inside the current scope
|
|
const auto scope = ThreadStacks.Get().Stack->Scope;
|
|
{
|
|
int32 returnedIndex = 0;
|
|
for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
|
|
{
|
|
const auto& e = scope->ReturnedValues[returnedIndex];
|
|
if (e.NodeId == node->ID && e.BoxId == 3)
|
|
break;
|
|
}
|
|
if (returnedIndex == scope->ReturnedValues.Count())
|
|
scope->ReturnedValues.AddOne();
|
|
auto& returnedValue = scope->ReturnedValues[returnedIndex];
|
|
returnedValue.NodeId = node->ID;
|
|
returnedValue.BoxId = 3;
|
|
returnedValue.Value = MoveTemp(result);
|
|
}
|
|
|
|
// Cache output parameters values inside the current scope
|
|
if (hasOutParams)
|
|
{
|
|
for (int32 paramIdx = 0; paramIdx < cache.ParamsCount; paramIdx++)
|
|
{
|
|
const bool isOut = paramIdx < 32 && (cache.OutParamsMask & (1 << static_cast<uint32>(paramIdx))) != 0;
|
|
if (isOut && node->GetBox(paramIdx + 4)->HasConnection())
|
|
{
|
|
int32 returnedIndex = 0;
|
|
for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
|
|
{
|
|
const auto& e = scope->ReturnedValues[returnedIndex];
|
|
if (e.NodeId == node->ID && e.BoxId == paramIdx + 4)
|
|
break;
|
|
}
|
|
if (returnedIndex == scope->ReturnedValues.Count())
|
|
scope->ReturnedValues.AddOne();
|
|
auto& returnedValue = scope->ReturnedValues[returnedIndex];
|
|
returnedValue.NodeId = node->ID;
|
|
returnedValue.BoxId = paramIdx + 4;
|
|
returnedValue.Value = MoveTemp(paramValues[paramIdx]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Call graph further
|
|
const auto returnedImpulse = &node->Boxes[2];
|
|
if (returnedImpulse && returnedImpulse->HasConnection())
|
|
eatBox(node, returnedImpulse->FirstConnection());
|
|
}
|
|
|
|
// Free parameters data
|
|
Memory::DestructItems(paramValues, cache.ParamsCount);
|
|
}
|
|
// Returned value or Output Parameter
|
|
if (boxBase->ID == 3 || boxBase->ID >= 4)
|
|
{
|
|
// Find returned value inside the current scope (from the previous method call)
|
|
const auto scope = ThreadStacks.Get().Stack->Scope;
|
|
int32 returnedIndex = 0;
|
|
for (; returnedIndex < scope->ReturnedValues.Count(); returnedIndex++)
|
|
{
|
|
const auto& e = scope->ReturnedValues[returnedIndex];
|
|
if (e.NodeId == node->ID && e.BoxId == boxBase->ID)
|
|
break;
|
|
}
|
|
if (returnedIndex != scope->ReturnedValues.Count())
|
|
value = scope->ReturnedValues[returnedIndex].Value;
|
|
}
|
|
break;
|
|
}
|
|
// Return
|
|
case 5:
|
|
{
|
|
auto& scope = ThreadStacks.Get().Stack->Scope;
|
|
scope->FunctionReturn = tryGetValue(node->GetBox(1), Value::Zero);
|
|
break;
|
|
}
|
|
// Function
|
|
case 6:
|
|
{
|
|
if (boxBase->ID == 0)
|
|
{
|
|
// Call function
|
|
if (boxBase->HasConnection())
|
|
eatBox(node, boxBase->FirstConnection());
|
|
}
|
|
else
|
|
{
|
|
// Evaluate method parameter value from the current scope
|
|
auto& scope = ThreadStacks.Get().Stack->Scope;
|
|
value = scope->Parameters[boxBase->ID - 1];
|
|
}
|
|
break;
|
|
}
|
|
// Get Field
|
|
case 7:
|
|
{
|
|
auto& cache = node->Data.GetSetField;
|
|
if (!cache.Field)
|
|
{
|
|
const auto typeName = (StringView)node->Values[0];
|
|
const auto fieldName = (StringView)node->Values[1];
|
|
const auto fieldTypeName = (StringView)node->Values[2];
|
|
const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
|
|
const StringAsANSI<60> fieldNameAnsi(fieldName.Get(), fieldName.Length());
|
|
const StringAsANSI<100> fieldTypeNameAnsi(fieldTypeName.Get(), fieldTypeName.Length());
|
|
void* field;
|
|
ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
|
|
if (typeHandle)
|
|
{
|
|
// Find field in the scripting type
|
|
field = typeHandle.Module->FindField(typeHandle, fieldNameAnsi.Get());
|
|
if (!field)
|
|
{
|
|
LOG(Error, "Missing field '{1}' in type '{0}'", typeName, fieldName);
|
|
PrintStack(LogType::Error);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fallback to C#-only types
|
|
const auto mclass = Scripting::FindClass(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
|
|
if (mclass)
|
|
{
|
|
field = mclass->GetField(fieldNameAnsi.Get());
|
|
if (!field)
|
|
{
|
|
LOG(Error, "Missing field '{1}' in type '{0}'", typeName, fieldName);
|
|
PrintStack(LogType::Error);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (typeName.HasChars())
|
|
{
|
|
LOG(Error, "Missing type '{0}'", typeName);
|
|
PrintStack(LogType::Error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Mock the scripting type for C# field that doesn't exist in engine script types database
|
|
typeHandle = ScriptingTypeHandle(GetBinaryModuleFlaxEngine(), 0);
|
|
}
|
|
|
|
// Cache field data
|
|
cache.Field = field;
|
|
cache.Module = typeHandle.Module;
|
|
ScriptingTypeFieldSignature signature;
|
|
cache.Module->GetFieldSignature(field, signature);
|
|
cache.IsStatic = signature.IsStatic;
|
|
}
|
|
|
|
// Evaluate object instance for non-static fields
|
|
Variant instance;
|
|
if (!cache.IsStatic)
|
|
{
|
|
// Evaluate object instance
|
|
const auto box = node->GetBox(1);
|
|
if (box->HasConnection())
|
|
{
|
|
instance = eatBox(node, box->FirstConnection());
|
|
}
|
|
else
|
|
{
|
|
// Unconnected instance box is treated as this script instance if type matches the member field class
|
|
auto& stack = ThreadStacks.Get();
|
|
instance.SetObject(stack.Stack->Instance);
|
|
}
|
|
}
|
|
|
|
// Get field value
|
|
if (cache.Module->GetFieldValue(cache.Field, instance, value))
|
|
{
|
|
PrintStack(LogType::Error);
|
|
}
|
|
break;
|
|
}
|
|
// Get Field
|
|
case 8:
|
|
{
|
|
auto& cache = node->Data.GetSetField;
|
|
if (!cache.Field)
|
|
{
|
|
const auto typeName = (StringView)node->Values[0];
|
|
const auto fieldName = (StringView)node->Values[1];
|
|
const auto fieldTypeName = (StringView)node->Values[2];
|
|
const StringAsANSI<100> typeNameAnsi(typeName.Get(), typeName.Length());
|
|
const StringAsANSI<60> fieldNameAnsi(fieldName.Get(), fieldName.Length());
|
|
const StringAsANSI<100> fieldTypeNameAnsi(fieldTypeName.Get(), fieldTypeName.Length());
|
|
void* field;
|
|
ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
|
|
if (typeHandle)
|
|
{
|
|
// Find field in the scripting type
|
|
field = typeHandle.Module->FindField(typeHandle, fieldNameAnsi.Get());
|
|
if (!field)
|
|
{
|
|
LOG(Error, "Missing field '{1}' in type '{0}'", typeName, fieldName);
|
|
PrintStack(LogType::Error);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fallback to C#-only types
|
|
const auto mclass = Scripting::FindClass(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
|
|
if (mclass)
|
|
{
|
|
field = mclass->GetField(fieldNameAnsi.Get());
|
|
if (!field)
|
|
{
|
|
LOG(Error, "Missing field '{1}' in type '{0}'", typeName, fieldName);
|
|
PrintStack(LogType::Error);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (typeName.HasChars())
|
|
{
|
|
LOG(Error, "Missing type '{0}'", typeName);
|
|
PrintStack(LogType::Error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Mock the scripting type for C# field that doesn't exist in engine script types database
|
|
typeHandle = ScriptingTypeHandle(GetBinaryModuleFlaxEngine(), 0);
|
|
}
|
|
|
|
// Cache field data
|
|
cache.Field = field;
|
|
cache.Module = typeHandle.Module;
|
|
ScriptingTypeFieldSignature signature;
|
|
cache.Module->GetFieldSignature(field, signature);
|
|
cache.IsStatic = signature.IsStatic;
|
|
}
|
|
|
|
// Evaluate object instance for non-static fields
|
|
Variant instance;
|
|
if (!cache.IsStatic)
|
|
{
|
|
// Evaluate object instance
|
|
const auto box = node->GetBox(1);
|
|
if (box->HasConnection())
|
|
{
|
|
instance = eatBox(node, box->FirstConnection());
|
|
}
|
|
else
|
|
{
|
|
// Unconnected instance box is treated as this script instance if type matches the member field class
|
|
auto& stack = ThreadStacks.Get();
|
|
instance.SetObject(stack.Stack->Instance);
|
|
}
|
|
}
|
|
|
|
// Set field value
|
|
value = tryGetValue(node->GetBox(0), 4, Value::Zero);
|
|
if (cache.Module->SetFieldValue(cache.Field, instance, value))
|
|
{
|
|
PrintStack(LogType::Error);
|
|
break;
|
|
}
|
|
|
|
// Call graph further
|
|
const auto returnedImpulse = &node->Boxes[3];
|
|
if (returnedImpulse && returnedImpulse->HasConnection())
|
|
eatBox(node, returnedImpulse->FirstConnection());
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void VisualScriptExecutor::ProcessGroupFlow(Box* boxBase, Node* node, Value& value)
|
|
{
|
|
switch (node->TypeID)
|
|
{
|
|
// If
|
|
case 1:
|
|
{
|
|
const bool condition = (bool)tryGetValue(node->GetBox(1), Value::Zero);
|
|
boxBase = node->GetBox(condition ? 2 : 3);
|
|
if (boxBase->HasConnection())
|
|
eatBox(node, boxBase->FirstConnection());
|
|
break;
|
|
}
|
|
// For Loop
|
|
case 2:
|
|
{
|
|
const auto scope = ThreadStacks.Get().Stack->Scope;
|
|
int32 iteratorIndex = 0;
|
|
for (; iteratorIndex < scope->ReturnedValues.Count(); iteratorIndex++)
|
|
{
|
|
const auto& e = scope->ReturnedValues[iteratorIndex];
|
|
if (e.NodeId == node->ID)
|
|
break;
|
|
}
|
|
switch (boxBase->ID)
|
|
{
|
|
// Loop
|
|
case 0:
|
|
{
|
|
if (iteratorIndex == scope->ReturnedValues.Count())
|
|
scope->ReturnedValues.AddOne();
|
|
auto& iteratorValue = scope->ReturnedValues[iteratorIndex];
|
|
iteratorValue.NodeId = node->ID;
|
|
iteratorValue.BoxId = 0;
|
|
iteratorValue.Value = (int32)tryGetValue(node->GetBox(1), 0, Value::Zero);
|
|
const int32 count = (int32)tryGetValue(node->GetBox(2), 1, Value::Zero);
|
|
for (; iteratorValue.Value.AsInt < count; iteratorValue.Value.AsInt++)
|
|
{
|
|
boxBase = node->GetBox(4);
|
|
if (boxBase->HasConnection())
|
|
eatBox(node, boxBase->FirstConnection());
|
|
}
|
|
boxBase = node->GetBox(6);
|
|
if (boxBase->HasConnection())
|
|
eatBox(node, boxBase->FirstConnection());
|
|
break;
|
|
}
|
|
// Break
|
|
case 3:
|
|
// Reset loop iterator
|
|
if (iteratorIndex != scope->ReturnedValues.Count())
|
|
scope->ReturnedValues[iteratorIndex].Value.AsInt = MAX_int32 - 1;
|
|
break;
|
|
// Index
|
|
case 5:
|
|
if (iteratorIndex != scope->ReturnedValues.Count())
|
|
value = scope->ReturnedValues[iteratorIndex].Value;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
// While Loop
|
|
case 3:
|
|
{
|
|
const auto scope = ThreadStacks.Get().Stack->Scope;
|
|
int32 iteratorIndex = 0;
|
|
for (; iteratorIndex < scope->ReturnedValues.Count(); iteratorIndex++)
|
|
{
|
|
const auto& e = scope->ReturnedValues[iteratorIndex];
|
|
if (e.NodeId == node->ID)
|
|
break;
|
|
}
|
|
switch (boxBase->ID)
|
|
{
|
|
// Loop
|
|
case 0:
|
|
{
|
|
if (iteratorIndex == scope->ReturnedValues.Count())
|
|
scope->ReturnedValues.AddOne();
|
|
auto& iteratorValue = scope->ReturnedValues[iteratorIndex];
|
|
iteratorValue.NodeId = node->ID;
|
|
iteratorValue.BoxId = 0;
|
|
iteratorValue.Value = 0;
|
|
for (; (bool)tryGetValue(node->GetBox(1), 1, Value::Zero) && iteratorValue.Value.AsInt != -1; iteratorValue.Value.AsInt++)
|
|
{
|
|
boxBase = node->GetBox(3);
|
|
if (boxBase->HasConnection())
|
|
eatBox(node, boxBase->FirstConnection());
|
|
}
|
|
boxBase = node->GetBox(5);
|
|
if (boxBase->HasConnection())
|
|
eatBox(node, boxBase->FirstConnection());
|
|
break;
|
|
}
|
|
// Break
|
|
case 2:
|
|
// Reset loop iterator
|
|
if (iteratorIndex != scope->ReturnedValues.Count())
|
|
scope->ReturnedValues[iteratorIndex].Value.AsInt = -1;
|
|
break;
|
|
// Index
|
|
case 4:
|
|
if (iteratorIndex != scope->ReturnedValues.Count())
|
|
value = scope->ReturnedValues[iteratorIndex].Value;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
REGISTER_BINARY_ASSET(VisualScript, "FlaxEngine.VisualScript", nullptr, false);
|
|
|
|
VisualScript::VisualScript(const SpawnParams& params, const AssetInfo* info)
|
|
: BinaryAsset(params, info)
|
|
{
|
|
}
|
|
|
|
Asset::LoadResult VisualScript::load()
|
|
{
|
|
// Build Visual Script typename that is based on asset id
|
|
String typeName = _id.ToString();
|
|
StringUtils::ConvertUTF162ANSI(typeName.Get(), _typenameChars, 32);
|
|
_typenameChars[32] = 0;
|
|
_typename = StringAnsiView(_typenameChars, 32);
|
|
|
|
// Load metadata
|
|
const auto metadataChunk = GetChunk(1);
|
|
if (metadataChunk == nullptr)
|
|
return LoadResult::MissingDataChunk;
|
|
MemoryReadStream metadataStream(metadataChunk->Get(), metadataChunk->Size());
|
|
int32 version;
|
|
metadataStream.ReadInt32(&version);
|
|
switch (version)
|
|
{
|
|
case 1:
|
|
{
|
|
metadataStream.ReadString(&Meta.BaseTypename, 31);
|
|
metadataStream.ReadInt32((int32*)&Meta.Flags);
|
|
break;
|
|
}
|
|
default:
|
|
LOG(Error, "Unknown Visual Script \'{1}\' metadata version {0}.", version, ToString());
|
|
return LoadResult::InvalidData;
|
|
}
|
|
|
|
// Load graph
|
|
const auto surfaceChunk = GetChunk(0);
|
|
if (surfaceChunk == nullptr)
|
|
return LoadResult::MissingDataChunk;
|
|
MemoryReadStream surfaceStream(surfaceChunk->Get(), surfaceChunk->Size());
|
|
if (Graph.Load(&surfaceStream, true))
|
|
{
|
|
LOG(Warning, "Failed to load graph \'{0}\'", ToString());
|
|
return LoadResult::Failed;
|
|
}
|
|
|
|
// Find method nodes
|
|
for (auto& node : Graph.Nodes)
|
|
{
|
|
switch (node.Type)
|
|
{
|
|
case GRAPH_NODE_MAKE_TYPE(16, 3):
|
|
{
|
|
auto& method = _methods.AddOne();
|
|
method.Script = this;
|
|
method.Node = &node;
|
|
method.Name = StringAnsi((StringView)node.Values[0]);
|
|
method.MethodFlags = (MethodFlags)((byte)MethodFlags::Virtual | (byte)MethodFlags::Override);
|
|
method.Signature.Name = method.Name;
|
|
method.Signature.IsStatic = false;
|
|
method.Signature.Params.Resize(node.Values[1].AsInt);
|
|
method.ParamNames.Resize(method.Signature.Params.Count());
|
|
break;
|
|
}
|
|
case GRAPH_NODE_MAKE_TYPE(16, 6):
|
|
{
|
|
auto& method = _methods.AddOne();
|
|
method.Script = this;
|
|
method.Node = &node;
|
|
method.Signature.IsStatic = false;
|
|
auto& signatureData = node.Values[0];
|
|
if (signatureData.Type.Type != VariantType::Blob || signatureData.AsBlob.Length == 0)
|
|
break;
|
|
MemoryReadStream signatureStream((byte*)signatureData.AsBlob.Data, signatureData.AsBlob.Length);
|
|
switch (signatureStream.ReadByte())
|
|
{
|
|
case 1:
|
|
{
|
|
signatureStream.ReadStringAnsi(&method.Name, 71);
|
|
method.MethodFlags = (MethodFlags)signatureStream.ReadByte();
|
|
method.Signature.IsStatic = ((byte)method.MethodFlags & (byte)MethodFlags::Static) != 0;
|
|
signatureStream.ReadVariantType(&method.Signature.ReturnType);
|
|
int32 parametersCount;
|
|
signatureStream.ReadInt32(¶metersCount);
|
|
method.Signature.Params.Resize(parametersCount);
|
|
method.ParamNames.Resize(parametersCount);
|
|
for (int32 i = 0; i < parametersCount; i++)
|
|
{
|
|
auto& param = method.Signature.Params[i];
|
|
signatureStream.ReadStringAnsi(&method.ParamNames[i], 13);
|
|
signatureStream.ReadVariantType(¶m.Type);
|
|
param.IsOut = signatureStream.ReadByte() != 0;
|
|
bool hasDefaultValue = signatureStream.ReadByte() != 0;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
method.Signature.Name = method.Name;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup fields list
|
|
_fields.Resize(Graph.Parameters.Count());
|
|
for (int32 i = 0; i < Graph.Parameters.Count(); i++)
|
|
{
|
|
auto& parameter = Graph.Parameters[i];
|
|
auto& field = _fields[i];
|
|
field.Script = this;
|
|
field.Parameter = ¶meter;
|
|
field.Index = i;
|
|
field.Name.Set(parameter.Name.Get(), parameter.Name.Length());
|
|
}
|
|
|
|
#if USE_EDITOR
|
|
if (_instances.HasItems())
|
|
{
|
|
// Setup scripting type
|
|
CacheScriptingType();
|
|
|
|
// Create default instance object to with valid new vtable
|
|
ScriptingObject* defaultInstance = _scriptingTypeHandle.GetType().GetDefaultInstance();
|
|
|
|
// Reinitialize existing Visual Script instances in the Editor
|
|
for (auto& e : _instances)
|
|
{
|
|
ScriptingObject* object = Scripting::TryFindObject<ScriptingObject>(e.Key);
|
|
if (!object)
|
|
continue;
|
|
|
|
// Hack vtable similarly to VisualScriptObjectSpawn
|
|
ScriptingType& visualScriptType = (ScriptingType&)object->GetType();
|
|
if (visualScriptType.Class.ScriptVTable)
|
|
{
|
|
// Override object vtable with hacked one that has Visual Script functions calls
|
|
ASSERT(visualScriptType.Class.VTable);
|
|
*(void**)object = visualScriptType.Class.VTable;
|
|
}
|
|
}
|
|
const int32 oldCount = _oldParamsLayout.Count();
|
|
const int32 count = Graph.Parameters.Count();
|
|
if (oldCount != 0 && count != 0)
|
|
{
|
|
// Update instanced data from previous format to the current graph parameters scheme
|
|
for (auto& e : _instances)
|
|
{
|
|
auto& instanceParams = e.Value;
|
|
Array<Variant> valuesCache(MoveTemp(instanceParams));
|
|
instanceParams.Resize(count);
|
|
for (int32 i = 0; i < count; i++)
|
|
{
|
|
const int32 oldIndex = _oldParamsLayout.Find(Graph.Parameters[i].Identifier);
|
|
instanceParams[i] = oldIndex != -1 ? valuesCache[oldIndex] : Graph.Parameters[i].Value;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Reset instances values to defaults
|
|
for (auto& e : _instances)
|
|
{
|
|
auto& instanceParams = e.Value;
|
|
instanceParams.Resize(count);
|
|
for (int32 i = 0; i < count; i++)
|
|
instanceParams[i] = Graph.Parameters[i].Value;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return LoadResult::Ok;
|
|
}
|
|
|
|
void VisualScript::unload(bool isReloading)
|
|
{
|
|
#if USE_EDITOR
|
|
if (isReloading)
|
|
{
|
|
// Cache existing instanced parameters IDs to restore values after asset reload (params order might be changed but the IDs are stable)
|
|
_oldParamsLayout.Resize(Graph.Parameters.Count());
|
|
for (int32 i = 0; i < Graph.Parameters.Count(); i++)
|
|
{
|
|
auto& param = Graph.Parameters[i];
|
|
_oldParamsLayout[i] = param.Identifier;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_oldParamsLayout.Clear();
|
|
}
|
|
#else
|
|
_instances.Clear();
|
|
#endif
|
|
|
|
// Clear resources
|
|
_methods.Clear();
|
|
_fields.Clear();
|
|
Graph.Clear();
|
|
|
|
// Note: preserve the registered scripting type but invalidate the locally cached handle
|
|
if (_scriptingTypeHandle)
|
|
{
|
|
auto& type = VisualScriptingModule.Types[_scriptingTypeHandle.TypeIndex];
|
|
if (type.Class.DefaultInstance)
|
|
{
|
|
Delete(type.Class.DefaultInstance);
|
|
type.Class.DefaultInstance = nullptr;
|
|
}
|
|
VisualScriptingModule.TypeNameToTypeIndex.RemoveValue(_scriptingTypeHandle.TypeIndex);
|
|
VisualScriptingModule.Scripts[_scriptingTypeHandle.TypeIndex] = nullptr;
|
|
_scriptingTypeHandleCached = _scriptingTypeHandle;
|
|
_scriptingTypeHandle = ScriptingTypeHandle();
|
|
}
|
|
}
|
|
|
|
AssetChunksFlag VisualScript::getChunksToPreload() const
|
|
{
|
|
return GET_CHUNK_FLAG(0) | GET_CHUNK_FLAG(1);
|
|
}
|
|
|
|
void VisualScript::CacheScriptingType()
|
|
{
|
|
auto& binaryModule = VisualScriptingModule;
|
|
|
|
// Find base type
|
|
const StringAnsi baseTypename(Meta.BaseTypename);
|
|
const ScriptingTypeHandle baseType = Scripting::FindScriptingType(baseTypename);
|
|
if (baseType)
|
|
{
|
|
// Find first native base C++ class of this Visual Script class
|
|
ScriptingTypeHandle nativeType = baseType;
|
|
while (nativeType && nativeType.GetType().Class.ScriptVTable)
|
|
{
|
|
nativeType = nativeType.GetType().GetBaseType();
|
|
}
|
|
if (!nativeType)
|
|
{
|
|
LOG(Error, "Missing native base class for {0}", ToString());
|
|
return;
|
|
}
|
|
|
|
// Create scripting type
|
|
if (_scriptingTypeHandleCached)
|
|
{
|
|
// Reuse cached slot (already created objects with that type handle can use the new type info eg. after asset reload when editing script in editor)
|
|
ASSERT(_scriptingTypeHandleCached.GetType().Fullname == _typename);
|
|
_scriptingTypeHandle = _scriptingTypeHandleCached;
|
|
_scriptingTypeHandleCached = ScriptingTypeHandle();
|
|
auto& type = VisualScriptingModule.Types[_scriptingTypeHandle.TypeIndex];
|
|
type.~ScriptingType();
|
|
new(&type)ScriptingType(_typename, &binaryModule, baseType.GetType().Size, ScriptingType::DefaultInitRuntime, VisualScriptingBinaryModule::VisualScriptObjectSpawn, baseType);
|
|
binaryModule.Scripts[_scriptingTypeHandle.TypeIndex] = this;
|
|
}
|
|
else
|
|
{
|
|
// Allocate new slot
|
|
const int32 typeIndex = binaryModule.Types.Count();
|
|
binaryModule.Types.AddUninitialized();
|
|
new(binaryModule.Types.Get() + binaryModule.Types.Count() - 1)ScriptingType(_typename, &binaryModule, baseType.GetType().Size, ScriptingType::DefaultInitRuntime, VisualScriptingBinaryModule::VisualScriptObjectSpawn, baseType);
|
|
binaryModule.TypeNameToTypeIndex[_typename] = typeIndex;
|
|
_scriptingTypeHandle = ScriptingTypeHandle(&binaryModule, typeIndex);
|
|
binaryModule.Scripts.Add(this);
|
|
|
|
#if USE_EDITOR
|
|
// When first Visual Script gets loaded register for other modules unload to clear runtime execution cache
|
|
if (typeIndex == 0)
|
|
{
|
|
Scripting::ScriptsReloading.Bind<VisualScriptingBinaryModule, &VisualScriptingBinaryModule::OnScriptsReloading>(&binaryModule);
|
|
}
|
|
#endif
|
|
}
|
|
auto& type = _scriptingTypeHandle.Module->Types[_scriptingTypeHandle.TypeIndex];
|
|
type.ManagedClass = baseType.GetType().ManagedClass;
|
|
|
|
// Create custom vtable for this class (build out of the wrapper C++ methods that call Visual Script graph)
|
|
// Call setup for all class starting from the first native type (first that uses virtual calls will allocate table of a proper size, further base types will just add own methods)
|
|
for (ScriptingTypeHandle e = nativeType; e;)
|
|
{
|
|
const ScriptingType& eType = e.GetType();
|
|
if (eType.Class.SetupScriptVTable)
|
|
{
|
|
ASSERT(eType.ManagedClass);
|
|
eType.Class.SetupScriptVTable(eType.ManagedClass, type.Class.ScriptVTable, type.Class.ScriptVTableBase);
|
|
}
|
|
e = eType.GetBaseType();
|
|
}
|
|
MMethod** scriptVTable = (MMethod**)type.Class.ScriptVTable;
|
|
while (scriptVTable && *scriptVTable)
|
|
{
|
|
const MMethod* referenceMethod = *scriptVTable;
|
|
|
|
// Find that method overriden in Visual Script (the current or one of the base classes in Visual Script)
|
|
auto node = FindMethod(referenceMethod->GetName(), referenceMethod->GetParametersCount());
|
|
if (node == nullptr)
|
|
{
|
|
// Check base classes that are Visual Script
|
|
auto e = baseType;
|
|
while (e.Module == &binaryModule && node == nullptr)
|
|
{
|
|
auto& eType = e.GetType();
|
|
Guid id;
|
|
if (!Guid::Parse(eType.Fullname, id))
|
|
{
|
|
if (const auto visualScript = Content::LoadAsync<VisualScript>(id))
|
|
{
|
|
node = visualScript->FindMethod(referenceMethod->GetName(), referenceMethod->GetParametersCount());
|
|
}
|
|
}
|
|
e = e.GetType().GetBaseType();
|
|
}
|
|
}
|
|
|
|
// Set the method to call (null entry marks unused entries that won't use Visual Script wrapper calls)
|
|
*scriptVTable = (MMethod*)node;
|
|
|
|
// Move to the next entry (table is null terminated)
|
|
scriptVTable++;
|
|
}
|
|
}
|
|
else if (Meta.BaseTypename.HasChars())
|
|
{
|
|
LOG(Error, "Failed to find a scripting type \'{0}\' that is a base type for {1}", Meta.BaseTypename, ToString());
|
|
}
|
|
else
|
|
{
|
|
LOG(Error, "Cannot use {0} as script because base typename is missing.", ToString());
|
|
}
|
|
}
|
|
|
|
VisualScriptingBinaryModule::VisualScriptingBinaryModule()
|
|
: _name("Visual Scripting")
|
|
{
|
|
}
|
|
|
|
ScriptingObject* VisualScriptingBinaryModule::VisualScriptObjectSpawn(const ScriptingObjectSpawnParams& params)
|
|
{
|
|
// Create native object (base type can be C++ or C#)
|
|
ScriptingType& visualScriptType = (ScriptingType&)params.Type.GetType();
|
|
ScriptingTypeHandle baseTypeHandle = visualScriptType.GetBaseType();
|
|
const ScriptingType* baseTypePtr = &baseTypeHandle.GetType();
|
|
while (baseTypePtr->Class.Spawn == &VisualScriptObjectSpawn)
|
|
{
|
|
baseTypeHandle = baseTypePtr->GetBaseType();
|
|
baseTypePtr = &baseTypeHandle.GetType();
|
|
}
|
|
ScriptingObject* object = baseTypePtr->Class.Spawn(params);
|
|
if (!object)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// Beware! Hacking vtables incoming! Undefined behaviors exploits! Low-level programming!
|
|
// What's happening here?
|
|
// We create a custom vtable for the Visual Script objects that use a native class object with virtual functions overrides.
|
|
// To make it easy to use in C++ we inject custom wrapper methods into C++ object vtable to execute Visual Script graph from them.
|
|
// Because virtual member functions calls are C++ ABI and impl-defined this is quite hard. But works.
|
|
if (visualScriptType.Class.ScriptVTable)
|
|
{
|
|
if (!visualScriptType.Class.VTable)
|
|
{
|
|
// Duplicate vtable
|
|
void** vtable = *(void***)object;
|
|
const int32 prefixSize = GetVTablePrefix();
|
|
int32 entriesCount = 0;
|
|
while (vtable[entriesCount] && entriesCount < 200)
|
|
entriesCount++;
|
|
const int32 size = entriesCount * sizeof(void*);
|
|
visualScriptType.Class.VTable = (void**)((byte*)Platform::Allocate(prefixSize + size, 16) + prefixSize);
|
|
Platform::MemoryCopy((byte*)visualScriptType.Class.VTable - prefixSize, (byte*)vtable - prefixSize, prefixSize + size);
|
|
|
|
// Override vtable entries by the class
|
|
for (ScriptingTypeHandle e = baseTypeHandle; e;)
|
|
{
|
|
const ScriptingType& eType = e.GetType();
|
|
if (eType.Class.SetupScriptObjectVTable)
|
|
eType.Class.SetupScriptObjectVTable(visualScriptType.Class.ScriptVTable, visualScriptType.Class.ScriptVTableBase, visualScriptType.Class.VTable, entriesCount, 1);
|
|
e = eType.GetBaseType();
|
|
}
|
|
}
|
|
|
|
// Override object vtable with hacked one that has Visual Script functions calls
|
|
*(void**)object = visualScriptType.Class.VTable;
|
|
}
|
|
|
|
// Mark as custom scripting type
|
|
object->Flags |= ObjectFlags::IsCustomScriptingType;
|
|
|
|
// Get Visual Script asset
|
|
ASSERT(&VisualScriptingModule == params.Type.Module);
|
|
VisualScript* visualScript = VisualScriptingModule.Scripts[params.Type.TypeIndex];
|
|
|
|
// Initialize instance data
|
|
auto& instanceParams = visualScript->_instances[object->GetID()];
|
|
instanceParams.Resize(visualScript->Graph.Parameters.Count());
|
|
for (int32 i = 0; i < instanceParams.Count(); i++)
|
|
instanceParams[i] = visualScript->Graph.Parameters[i].Value;
|
|
|
|
return object;
|
|
}
|
|
|
|
#if USE_EDITOR
|
|
|
|
void VisualScriptingBinaryModule::OnScriptsReloading()
|
|
{
|
|
// Clear any cached types from that module across all loaded Visual Scripts
|
|
for (auto& script : Scripts)
|
|
{
|
|
if (!script || !script->IsLoaded())
|
|
continue;
|
|
ScopeLock lock(script->Locker);
|
|
|
|
// Clear cached types (underlying base class could be in reloaded C# scripts)
|
|
if (script->_scriptingTypeHandle)
|
|
{
|
|
auto& type = VisualScriptingModule.Types[script->_scriptingTypeHandle.TypeIndex];
|
|
if (type.Class.DefaultInstance)
|
|
{
|
|
Delete(type.Class.DefaultInstance);
|
|
type.Class.DefaultInstance = nullptr;
|
|
}
|
|
VisualScriptingModule.TypeNameToTypeIndex.RemoveValue(script->_scriptingTypeHandle.TypeIndex);
|
|
script->_scriptingTypeHandleCached = script->_scriptingTypeHandle;
|
|
script->_scriptingTypeHandle = ScriptingTypeHandle();
|
|
}
|
|
|
|
// Clear methods cache
|
|
for (auto& node : script->Graph.Nodes)
|
|
{
|
|
switch (node.Type)
|
|
{
|
|
// Invoke Method
|
|
case GRAPH_NODE_MAKE_TYPE(16, 4):
|
|
{
|
|
node.Data.InvokeMethod.Method = nullptr;
|
|
break;
|
|
}
|
|
// Get/Set Field
|
|
case GRAPH_NODE_MAKE_TYPE(16, 7):
|
|
case GRAPH_NODE_MAKE_TYPE(16, 8):
|
|
{
|
|
node.Data.GetSetField.Field = nullptr;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
const StringAnsi& VisualScriptingBinaryModule::GetName() const
|
|
{
|
|
return _name;
|
|
}
|
|
|
|
bool VisualScriptingBinaryModule::IsLoaded() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool VisualScriptingBinaryModule::FindScriptingType(const StringAnsiView& typeName, int32& typeIndex)
|
|
{
|
|
// Type Name for Visual Scripts is 32 chars Guid representation of asset ID
|
|
if (typeName.Length() == 32)
|
|
{
|
|
if (TypeNameToTypeIndex.TryGet(typeName, typeIndex))
|
|
return true;
|
|
Guid id;
|
|
if (!Guid::Parse(typeName, id))
|
|
{
|
|
const auto visualScript = Content::LoadAsync<VisualScript>(id);
|
|
if (visualScript)
|
|
{
|
|
const auto handle = visualScript->GetScriptingType();
|
|
if (handle)
|
|
{
|
|
typeIndex = handle.TypeIndex;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void* VisualScriptingBinaryModule::FindMethod(const ScriptingTypeHandle& typeHandle, const StringAnsiView& name, int32 numParams)
|
|
{
|
|
return (void*)Scripts[typeHandle.TypeIndex]->FindMethod(name, numParams);
|
|
}
|
|
|
|
bool VisualScriptingBinaryModule::InvokeMethod(void* method, const Variant& instance, Span<Variant> paramValues, Variant& result)
|
|
{
|
|
auto vsMethod = (VisualScript::Method*)method;
|
|
ScriptingObject* instanceObject = nullptr;
|
|
if (!vsMethod->Signature.IsStatic)
|
|
{
|
|
instanceObject = (ScriptingObject*)instance;
|
|
if (!instanceObject || instanceObject->GetTypeHandle() != vsMethod->Script->GetScriptingType())
|
|
{
|
|
if (!instanceObject)
|
|
LOG(Error, "Failed to call method '{0}.{1}' (args count: {2}) without object instance", String(vsMethod->Script->GetScriptTypeName()), String(vsMethod->Name), vsMethod->ParamNames.Count());
|
|
else
|
|
LOG(Error, "Failed to call method '{0}.{1}' (args count: {2}) with invalid object instance of type '{3}'", String(vsMethod->Script->GetScriptTypeName()), String(vsMethod->Name), vsMethod->ParamNames.Count(), String(instanceObject->GetType().Fullname));
|
|
return true;
|
|
}
|
|
}
|
|
result = VisualScripting::Invoke(vsMethod, instanceObject, paramValues);
|
|
return false;
|
|
}
|
|
|
|
void VisualScriptingBinaryModule::GetMethodSignature(void* method, ScriptingTypeMethodSignature& methodSignature)
|
|
{
|
|
const auto vsMethod = (const VisualScript::Method*)method;
|
|
methodSignature = vsMethod->Signature;
|
|
}
|
|
|
|
void* VisualScriptingBinaryModule::FindField(const ScriptingTypeHandle& typeHandle, const StringAnsiView& name)
|
|
{
|
|
return (void*)Scripts[typeHandle.TypeIndex]->FindField(name);
|
|
}
|
|
|
|
void VisualScriptingBinaryModule::GetFieldSignature(void* field, ScriptingTypeFieldSignature& fieldSignature)
|
|
{
|
|
const auto vsFiled = (VisualScript::Field*)field;
|
|
fieldSignature.Name = vsFiled->Name;
|
|
fieldSignature.ValueType = vsFiled->Parameter->Type;
|
|
fieldSignature.IsStatic = false;
|
|
}
|
|
|
|
bool VisualScriptingBinaryModule::GetFieldValue(void* field, const Variant& instance, Variant& result)
|
|
{
|
|
const auto vsFiled = (VisualScript::Field*)field;
|
|
const auto instanceObject = (ScriptingObject*)instance;
|
|
if (!instanceObject)
|
|
{
|
|
LOG(Error, "Failed to get field '{0}' without object instance", vsFiled->Parameter->Name);
|
|
return true;
|
|
}
|
|
const auto instanceParams = vsFiled->Script->_instances.Find(instanceObject->GetID());
|
|
if (!instanceParams)
|
|
{
|
|
LOG(Error, "Missing parameters for the object instance.");
|
|
return true;
|
|
}
|
|
result = instanceParams->Value[vsFiled->Index];
|
|
return false;
|
|
}
|
|
|
|
bool VisualScriptingBinaryModule::SetFieldValue(void* field, const Variant& instance, Variant& value)
|
|
{
|
|
const auto vsFiled = (VisualScript::Field*)field;
|
|
const auto instanceObject = (ScriptingObject*)instance;
|
|
if (!instanceObject)
|
|
{
|
|
LOG(Error, "Failed to set field '{0}' without object instance", vsFiled->Parameter->Name);
|
|
return true;
|
|
}
|
|
const auto instanceParams = vsFiled->Script->_instances.Find(instanceObject->GetID());
|
|
if (!instanceParams)
|
|
{
|
|
LOG(Error, "Missing parameters for the object instance.");
|
|
return true;
|
|
}
|
|
instanceParams->Value[vsFiled->Index] = value;
|
|
return false;
|
|
}
|
|
|
|
void VisualScriptingBinaryModule::SerializeObject(JsonWriter& stream, ScriptingObject* object, const ScriptingObject* otherObj)
|
|
{
|
|
char idName[33];
|
|
stream.StartObject();
|
|
const auto asset = Scripts[object->GetTypeHandle().TypeIndex].Get();
|
|
if (asset)
|
|
{
|
|
const auto instanceParams = asset->_instances.Find(object->GetID());
|
|
if (instanceParams)
|
|
{
|
|
auto& params = instanceParams->Value;
|
|
if (otherObj)
|
|
{
|
|
// Serialize parameters diff
|
|
const auto otherParams = asset->_instances.Find(otherObj->GetID());
|
|
if (otherParams)
|
|
{
|
|
for (int32 paramIndex = 0; paramIndex < params.Count(); paramIndex++)
|
|
{
|
|
auto& param = asset->Graph.Parameters[paramIndex];
|
|
auto& value = params[paramIndex];
|
|
auto& otherValue = otherParams->Value[paramIndex];
|
|
if (value != otherValue)
|
|
{
|
|
param.Identifier.ToString(idName, Guid::FormatType::N);
|
|
stream.Key(idName, 32);
|
|
Serialization::Serialize(stream, params[paramIndex], &otherValue);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int32 paramIndex = 0; paramIndex < params.Count(); paramIndex++)
|
|
{
|
|
auto& param = asset->Graph.Parameters[paramIndex];
|
|
auto& value = params[paramIndex];
|
|
auto& otherValue = param.Value;
|
|
if (value != otherValue)
|
|
{
|
|
param.Identifier.ToString(idName, Guid::FormatType::N);
|
|
stream.Key(idName, 32);
|
|
Serialization::Serialize(stream, params[paramIndex], &otherValue);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Serialize all parameters
|
|
for (int32 paramIndex = 0; paramIndex < params.Count(); paramIndex++)
|
|
{
|
|
auto& param = asset->Graph.Parameters[paramIndex];
|
|
auto& value = params[paramIndex];
|
|
param.Identifier.ToString(idName, Guid::FormatType::N);
|
|
stream.Key(idName, 32);
|
|
Serialization::Serialize(stream, value, nullptr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
stream.EndObject();
|
|
}
|
|
|
|
void VisualScriptingBinaryModule::DeserializeObject(ISerializable::DeserializeStream& stream, ScriptingObject* object, ISerializeModifier* modifier)
|
|
{
|
|
ASSERT(stream.IsObject());
|
|
const auto asset = Scripts[object->GetTypeHandle().TypeIndex].Get();
|
|
if (asset)
|
|
{
|
|
const auto instanceParams = asset->_instances.Find(object->GetID());
|
|
if (instanceParams)
|
|
{
|
|
// Deserialize all parameters
|
|
auto& params = instanceParams->Value;
|
|
for (auto i = stream.MemberBegin(); i != stream.MemberEnd(); ++i)
|
|
{
|
|
StringAnsiView idNameAnsi(i->name.GetString(), i->name.GetStringLength());
|
|
Guid paramId;
|
|
if (!Guid::Parse(idNameAnsi, paramId))
|
|
{
|
|
int32 paramIndex;
|
|
if (asset->Graph.GetParameter(paramId, paramIndex))
|
|
{
|
|
Serialization::Deserialize(i->value, params[paramIndex], modifier);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void VisualScriptingBinaryModule::OnObjectIdChanged(ScriptingObject* object, const Guid& oldId)
|
|
{
|
|
const auto asset = Scripts[object->GetTypeHandle().TypeIndex].Get();
|
|
if (asset)
|
|
{
|
|
auto& instanceParams = asset->_instances[object->GetID()];
|
|
auto oldParams = asset->_instances.Find(oldId);
|
|
if (oldParams)
|
|
{
|
|
instanceParams = MoveTemp(oldParams->Value);
|
|
asset->_instances.Remove(oldParams);
|
|
}
|
|
}
|
|
}
|
|
|
|
void VisualScriptingBinaryModule::OnObjectDeleted(ScriptingObject* object)
|
|
{
|
|
const auto asset = Scripts[object->GetTypeHandle().TypeIndex].Get();
|
|
if (asset)
|
|
{
|
|
// Cleanup object data
|
|
asset->_instances.Remove(object->GetID());
|
|
}
|
|
}
|
|
|
|
void VisualScriptingBinaryModule::Destroy(bool isReloading)
|
|
{
|
|
// Skip module unregister during reloads (Visual Scripts are persistent)
|
|
if (isReloading)
|
|
return;
|
|
|
|
BinaryModule::Destroy(isReloading);
|
|
}
|
|
|
|
ScriptingTypeHandle VisualScript::GetScriptingType()
|
|
{
|
|
if (!_scriptingTypeHandle && !WaitForLoaded())
|
|
{
|
|
CacheScriptingType();
|
|
}
|
|
return _scriptingTypeHandle;
|
|
}
|
|
|
|
ScriptingObject* VisualScript::CreateInstance()
|
|
{
|
|
const auto scriptingTypeHandle = GetScriptingType();
|
|
return scriptingTypeHandle ? scriptingTypeHandle.GetType().Class.Spawn(ScriptingObjectSpawnParams(Guid::New(), scriptingTypeHandle)) : nullptr;
|
|
}
|
|
|
|
Variant VisualScript::GetScriptInstanceParameterValue(const StringView& name, ScriptingObject* instance) const
|
|
{
|
|
CHECK_RETURN(instance, Variant());
|
|
for (int32 paramIndex = 0; paramIndex < Graph.Parameters.Count(); paramIndex++)
|
|
{
|
|
if (Graph.Parameters[paramIndex].Name == name)
|
|
{
|
|
const auto instanceParams = _instances.Find(instance->GetID());
|
|
if (instanceParams)
|
|
return instanceParams->Value[paramIndex];
|
|
LOG(Error, "Failed to access Visual Script parameter {1} for {0}.", instance->ToString(), name);
|
|
return Graph.Parameters[paramIndex].Value;
|
|
}
|
|
}
|
|
LOG(Warning, "Failed to get {0} parameter '{1}'", ToString(), name);
|
|
return Variant();
|
|
}
|
|
|
|
void VisualScript::SetScriptInstanceParameterValue(const StringView& name, ScriptingObject* instance, const Variant& value) const
|
|
{
|
|
CHECK(instance);
|
|
for (int32 paramIndex = 0; paramIndex < Graph.Parameters.Count(); paramIndex++)
|
|
{
|
|
if (Graph.Parameters[paramIndex].Name == name)
|
|
{
|
|
const auto instanceParams = _instances.Find(instance->GetID());
|
|
if (instanceParams)
|
|
{
|
|
instanceParams->Value[paramIndex] = value;
|
|
return;
|
|
}
|
|
LOG(Error, "Failed to access Visual Script parameter {1} for {0}.", instance->ToString(), name);
|
|
return;
|
|
}
|
|
}
|
|
LOG(Warning, "Failed to set {0} parameter '{1}'", ToString(), name);
|
|
}
|
|
|
|
void VisualScript::SetScriptInstanceParameterValue(const StringView& name, ScriptingObject* instance, Variant&& value) const
|
|
{
|
|
CHECK(instance);
|
|
for (int32 paramIndex = 0; paramIndex < Graph.Parameters.Count(); paramIndex++)
|
|
{
|
|
if (Graph.Parameters[paramIndex].Name == name)
|
|
{
|
|
const auto instanceParams = _instances.Find(instance->GetID());
|
|
if (instanceParams)
|
|
{
|
|
instanceParams->Value[paramIndex] = MoveTemp(value);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
LOG(Warning, "Failed to set {0} parameter '{1}'", ToString(), name);
|
|
}
|
|
|
|
const VisualScript::Method* VisualScript::FindMethod(const StringAnsiView& name, int32 numParams) const
|
|
{
|
|
for (const auto& e : _methods)
|
|
{
|
|
if (e.Signature.Params.Count() == numParams && e.Name == name)
|
|
return &e;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const VisualScript::Field* VisualScript::FindField(const StringAnsiView& name) const
|
|
{
|
|
for (const auto& e : _fields)
|
|
{
|
|
if (e.Name == name)
|
|
return &e;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
BytesContainer VisualScript::LoadSurface()
|
|
{
|
|
ScopeLock lock(Locker);
|
|
if (!LoadChunks(GET_CHUNK_FLAG(0)))
|
|
{
|
|
const auto data = GetChunk(0);
|
|
BytesContainer result;
|
|
result.Copy(data->Data);
|
|
return result;
|
|
}
|
|
|
|
LOG(Warning, "\'{0}\' surface data is missing.", GetPath());
|
|
return BytesContainer();
|
|
}
|
|
|
|
#if USE_EDITOR
|
|
|
|
bool VisualScript::SaveSurface(BytesContainer& data, const Metadata& meta)
|
|
{
|
|
// Wait for asset to be loaded or don't if last load failed
|
|
if (LastLoadFailed())
|
|
{
|
|
LOG(Warning, "Saving asset that failed to load.");
|
|
}
|
|
else if (WaitForLoaded())
|
|
{
|
|
LOG(Error, "Asset loading failed. Cannot save it.");
|
|
return true;
|
|
}
|
|
|
|
ScopeLock lock(Locker);
|
|
|
|
// Release all chunks
|
|
for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
|
|
ReleaseChunk(i);
|
|
|
|
// Set Visject Surface data
|
|
auto visjectSurfaceChunk = GetOrCreateChunk(0);
|
|
visjectSurfaceChunk->Data.Copy(data);
|
|
|
|
// Set metadata
|
|
auto metadataChunk = GetOrCreateChunk(1);
|
|
MemoryWriteStream metaStream(512);
|
|
{
|
|
metaStream.WriteInt32(1);
|
|
metaStream.WriteString(meta.BaseTypename, 31);
|
|
metaStream.WriteInt32((int32)meta.Flags);
|
|
}
|
|
metadataChunk->Data.Copy(metaStream.GetHandle(), metaStream.GetPosition());
|
|
|
|
// Save
|
|
AssetInitData assetData;
|
|
assetData.SerializedVersion = 1;
|
|
if (SaveAsset(assetData))
|
|
{
|
|
LOG(Error, "Cannot save \'{0}\'", ToString());
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void VisualScript::GetMethodSignature(int32 index, String& name, byte& flags, String& returnTypeName, Array<String>& paramNames, Array<String>& paramTypeNames, Array<bool>& paramOuts)
|
|
{
|
|
auto& method = _methods[index];
|
|
name = String(method.Name);
|
|
flags = (byte)method.MethodFlags;
|
|
returnTypeName = method.Signature.ReturnType.GetTypeName();
|
|
paramNames.Resize(method.Signature.Params.Count());
|
|
paramTypeNames.Resize(method.Signature.Params.Count());
|
|
paramOuts.Resize(method.Signature.Params.Count());
|
|
for (int32 i = 0; i < method.Signature.Params.Count(); i++)
|
|
{
|
|
auto& param = method.Signature.Params[i];
|
|
paramNames[i] = String(method.ParamNames[i]);
|
|
paramTypeNames[i] = param.Type.GetTypeName();
|
|
paramOuts[i] = param.IsOut;
|
|
}
|
|
}
|
|
|
|
Span<byte> VisualScript::GetMetaData(int32 typeID)
|
|
{
|
|
auto meta = Graph.Meta.GetEntry(typeID);
|
|
return meta ? ToSpan(meta->Data.Get(), meta->Data.Count()) : Span<byte>(nullptr, 0);
|
|
}
|
|
|
|
Span<byte> VisualScript::GetMethodMetaData(int32 index, int32 typeID)
|
|
{
|
|
auto& method = _methods[index];
|
|
auto meta = method.Node->Meta.GetEntry(typeID);
|
|
return meta ? ToSpan(meta->Data.Get(), meta->Data.Count()) : Span<byte>(nullptr, 0);
|
|
}
|
|
|
|
#endif
|
|
|
|
VisualScripting::StackFrame* VisualScripting::GetThreadStackTop()
|
|
{
|
|
return ThreadStacks.Get().Stack;
|
|
}
|
|
|
|
String VisualScripting::GetStackTrace()
|
|
{
|
|
String result;
|
|
auto frame = ThreadStacks.Get().Stack;
|
|
while (frame)
|
|
{
|
|
String node;
|
|
switch (frame->Node->Type)
|
|
{
|
|
// Get/Set Parameter
|
|
case GRAPH_NODE_MAKE_TYPE(6, 3):
|
|
case GRAPH_NODE_MAKE_TYPE(6, 4):
|
|
{
|
|
const auto param = frame->Script->Graph.GetParameter((Guid)frame->Node->Values[0]);
|
|
node = frame->Node->TypeID == 3 ? TEXT("Get ") : TEXT("Set ");
|
|
node += param ? param->Name : ((Guid)frame->Node->Values[0]).ToString();
|
|
break;
|
|
}
|
|
// Method Override
|
|
case GRAPH_NODE_MAKE_TYPE(16, 3):
|
|
node = (StringView)frame->Node->Values[0];
|
|
node += TEXT("()");
|
|
break;
|
|
// Invoke Method
|
|
case GRAPH_NODE_MAKE_TYPE(16, 4):
|
|
node = (StringView)frame->Node->Values[0];
|
|
node += TEXT(".");
|
|
node += (StringView)frame->Node->Values[1];
|
|
node += TEXT("()");
|
|
break;
|
|
// Function
|
|
case GRAPH_NODE_MAKE_TYPE(16, 6):
|
|
node = String(frame->Script->GetScriptTypeName());
|
|
for (int32 i = 0; i < frame->Script->_methods.Count(); i++)
|
|
{
|
|
auto& method = frame->Script->_methods[i];
|
|
if (method.Node == frame->Node)
|
|
{
|
|
node += TEXT(".");
|
|
node += String(method.Name);
|
|
node += TEXT("()");
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
node = StringUtils::ToString(frame->Node->Type);
|
|
break;
|
|
}
|
|
result += String::Format(TEXT(" at {0}:{1} in node {2}\n"), StringUtils::GetFileNameWithoutExtension(frame->Script->GetPath()), frame->Script->GetID(), node);
|
|
frame = frame->PreviousFrame;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
VisualScriptingBinaryModule* VisualScripting::GetBinaryModule()
|
|
{
|
|
return &VisualScriptingModule;
|
|
}
|
|
|
|
Variant VisualScripting::Invoke(VisualScript::Method* method, ScriptingObject* instance, Span<Variant> parameters)
|
|
{
|
|
CHECK_RETURN(method && method->Script->IsLoaded(), Variant::Zero);
|
|
|
|
// Add to the calling stack
|
|
ScopeContext scope;
|
|
scope.Parameters = parameters;
|
|
auto& stack = ThreadStacks.Get();
|
|
StackFrame frame;
|
|
frame.Script = method->Script;
|
|
frame.Node = method->Node;
|
|
frame.Box = method->Node->GetBox(0);
|
|
frame.Instance = instance;
|
|
frame.PreviousFrame = stack.Stack;
|
|
frame.Scope = &scope;
|
|
stack.Stack = &frame;
|
|
stack.StackFramesCount++;
|
|
|
|
// Call per group custom processing event
|
|
const auto func = VisualScriptingExecutor._perGroupProcessCall[method->Node->GroupID];
|
|
(VisualScriptingExecutor.*func)(frame.Box, method->Node, scope.FunctionReturn);
|
|
|
|
// Remove from the calling stack
|
|
stack.StackFramesCount--;
|
|
stack.Stack = frame.PreviousFrame;
|
|
|
|
return scope.FunctionReturn;
|
|
}
|
|
|
|
#if VISUAL_SCRIPT_DEBUGGING
|
|
|
|
bool VisualScripting::Evaluate(VisualScript* script, ScriptingObject* instance, uint32 nodeId, uint32 boxId, Variant& result)
|
|
{
|
|
if (!script)
|
|
return false;
|
|
const auto node = script->Graph.GetNode(nodeId);
|
|
if (!node)
|
|
return false;
|
|
const auto box = node->GetBox(boxId);
|
|
if (!box)
|
|
return false;
|
|
|
|
// Add to the calling stack
|
|
ScopeContext scope;
|
|
auto& stack = ThreadStacks.Get();
|
|
StackFrame frame;
|
|
frame.Script = script;
|
|
frame.Node = node;
|
|
frame.Box = box;
|
|
frame.Instance = instance;
|
|
frame.PreviousFrame = stack.Stack;
|
|
frame.Scope = &scope;
|
|
stack.Stack = &frame;
|
|
stack.StackFramesCount++;
|
|
|
|
// Call per group custom processing event
|
|
const auto func = VisualScriptingExecutor._perGroupProcessCall[node->GroupID];
|
|
(VisualScriptingExecutor.*func)(box, node, result);
|
|
|
|
// Remove from the calling stack
|
|
stack.StackFramesCount--;
|
|
stack.Stack = frame.PreviousFrame;
|
|
|
|
return true;
|
|
}
|
|
|
|
#endif
|