mirror of
https://github.com/aharabada/glitchy-engine-beef.git
synced 2026-09-05 21:01:52 +00:00
Basic Script reloading
+ ScriptComponent now saves script name instead of direct ScriptClass reference + Added ClearDictionaryAndDeleteKeys helper + Moved ScriptFieldType to separate file + _ScriptFieldMap now uses String as Key
This commit is contained in:
@@ -60,5 +60,15 @@ namespace GlitchyEngine
|
||||
delete container;
|
||||
}
|
||||
}
|
||||
|
||||
public static mixin ClearDictionaryAndDeleteKeys(var dictionary)
|
||||
{
|
||||
if (dictionary != null)
|
||||
{
|
||||
for (var value in dictionary)
|
||||
delete value.key;
|
||||
delete dictionary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,13 @@ public struct ScriptField
|
||||
|
||||
public struct ScriptFieldInstance
|
||||
{
|
||||
public ScriptField Field;
|
||||
public ScriptFieldType Type;
|
||||
// TODO: I hate this, but we need to be able to store an entire matrix
|
||||
internal uint8[sizeof(GlitchyEngine.Math.Matrix)] _data;
|
||||
|
||||
public this(ScriptField field)
|
||||
public this(ScriptFieldType type)
|
||||
{
|
||||
Field = field;
|
||||
Type = type;
|
||||
_data = default;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public struct ScriptFieldInstance
|
||||
}
|
||||
}
|
||||
|
||||
public typealias ScriptFieldMap = Dictionary<StringView, ScriptFieldInstance>;
|
||||
public typealias ScriptFieldMap = Dictionary<String, ScriptFieldInstance>;
|
||||
|
||||
abstract class SharpType : RefCounter
|
||||
{
|
||||
|
||||
@@ -2,100 +2,14 @@ using Mono;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections;
|
||||
using GlitchyEngine.Core;
|
||||
using GlitchyEngine.Math;
|
||||
using GlitchyEngine.World;
|
||||
using GlitchyEngine.Core;
|
||||
|
||||
namespace GlitchyEngine.Scripting;
|
||||
|
||||
using internal GlitchyEngine.Scripting;
|
||||
|
||||
enum ScriptFieldType
|
||||
{
|
||||
None,
|
||||
|
||||
Class,
|
||||
Enum,
|
||||
Struct,
|
||||
|
||||
// TODO!
|
||||
Bool,
|
||||
|
||||
SByte,
|
||||
Short,
|
||||
Int, Int2, Int3, Int4,
|
||||
Long,
|
||||
Byte,
|
||||
UShort,
|
||||
UInt, // UInt2, UInt3, UInt4,
|
||||
ULong,
|
||||
// Half, Half2, Half3, Half4,
|
||||
Float, float2, float3, float4,
|
||||
Double, Double2, Double3, Double4,
|
||||
|
||||
Entity
|
||||
}
|
||||
|
||||
extension ScriptFieldType
|
||||
{
|
||||
public Type GetBeefType()
|
||||
{
|
||||
switch(this)
|
||||
{
|
||||
case .Bool:
|
||||
return typeof(bool);
|
||||
|
||||
case .SByte:
|
||||
return typeof(int8);
|
||||
case .Short:
|
||||
return typeof(int16);
|
||||
case .Int:
|
||||
return typeof(int32);
|
||||
case .Int2:
|
||||
return typeof(int2);
|
||||
case .Int3:
|
||||
return typeof(int3);
|
||||
case .Int4:
|
||||
return typeof(int4);
|
||||
case .Long:
|
||||
return typeof(int64);
|
||||
|
||||
case .Byte:
|
||||
return typeof(uint8);
|
||||
case .UShort:
|
||||
return typeof(uint16);
|
||||
case .UInt:
|
||||
return typeof(uint32);
|
||||
case .ULong:
|
||||
return typeof(uint64);
|
||||
|
||||
case .Float:
|
||||
return typeof(float);
|
||||
case .float2:
|
||||
return typeof(float2);
|
||||
case .float3:
|
||||
return typeof(float3);
|
||||
case .float4:
|
||||
return typeof(float4);
|
||||
|
||||
case .Double:
|
||||
return typeof(double);
|
||||
case .Double2:
|
||||
return typeof(double2);
|
||||
case .Double3:
|
||||
return typeof(double3);
|
||||
case .Double4:
|
||||
return typeof(double4);
|
||||
|
||||
case .Entity:
|
||||
return typeof(UUID);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static sealed class ScriptEngineHelper
|
||||
{
|
||||
private static Dictionary<StringView, ScriptFieldType> _scriptFieldTypes = new Dictionary<StringView, ScriptFieldType>()
|
||||
@@ -163,13 +77,21 @@ static class ScriptEngine
|
||||
|
||||
public static Scene Context => s_Context;
|
||||
|
||||
private static Dictionary<UUID, ScriptFieldMap> _entityFields = new .() ~ DeleteDictionaryAndValues!(_);
|
||||
private static Dictionary<UUID, ScriptFieldMap> _entityFields = new .() ~
|
||||
{
|
||||
for (var value in _entityFields.Values)
|
||||
{
|
||||
DeleteDictionaryAndKeys!(value);
|
||||
}
|
||||
|
||||
delete _;
|
||||
};
|
||||
|
||||
internal static class Attributes
|
||||
{
|
||||
internal static MonoClass* s_ShowInEditorAttribute;
|
||||
}
|
||||
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
Mono.mono_set_assemblies_path("mono/lib");
|
||||
@@ -179,15 +101,18 @@ static class ScriptEngine
|
||||
|
||||
ScriptGlue.Init();
|
||||
|
||||
LoadScriptAssemblies();
|
||||
}
|
||||
|
||||
static void LoadScriptAssemblies()
|
||||
{
|
||||
CreateAppDomain("GlitchyEngineScriptRuntime");
|
||||
(s_CoreAssembly, s_CoreAssemblyImage) = LoadAssembly("resources/scripts/ScriptCore.dll");
|
||||
(s_AppAssembly, s_AppAssemblyImage) = LoadAssembly("SandboxProject/Assets/Scripts/bin/Sandbox.dll");
|
||||
|
||||
|
||||
GetEntitiesFromAssemblies();
|
||||
|
||||
ScriptGlue.RegisterManagedComponents();
|
||||
|
||||
//Samples();
|
||||
}
|
||||
|
||||
public static void SetContext(Scene scene)
|
||||
@@ -206,26 +131,38 @@ static class ScriptEngine
|
||||
SetContext(null);
|
||||
}
|
||||
|
||||
public static void InitializeInstance(Entity entity, ScriptComponent* script)
|
||||
public static bool InitializeInstance(Entity entity, ScriptComponent* script)
|
||||
{
|
||||
ScriptClass scriptClass = GetScriptClass(script.ScriptClassName);
|
||||
|
||||
if (scriptClass == null)
|
||||
return false;
|
||||
|
||||
script.Instance = new ScriptInstance(scriptClass);
|
||||
script.Instance..ReleaseRef();
|
||||
|
||||
_entityScriptInstances[entity.UUID] = script.Instance..AddRef();
|
||||
|
||||
script.Instance.Instantiate(entity.UUID);
|
||||
|
||||
CopyEditorFieldsToInstance(entity, script);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void CopyEditorFieldsToInstance(Entity entity, ScriptComponent* script)
|
||||
{
|
||||
// Technically the map is for a different entity (namely the editor-entity),
|
||||
// however the UUID is the same, so we get the correct field map
|
||||
let fiels = GetScriptFieldMap(entity);
|
||||
let fields = GetScriptFieldMap(entity);
|
||||
|
||||
for (var (fieldName, field) in fiels)
|
||||
for (var (fieldName, field) in fields)
|
||||
{
|
||||
// TODO: a litte assertion maybe?
|
||||
|
||||
script.Instance.SetFieldValue(field.Field, field._data);
|
||||
ScriptField scriptField = script.Instance.ScriptClass.Fields[fieldName];
|
||||
|
||||
script.Instance.SetFieldValue(scriptField, field._data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,14 +214,23 @@ static class ScriptEngine
|
||||
entry.value.ReleaseRef();
|
||||
}
|
||||
_entityScripts.Clear();
|
||||
|
||||
if (s_EngineObject == null)
|
||||
{
|
||||
s_EngineObject = new ScriptClass("GlitchyEngine.Core", "EngineObject", s_CoreAssemblyImage);
|
||||
s_EntityRoot = new ScriptClass("GlitchyEngine", "Entity", s_CoreAssemblyImage);
|
||||
|
||||
Log.EngineLogger.Assert(s_EntityRoot != null);
|
||||
for (var sharpClass in _sharpClasses)
|
||||
{
|
||||
sharpClass.value.ReleaseRef();
|
||||
}
|
||||
_sharpClasses.Clear();
|
||||
|
||||
if (s_EngineObject != null)
|
||||
{
|
||||
s_EngineObject.ReleaseRef();
|
||||
s_EntityRoot.ReleaseRef();
|
||||
}
|
||||
|
||||
s_EngineObject = new ScriptClass("GlitchyEngine.Core", "EngineObject", s_CoreAssemblyImage);
|
||||
s_EntityRoot = new ScriptClass("GlitchyEngine", "Entity", s_CoreAssemblyImage);
|
||||
|
||||
Log.EngineLogger.Assert(s_EntityRoot != null);
|
||||
|
||||
Attributes.s_ShowInEditorAttribute = Mono.mono_class_from_name(s_CoreAssemblyImage, "GlitchyEngine.Editor", "ShowInEditorAttribute");
|
||||
|
||||
@@ -311,13 +257,28 @@ static class ScriptEngine
|
||||
}
|
||||
}
|
||||
|
||||
public static void ReloadAssemblies()
|
||||
{
|
||||
Mono.mono_domain_set(s_RootDomain, false);
|
||||
|
||||
Mono.mono_domain_unload(s_AppDomain);
|
||||
|
||||
LoadScriptAssemblies();
|
||||
|
||||
// TODO: ScriptFields might get added
|
||||
// TODO: ScriptField Types may change after reload!
|
||||
// TODO: Scripts may be renamed (probably not detectable (trivially))
|
||||
|
||||
// TODO: Reload in play mode
|
||||
// Only scripts that were changed should actually be reinstatiated
|
||||
}
|
||||
|
||||
public static void Shutdown()
|
||||
{
|
||||
/*Mono.mono_assembly_close(s_CoreAssembly);
|
||||
s_CoreAssembly = null;
|
||||
|
||||
Mono.mono_domain_set(s_RootDomain, false);
|
||||
|
||||
Mono.mono_domain_unload(s_AppDomain);
|
||||
s_AppDomain = null;*/
|
||||
s_AppDomain = null;
|
||||
|
||||
Mono.mono_jit_cleanup(s_RootDomain);
|
||||
s_RootDomain = null;
|
||||
@@ -375,19 +336,23 @@ static class ScriptEngine
|
||||
|
||||
if (_entityFields.TryGetValue(entity.UUID, var entityFields))
|
||||
{
|
||||
entityFields.Clear();
|
||||
ClearDictionaryAndDeleteKeys!(entityFields);
|
||||
}
|
||||
else
|
||||
{
|
||||
entityFields = new Dictionary<StringView, ScriptFieldInstance>();
|
||||
entityFields = new ScriptFieldMap();
|
||||
_entityFields.Add(entity.UUID, entityFields);
|
||||
}
|
||||
|
||||
let scriptComponent = entity.GetComponent<ScriptComponent>();
|
||||
|
||||
for (let (fieldName, field) in scriptComponent.ScriptClass.Fields)
|
||||
ScriptClass scriptClass = GetScriptClass(scriptComponent.ScriptClassName);
|
||||
|
||||
Log.EngineLogger.AssertDebug(scriptClass != null);
|
||||
|
||||
for (let (fieldName, field) in scriptClass.Fields)
|
||||
{
|
||||
entityFields.Add(fieldName, ScriptFieldInstance(field));
|
||||
entityFields.Add(new String(fieldName), ScriptFieldInstance(field.FieldType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,6 +376,12 @@ static class ScriptEngine
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ScriptClass GetScriptClass(StringView name)
|
||||
{
|
||||
EntityClasses.TryGetValue(name, let scriptClass);
|
||||
|
||||
return scriptClass;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using GlitchyEngine.Core;
|
||||
using GlitchyEngine.Math;
|
||||
|
||||
namespace GlitchyEngine.Scripting;
|
||||
|
||||
enum ScriptFieldType
|
||||
{
|
||||
case None;
|
||||
|
||||
case Class;
|
||||
case Enum;
|
||||
case Struct;
|
||||
|
||||
case Bool;
|
||||
|
||||
case SByte;
|
||||
case Short;
|
||||
case Int, Int2, Int3, Int4;
|
||||
case Long;
|
||||
case Byte;
|
||||
case UShort;
|
||||
case UInt; // UInt2, UInt3, UInt4;
|
||||
case ULong;
|
||||
|
||||
case Float, float2, float3, float4;
|
||||
case Double, Double2, Double3, Double4;
|
||||
|
||||
case Entity;
|
||||
|
||||
public Type GetBeefType()
|
||||
{
|
||||
switch(this)
|
||||
{
|
||||
case .Bool:
|
||||
return typeof(bool);
|
||||
|
||||
case .SByte:
|
||||
return typeof(int8);
|
||||
case .Short:
|
||||
return typeof(int16);
|
||||
case .Int:
|
||||
return typeof(int32);
|
||||
case .Int2:
|
||||
return typeof(int2);
|
||||
case .Int3:
|
||||
return typeof(int3);
|
||||
case .Int4:
|
||||
return typeof(int4);
|
||||
case .Long:
|
||||
return typeof(int64);
|
||||
|
||||
case .Byte:
|
||||
return typeof(uint8);
|
||||
case .UShort:
|
||||
return typeof(uint16);
|
||||
case .UInt:
|
||||
return typeof(uint32);
|
||||
case .ULong:
|
||||
return typeof(uint64);
|
||||
|
||||
case .Float:
|
||||
return typeof(float);
|
||||
case .float2:
|
||||
return typeof(float2);
|
||||
case .float3:
|
||||
return typeof(float3);
|
||||
case .float4:
|
||||
return typeof(float4);
|
||||
|
||||
case .Double:
|
||||
return typeof(double);
|
||||
case .Double2:
|
||||
return typeof(double2);
|
||||
case .Double3:
|
||||
return typeof(double3);
|
||||
case .Double4:
|
||||
return typeof(double4);
|
||||
|
||||
case .Entity:
|
||||
return typeof(UUID);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,10 @@ static class ScriptGlue
|
||||
|
||||
public static void RegisterManagedComponents()
|
||||
{
|
||||
s_AddComponentMethods.Clear();
|
||||
s_HasComponentMethods.Clear();
|
||||
s_RemoveComponentMethods.Clear();
|
||||
|
||||
RegisterComponent<TransformComponent>("GlitchyEngine.Transform");
|
||||
RegisterComponent<Rigidbody2DComponent>("GlitchyEngine.RigidBody2D");
|
||||
}
|
||||
|
||||
@@ -507,14 +507,20 @@ namespace GlitchyEngine.World
|
||||
|
||||
struct ScriptComponent : IDisposableComponent
|
||||
{
|
||||
private ScriptClass _scriptClass = null;
|
||||
private String _scriptClassName = null;
|
||||
|
||||
private ScriptInstance _instance = null;
|
||||
|
||||
public ScriptClass ScriptClass
|
||||
public StringView ScriptClassName
|
||||
{
|
||||
get => _scriptClass;
|
||||
set mut => SetReference!(_scriptClass, value);
|
||||
get => _scriptClassName;
|
||||
set mut
|
||||
{
|
||||
if (_scriptClassName == null)
|
||||
_scriptClassName = new String(value);
|
||||
|
||||
_scriptClassName.Set(value);
|
||||
}
|
||||
}
|
||||
|
||||
public ScriptInstance Instance
|
||||
@@ -525,16 +531,13 @@ namespace GlitchyEngine.World
|
||||
set mut => SetReference!(_instance, value);
|
||||
}
|
||||
|
||||
/// Gets whether or not this script component has a script assigned (that means ScriptClass isn't null).
|
||||
public bool HasScript => _scriptClass != null;
|
||||
|
||||
public bool IsInitialized => _instance?.IsInitialized ?? false;
|
||||
|
||||
public bool IsCreated => _instance?.IsCreated ?? false;
|
||||
|
||||
public void Dispose() mut
|
||||
{
|
||||
ReleaseRefAndNullify!(_scriptClass);
|
||||
delete _scriptClassName;
|
||||
ReleaseRefAndNullify!(_instance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,11 +93,10 @@ namespace GlitchyEngine.World
|
||||
Entity targetEntity = target.GetEntityByID(sourceEntity.UUID);
|
||||
ScriptComponent* targetComponent = targetEntity.AddComponent<ScriptComponent>();
|
||||
|
||||
targetComponent.ScriptClass = sourceComponent.ScriptClass;
|
||||
targetComponent.Instance = new ScriptInstance(sourceComponent.ScriptClass);
|
||||
targetComponent.Instance..ReleaseRef();
|
||||
targetComponent.ScriptClassName = sourceComponent.ScriptClassName;
|
||||
|
||||
// Initializes the created instance
|
||||
// TODO: this returns false, if no script with ScriptClassName exists, we have to handle this case correctly I think.
|
||||
ScriptEngine.InitializeInstance(targetEntity, targetComponent);
|
||||
}
|
||||
|
||||
|
||||
@@ -189,9 +189,11 @@ class SceneSerializer
|
||||
|
||||
SerializeComponent<ScriptComponent>(writer, entity, "ScriptComponent", scope (component) =>
|
||||
{
|
||||
Serialize.Value(writer, "ScriptClass", component.ScriptClass?.FullName);
|
||||
Serialize.Value(writer, "ScriptClass", component.ScriptClassName);
|
||||
|
||||
if (component.HasScript)
|
||||
//if (component.HasScript)
|
||||
// TODO: Thats not a good check, I think. At least we know the script class is valid
|
||||
if (ScriptEngine.GetScriptClass(component.ScriptClassName) != null)
|
||||
{
|
||||
let fields = ScriptEngine.GetScriptFieldMap(entity);
|
||||
|
||||
@@ -201,11 +203,10 @@ class SceneSerializer
|
||||
{
|
||||
for (var (fieldName, fieldInstance) in fields)
|
||||
{
|
||||
if (fieldInstance.Field.FieldType == .None)
|
||||
if (fieldInstance.Type == .None)
|
||||
continue;
|
||||
|
||||
|
||||
switch (fieldInstance.Field.FieldType)
|
||||
switch (fieldInstance.Type)
|
||||
{
|
||||
case .Enum, .Class, .Struct:
|
||||
// TODO: implement
|
||||
@@ -213,10 +214,10 @@ class SceneSerializer
|
||||
writer.Identifier(fieldName);
|
||||
|
||||
String str = scope .();
|
||||
fieldInstance.Field.FieldType.ToString(str);
|
||||
fieldInstance.Type.ToString(str);
|
||||
writer.Type(str);
|
||||
|
||||
Serialize.Value(writer, ValueView(fieldInstance.Field.FieldType.GetBeefType(), &fieldInstance.[Friend]_data), gBonEnv);
|
||||
Serialize.Value(writer, ValueView(fieldInstance.Type.GetBeefType(), &fieldInstance.[Friend]_data), gBonEnv);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -558,15 +559,18 @@ class SceneSerializer
|
||||
|
||||
if (scriptClassName != null)
|
||||
{
|
||||
if (ScriptEngine.EntityClasses.TryGetValue(scriptClassName, let scriptClass))
|
||||
{
|
||||
//if (ScriptEngine.EntityClasses.TryGetValue(scriptClassName, let scriptClass))
|
||||
/*{
|
||||
component.ScriptClass = scriptClass;
|
||||
}
|
||||
}*/
|
||||
|
||||
component.ScriptClassName = scriptClassName;
|
||||
|
||||
delete scriptClassName;
|
||||
}
|
||||
|
||||
if (component.HasScript)
|
||||
//if (component.HasScript)
|
||||
if (ScriptEngine.GetScriptClass(component.ScriptClassName) != null)
|
||||
{
|
||||
// This whole operation is technically a bit junk, because we are not guaranteed to successfully deserialize the scene,
|
||||
// however we are editing the ScriptEngine because it doesn't care about which scene is active right now.
|
||||
@@ -606,15 +610,18 @@ class SceneSerializer
|
||||
|
||||
StringView fieldName = Try!(reader.Identifier());
|
||||
|
||||
if (fields.ContainsKey(fieldName))
|
||||
// Allocate a string on the stack, because the dictionary uses a string as key
|
||||
String fieldNameString = scope .(fieldName);
|
||||
|
||||
if (fields.ContainsKey(fieldNameString))
|
||||
{
|
||||
var field = ref fields[fieldName];
|
||||
var field = ref fields[fieldNameString];
|
||||
|
||||
Result<StringView> fieldTypeName = reader.Type();
|
||||
|
||||
if (fieldTypeName case .Err)
|
||||
{
|
||||
Log.EngineLogger.Error($"Failed to read field type for field \"{fieldName}\" in script \"{component.ScriptClass.FullName}\" of entity {entity.UUID} (\"{entity.Name}\")");
|
||||
Log.EngineLogger.Error($"Failed to read field type for field \"{fieldName}\" in script \"{component.ScriptClassName}\" of entity {entity.UUID} (\"{entity.Name}\")");
|
||||
reader.FileEntrySkip(1);
|
||||
dontRemoveComma = true;
|
||||
continue;
|
||||
@@ -622,9 +629,9 @@ class SceneSerializer
|
||||
|
||||
Result<ScriptFieldType> fieldType = Enum.Parse<ScriptFieldType>(fieldTypeName, true);
|
||||
|
||||
if ((fieldType case .Err) || (fieldType != field.Field.FieldType))
|
||||
if ((fieldType case .Err) || (fieldType != field.Type))
|
||||
{
|
||||
Log.EngineLogger.Error($"Unexpected field type (\"{fieldTypeName}\" instead of \"{field.Field.FieldType}\" for field: \"{fieldName}\" in script \"{component.ScriptClass.FullName}\" of entity {entity.UUID} (\"{entity.Name}\")");
|
||||
Log.EngineLogger.Error($"Unexpected field type (\"{fieldTypeName}\" instead of \"{field.Type}\" for field: \"{fieldName}\" in script \"{component.ScriptClassName}\" of entity {entity.UUID} (\"{entity.Name}\")");
|
||||
reader.FileEntrySkip(1);
|
||||
dontRemoveComma = true;
|
||||
continue;
|
||||
@@ -632,9 +639,9 @@ class SceneSerializer
|
||||
|
||||
void* data = &field.[Friend]_data;
|
||||
|
||||
if (Deserialize.Value(reader, ValueView(field.Field.FieldType.GetBeefType(), data), gBonEnv) case .Err)
|
||||
if (Deserialize.Value(reader, ValueView(field.Type.GetBeefType(), data), gBonEnv) case .Err)
|
||||
{
|
||||
Log.EngineLogger.Error($"Failed to deserialize data for field: \"{fieldName}\" in script \"{component.ScriptClass.FullName}\" of entity {entity.UUID} (\"{entity.Name}\")");
|
||||
Log.EngineLogger.Error($"Failed to deserialize data for field: \"{fieldName}\" in script \"{component.ScriptClassName}\" of entity {entity.UUID} (\"{entity.Name}\")");
|
||||
reader.FileEntrySkip(1);
|
||||
dontRemoveComma = true;
|
||||
continue;
|
||||
@@ -642,7 +649,7 @@ class SceneSerializer
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.EngineLogger.Error($"Script \"{component.ScriptClass.FullName}\" doesn't have a field with name \"{fieldName}\". (Entity {entity.UUID} (\"{entity.Name}\"))");
|
||||
Log.EngineLogger.Error($"Script \"{component.ScriptClassName}\" doesn't have a field with name \"{fieldName}\". (Entity {entity.UUID} (\"{entity.Name}\"))");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user