Files
FlaxEngine/Source/Engine/Content/Assets/VisualScript.cpp
T

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(&parameterNameLength);
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(&paramValue);
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