2469 lines
89 KiB
C++
2469 lines
89 KiB
C++
// Copyright (c) Wojciech Figat. All rights reserved.
|
|
|
|
#include "VisualScript.h"
|
|
#include "Engine/Core/Log.h"
|
|
#include "Engine/Core/Types/Span.h"
|
|
#include "Engine/Core/Types/DataContainer.h"
|
|
#include "Engine/Content/Content.h"
|
|
#include "Engine/Content/Factories/BinaryAssetFactory.h"
|
|
#include "Engine/Scripting/Scripting.h"
|
|
#include "Engine/Scripting/Events.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/MException.h"
|
|
#include "Engine/Serialization/MemoryReadStream.h"
|
|
#include "Engine/Serialization/MemoryWriteStream.h"
|
|
#include "Engine/Serialization/Serialization.h"
|
|
#include "Engine/Serialization/JsonWriter.h"
|
|
#include "Engine/Profiler/ProfilerCPU.h"
|
|
#include "Engine/Profiler/ProfilerMemory.h"
|
|
#include "Engine/Utilities/StringConverter.h"
|
|
#include "Engine/Threading/MainThreadTask.h"
|
|
#include "Engine/Level/SceneObject.h"
|
|
#include "FlaxEngine.Gen.h"
|
|
|
|
namespace
|
|
{
|
|
struct VisualScriptThread
|
|
{
|
|
uint32 StackFramesCount;
|
|
VisualScripting::StackFrame* Stack;
|
|
};
|
|
|
|
ThreadLocal<VisualScriptThread> ThreadStacks;
|
|
VisualScriptingBinaryModule VisualScriptingModule;
|
|
VisualScriptExecutor VisualScriptingExecutor;
|
|
|
|
void PrintStack(LogType type)
|
|
{
|
|
#if LOG_ENABLE
|
|
const String stack = VisualScripting::GetStackTrace();
|
|
Log::Logger::Write(type, TEXT("Visual Script stack trace:"));
|
|
Log::Logger::Write(type, stack);
|
|
Log::Logger::Write(type, TEXT(""));
|
|
#endif
|
|
}
|
|
|
|
bool SerializeValue(const Variant& a, const Variant& b)
|
|
{
|
|
bool result = a != b;
|
|
if (result)
|
|
{
|
|
// Special case for scene objects to handle prefab object references
|
|
auto* aSceneObject = ScriptingObject::Cast<SceneObject>((ScriptingObject*)a);
|
|
auto* bSceneObject = ScriptingObject::Cast<SceneObject>((ScriptingObject*)b);
|
|
if (aSceneObject && bSceneObject)
|
|
{
|
|
result = Serialization::ShouldSerialize(aSceneObject, bSceneObject);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
#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[6] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupParameters;
|
|
_perGroupProcessCall[7] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupTools;
|
|
_perGroupProcessCall[16] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupFunction;
|
|
_perGroupProcessCall[17] = (ProcessBoxHandler)&VisualScriptExecutor::ProcessGroupFlow;
|
|
}
|
|
|
|
void VisualScriptExecutor::Invoke(const Guid& scriptId, int32 nodeId, int32 boxId, const Guid& instanceId, Variant& result) const
|
|
{
|
|
auto script = Content::Load<VisualScript>(scriptId);
|
|
if (!script)
|
|
return;
|
|
const auto node = script->Graph.GetNode(nodeId);
|
|
if (!node)
|
|
return;
|
|
const auto box = node->GetBox(boxId);
|
|
if (!box)
|
|
return;
|
|
auto instance = Scripting::FindObject<ScriptingObject>(instanceId);
|
|
|
|
// Add to the calling stack
|
|
VisualScripting::ScopeContext scope;
|
|
auto& stack = ThreadStacks.Get();
|
|
VisualScripting::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;
|
|
}
|
|
|
|
void VisualScriptExecutor::OnError(Node* node, Box* box, const StringView& message)
|
|
{
|
|
VisjectExecutor::OnError(node, box, message);
|
|
PrintStack(LogType::Error);
|
|
}
|
|
|
|
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::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);
|
|
ScopeLock lock(stack.Stack->Script->Locker);
|
|
const auto instanceParams = stack.Stack->Script->_instances.Find(stack.Stack->Instance->GetID());
|
|
if (param && instanceParams)
|
|
{
|
|
value = instanceParams->Value.Params[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);
|
|
ScopeLock lock(stack.Stack->Script->Locker);
|
|
const auto instanceParams = stack.Stack->Script->_instances.Find(stack.Stack->Instance->GetID());
|
|
if (param && instanceParams)
|
|
{
|
|
instanceParams->Value.Params[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 (box->ID == 0 && 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;
|
|
// 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)
|
|
{
|
|
#if USE_CSHARP
|
|
MClass* klass = Scripting::FindClass(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
|
|
MClass* objKlass = MUtils::GetClass(obj);
|
|
if (!klass || !objKlass || !objKlass->IsSubClassOf(klass))
|
|
obj = Value::Null;
|
|
#else
|
|
const ScriptingTypeHandle type = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length()));
|
|
const ScriptingTypeHandle objType = Scripting::FindScriptingType(obj.Type.GetTypeName());
|
|
if (!type || !objType || !objType.IsSubclassOf(type))
|
|
obj = Value::Null;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
// Reroute
|
|
case 29:
|
|
if (node->GetBox(0) == box)
|
|
{
|
|
// Impulse flow
|
|
box = node->GetBox(1);
|
|
if (box->HasConnection())
|
|
eatBox(node, box->FirstConnection());
|
|
}
|
|
else
|
|
value = tryGetValue(node->GetBox(0), Value::Zero);
|
|
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.Read(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.Read(param.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
|
|
{
|
|
#if !COMPILE_WITHOUT_CSHARP
|
|
// 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
|
|
#endif
|
|
{
|
|
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;
|
|
int32 index = boxBase->ID - 1;
|
|
if (index < scope->Parameters.Length())
|
|
value = scope->Parameters.Get()[index];
|
|
}
|
|
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;
|
|
}
|
|
// Bind/Unbind
|
|
case 9:
|
|
case 10:
|
|
{
|
|
const bool bind = node->TypeID == 9;
|
|
auto& stack = ThreadStacks.Get();
|
|
if (!stack.Stack->Instance)
|
|
{
|
|
// TODO: add support for binding to events in static Visual Script
|
|
LOG(Error, "Cannot bind to event in static Visual Script.");
|
|
PrintStack(LogType::Error);
|
|
break;
|
|
}
|
|
const auto object = stack.Stack->Instance;
|
|
|
|
// Find method to bind
|
|
VisualScriptGraphNode* methodNode = nullptr;
|
|
const auto graph = stack.Stack && stack.Stack->Script ? &stack.Stack->Script->Graph : nullptr;
|
|
if (graph)
|
|
methodNode = graph->GetNode((uint32)node->Values[2]);
|
|
if (!methodNode)
|
|
{
|
|
LOG(Error, "Missing function handler to bind to the event.");
|
|
PrintStack(LogType::Error);
|
|
break;
|
|
}
|
|
VisualScript::Method* method = nullptr;
|
|
for (auto& m : stack.Stack->Script->_methods)
|
|
{
|
|
if (m.Node == methodNode)
|
|
{
|
|
method = &m;
|
|
break;
|
|
}
|
|
}
|
|
if (!method)
|
|
{
|
|
LOG(Error, "Missing method to bind to the event.");
|
|
PrintStack(LogType::Error);
|
|
break;
|
|
}
|
|
|
|
// Find event
|
|
const StringView eventTypeName(node->Values[0]);
|
|
const StringView eventName(node->Values[1]);
|
|
const StringAsANSI<100> eventTypeNameAnsi(eventTypeName.Get(), eventTypeName.Length());
|
|
const ScriptingTypeHandle eventType = Scripting::FindScriptingType(StringAnsiView(eventTypeNameAnsi.Get(), eventTypeName.Length()));
|
|
|
|
// Find event binding callback
|
|
auto eventBinder = ScriptingEvents::EventsTable.TryGet(Pair<ScriptingTypeHandle, StringView>(eventType, eventName));
|
|
if (!eventBinder)
|
|
{
|
|
LOG(Error, "Cannot bind to missing event {0} from type {1}.", eventName, eventTypeName);
|
|
PrintStack(LogType::Error);
|
|
break;
|
|
}
|
|
|
|
// Evaluate object instance
|
|
const auto box = node->GetBox(1);
|
|
Variant instance;
|
|
if (box->HasConnection())
|
|
instance = eatBox(node, box->FirstConnection());
|
|
else
|
|
instance.SetObject(object);
|
|
ScriptingObject* instanceObj = (ScriptingObject*)instance;
|
|
if (!instanceObj)
|
|
{
|
|
LOG(Error, "Cannot bind event to null object.");
|
|
PrintStack(LogType::Error);
|
|
break;
|
|
}
|
|
if (boxBase->ID == 1)
|
|
{
|
|
value = instance;
|
|
break;
|
|
}
|
|
// TODO: check if instance is of event type (including inheritance)
|
|
|
|
// Add Visual Script method to the event bindings table
|
|
const auto& type = object->GetType();
|
|
Guid id;
|
|
if (Guid::Parse(type.Fullname, id))
|
|
break;
|
|
if (const auto visualScript = (VisualScript*)Content::GetAsset(id))
|
|
{
|
|
if (auto i = visualScript->GetScriptInstance(object))
|
|
{
|
|
VisualScript::EventBinding* eventBinding = nullptr;
|
|
for (auto& b : i->EventBindings)
|
|
{
|
|
if (b.Type == eventType && b.Name == eventName)
|
|
{
|
|
eventBinding = &b;
|
|
break;
|
|
}
|
|
}
|
|
if (bind)
|
|
{
|
|
// Bind to the event
|
|
if (!eventBinding)
|
|
{
|
|
eventBinding = &i->EventBindings.AddOne();
|
|
eventBinding->Type = eventType;
|
|
eventBinding->Name = eventName;
|
|
}
|
|
eventBinding->BindedMethods.Add(method);
|
|
if (eventBinding->BindedMethods.Count() == 1)
|
|
(*eventBinder)(instanceObj, object, true);
|
|
}
|
|
else if (eventBinding)
|
|
{
|
|
// Unbind from the event
|
|
if (eventBinding->BindedMethods.Count() == 1)
|
|
(*eventBinder)(instanceObj, object, false);
|
|
eventBinding->BindedMethods.Remove(method);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Call graph further
|
|
const auto returnedImpulse = &node->Boxes[2];
|
|
if (returnedImpulse && returnedImpulse->HasConnection())
|
|
eatBox(node, returnedImpulse->FirstConnection());
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void VisualScriptExecutor::ProcessGroupFlow(Box* boxBase, Node* node, Value& value)
|
|
{
|
|
switch (node->TypeID)
|
|
{
|
|
// If
|
|