diff --git a/GlitchyEditor/src/EditorLayer.bf b/GlitchyEditor/src/EditorLayer.bf index ecb7fb4..4eb6ea8 100644 --- a/GlitchyEditor/src/EditorLayer.bf +++ b/GlitchyEditor/src/EditorLayer.bf @@ -1023,7 +1023,7 @@ namespace GlitchyEditor } /// Sets the current editor scene - private void SetEditorScene(Scene scene) + private void SetEditorScene(Scene scene, Dictionary serializedObjects = null) { if (_editorScene != null) { @@ -1037,6 +1037,12 @@ namespace GlitchyEditor if (_editorScene != null) { _editorScene.StartRuntime(); + + if (serializedObjects != null) + { + // Reconstruct state + ScriptEngine.DeserializeScriptInstances(serializedObjects); + } } } @@ -1139,7 +1145,7 @@ namespace GlitchyEditor // Make sure we actually loaded something! if (result case .Ok) { - SetEditorScene(newScene); + SetEditorScene(newScene, serializer.SerializedObjects); String relativePath = scope .(); Path.GetRelativePath(filename, _currentProject.WorkspacePath, relativePath); diff --git a/GlitchyEngine/src/Extension/Bon/Serialize.bf b/GlitchyEngine/src/Extension/Bon/Serialize.bf index be89cf6..65cd734 100644 --- a/GlitchyEngine/src/Extension/Bon/Serialize.bf +++ b/GlitchyEngine/src/Extension/Bon/Serialize.bf @@ -33,5 +33,67 @@ namespace Bon.Integrated value = ?; return Deserialize.Value(reader, ValueView(typeof(T), &value), env); } + + public static bool IsBool(BonReader reader, BonEnvironment env = gBonEnv) + { + return reader.inStr.StartsWith(bool.TrueString, StringComparison.OrdinalIgnoreCase) || reader.inStr.StartsWith(bool.FalseString, StringComparison.OrdinalIgnoreCase); + } + + public enum NumberType + { + None, + Integer, + Float, + Double, + Decimal + } + + /*public static bool IsNumber(BonReader reader, out NumberType numberType) + { + numberType = .None; + + StringView tmpView = reader.inStr; + + if (tmpView.StartsWith('-') || tmpView.StartsWith('+')) + tmpView.RemoveFromStart(1); + + if (tmpView.StartsWith('NaN', StringComparison.OrdinalIgnoreCase) || tmpView.StartsWith('infinity', StringComparison.OrdinalIgnoreCase)) + { + numberType = .Double; + return true; + } + + bool isNumber = false; + + while (true) + { + if (tmpView[0].IsNumber) + { + isNumber = true; + tmpView.RemoveFromStart(1); + } + else if (tmpView.StartsWith('.')) + { + // We also don't allow floats like .5f + if (!isNumber) + return false; + + tmpView.RemoveFromStart(1); + + // Err on the side of caution and assume double for precision + numberType = .Double; + } + } + + + if (tmpView.StartsWith('f')) + numberType = .Float; + else if (tmpView.StartsWith('m')) + numberType = .Decimal; + else if (numberType != .Double) + numberType = .Integer; + + return isNumber; + }*/ } } diff --git a/GlitchyEngine/src/Scripting/MonoDecimal.bf b/GlitchyEngine/src/Scripting/MonoDecimal.bf new file mode 100644 index 0000000..8cdcf54 --- /dev/null +++ b/GlitchyEngine/src/Scripting/MonoDecimal.bf @@ -0,0 +1,151 @@ +using System; +using Bon.Integrated; +using Bon; +namespace GlitchyEngine.Scripting; + +/// A struct providing access to the components of a mono decimal. +struct MonoDecimal +{ + [Bitfield(.Private, .BitsAt(16, 0), "_dontUse", .Read)] + [Bitfield(.Public, .BitsAt(8, 16), "Exponent")] + [Bitfield(.Private, .BitsAt(7, 24), "_dontUse2")] + [Bitfield(.Public, .BitsAt(1, 31), "Sign")] + private uint32 _flags; + + private uint32 _high; + private uint32 _low; + private uint32 _mid; + + public uint32[3] Mantissa => .(_low, _mid, _high); + + static this + { + gBonEnv.typeHandlers.Add(typeof(MonoDecimal), + ((.)new => AssetSerialize, new => AssetDeserialize)); + } + + static void AssetSerialize(BonWriter writer, ValueView value, BonEnvironment environment, SerializeValueState state) + { + Log.EngineLogger.Assert(value.type == typeof(Self)); + + MonoDecimal decimal = value.Get(); + + decimal.ToString(writer.outStr); + } + + static Result AssetDeserialize(BonReader reader, ValueView value, BonEnvironment environment, DeserializeValueState state) + { + Log.EngineLogger.Assert(value.type == typeof(Self)); + + return .Ok; + } + + public override void ToString(String strBuffer) + { + uint64 d, r; + + uint32[3] a = .(_high, _mid, _low); + + char8[30] str = .(); + char8* ptr = &str[29]; + + int digits = 0; + + repeat + { + r = a [0]; + + d = r / 10; + r = ((r - d * 10) << 32) + a [1]; + a [0] = (uint32)d; + + d = r / 10; + r = ((r - d * 10) << 32) + a [2]; + a [1] = (uint32)d; + + d = r / 10; + r = r - d * 10; + a [2] = (uint32)d; + + *ptr = '0' + (uint8)r; + ptr--; + digits++; + if (digits == Exponent) + { + *ptr = '.'; + ptr--; + digits++; + } + } + while (a[0] > 0 || a[1] > 0 || a[2] > 0 || digits < (Exponent + 2)); + + if (Sign) + strBuffer.Append('-'); + + strBuffer.Append(StringView(&str[30 - digits], digits)); + strBuffer.Append('m'); + } + + public static Result Parse(StringView strBuffer) + { + var strBuffer; + + MonoDecimal result = .(); + + if (strBuffer.StartsWith('-')) + { + strBuffer.RemoveFromStart(1); + result.Sign = true; + } + + if (strBuffer.EndsWith('m')) + { + strBuffer.RemoveFromEnd(1); + } + + int decimalPointIndex = strBuffer.IndexOf('.'); + + if (decimalPointIndex != -1) + { + int exponent = strBuffer.Length - decimalPointIndex - 1; + + if (exponent < 0 || exponent > 28) + return .Err; + + result.Exponent = (uint8)exponent; + + if (strBuffer.IndexOf('.', decimalPointIndex + 1) != -1) + return .Err; + } + + repeat + { + if (strBuffer[0] != '.') + { + int32 digit = strBuffer[0] - '0'; + + if (digit < 0 || digit > 10) + return .Err; + + uint64 tmp, carry; + + tmp = result._low * 10; + carry = (tmp + (uint64)digit) >> 32; + result._low = (uint32)tmp + (uint32)digit; + + tmp = result._mid * 10 + carry; + carry = tmp >> 32; + result._mid = (uint32)tmp; + + tmp = result._high * 10 + carry; + carry = tmp >> 32; + result._high = (uint32)tmp; + } + + strBuffer.RemoveFromStart(1); + } + while (!strBuffer.IsEmpty); + + return .Ok(result); + } +} diff --git a/GlitchyEngine/src/Scripting/ScriptGlue.bf b/GlitchyEngine/src/Scripting/ScriptGlue.bf index e856c9f..cac1788 100644 --- a/GlitchyEngine/src/Scripting/ScriptGlue.bf +++ b/GlitchyEngine/src/Scripting/ScriptGlue.bf @@ -528,6 +528,27 @@ static class ScriptGlue id = UUID.Create(); } + [RegisterCall("ScriptGlue::Print_Decimal")] + static void Print_Decimal(MonoDecimal monoDecimal) + { + Log.ClientLogger.Info(scope $"{monoDecimal}"); + } + + [RegisterCall("ScriptGlue::Get_Decimal")] + static void Get_Decimal(MonoString* string, out MonoDecimal monoDecimal) + { + char8* str = Mono.mono_string_to_utf8(string); + + Result decimalo = MonoDecimal.Parse(StringView(str)); + + if (!(decimalo case .Ok(out monoDecimal))) + { + monoDecimal = default; + } + + Mono.mono_free(str); + } + #region Application [RegisterCall("ScriptGlue::Application_IsEditor")] @@ -590,7 +611,7 @@ static class ScriptGlue } [RegisterCall("ScriptGlue::Serialization_DeserializeField")] - public static void Serialization_DeserializeField(void* internalContext, SerializationType expectedType, MonoString* fieldName, uint8* target) + public static void Serialization_DeserializeField(void* internalContext, SerializationType expectedType, MonoString* fieldName, uint8* target, out SerializationType actualType) { SerializedObject context = Internal.UnsafeCastToObject(internalContext) as SerializedObject; @@ -598,7 +619,7 @@ static class ScriptGlue char8* name = Mono.mono_string_to_utf8(fieldName); - context.GetField(StringView(name), expectedType, target); + context.GetField(StringView(name), expectedType, target, out actualType); Mono.mono_free(name); } @@ -612,6 +633,8 @@ static class ScriptGlue objectContext = null; + Log.EngineLogger.AssertDebug(context.AllObjects.ContainsKey(context.Id)); + if (!context.AllObjects.TryGetValue(id, let foundObject)) return; diff --git a/GlitchyEngine/src/Serialization/SerializationType.bf b/GlitchyEngine/src/Serialization/SerializationType.bf index 1f7d9f3..66b7f18 100644 --- a/GlitchyEngine/src/Serialization/SerializationType.bf +++ b/GlitchyEngine/src/Serialization/SerializationType.bf @@ -1,36 +1,45 @@ using GlitchyEngine.Core; using System; +using Bon; namespace GlitchyEngine.Serialization; -public enum SerializationType : int32 +[BonTarget] +public enum SerializationType : uint32 { - case None; + case None = 0; - case Bool; - - case Char; - case String; - - case Int8; - case Int16; - case Int32; - case Int64; - case UInt8; - case UInt16; - case UInt32; - case UInt64; - - case Float; - case Double; - case Decimal; + case Bool = 1 << 31; - case Enum; + case TextTypes = 1 << 30; + + case Char = TextTypes | 1; + case String = TextTypes | 2; + + case Number = 1 << 29; + case Integer = Number | 1 << 28; + + case Int8 = Integer | 1; + case Int16 = Integer | 2; + case Int32 = Integer | 3; + case Int64 = Integer | 4; + case UInt8 = Integer | 5; + case UInt16 = Integer | 6; + case UInt32 = Integer | 7; + case UInt64 = Integer | 8; - case EntityReference; - case ComponentReference; + case FloatingPoint = Number | 1 << 27; + + case Float = FloatingPoint | 1; + case Double = FloatingPoint | 2; + case Decimal = FloatingPoint | 3; + + case Enum = 1 << 26; - case ObjectReference; + case EntityReference = 1 << 25; + case ComponentReference = 1 << 24; + + case ObjectReference = 1 << 23; public int GetSize() { @@ -39,7 +48,7 @@ public enum SerializationType : int32 case .Bool: return 1; case .Char: - return 1; + return 2; case .Int8, .UInt8: return 1; case .Int16, .UInt16: @@ -64,4 +73,16 @@ public enum SerializationType : int32 return 0; } } + + public bool IsNumber => this.HasFlag(.Number); + public bool IsInteger => this.HasFlag(.Integer); + public bool IsFloatpoint => this.HasFlag(.FloatingPoint); + + public bool CanConvertTo(SerializationType destinationType) + { + if (this.IsNumber && destinationType.IsNumber) + return true; + + return false; + } } diff --git a/GlitchyEngine/src/Serialization/SerializedObject.bf b/GlitchyEngine/src/Serialization/SerializedObject.bf index b5248cd..07f35e3 100644 --- a/GlitchyEngine/src/Serialization/SerializedObject.bf +++ b/GlitchyEngine/src/Serialization/SerializedObject.bf @@ -3,9 +3,13 @@ using GlitchyEngine.Scripting; using Mono; using System; using System.Collections; +using Bon.Integrated; +using Bon; +using System.Reflection; namespace GlitchyEngine.Serialization; +[BonTarget] class SerializedObject { [Union] @@ -61,6 +65,25 @@ class SerializedObject TypeName = typeNameCopy; } + private void SetDataSimple(SerializationType fieldType, void* rawValue, ref FieldData data) + { + Internal.MemCpy(&data.RawData, rawValue, fieldType.GetSize()); + } + + private void SetRawData(SerializationType fieldType, T rawValue, ref FieldData data) + { + #unwarn + Internal.MemCpy(&data.RawData, &rawValue, fieldType.GetSize()); + } + + private void AddField(StringView fieldName, SerializationType fieldType, FieldData data) + { + String nameCopy = new String(fieldName); + _ownedString.Add(nameCopy); + + Fields.Add(nameCopy, (fieldType, data)); + } + public void AddField(StringView name, SerializationType primitiveType, MonoObject* value, MonoString* fullTypeName) { FieldData data = .(); @@ -103,25 +126,60 @@ class SerializedObject default: void* rawValue = Mono.mono_object_unbox(value); - Internal.MemCpy(&data.RawData, rawValue, primitiveType.GetSize()); - - String nameCopy = new String(name); - _ownedString.Add(nameCopy); + SetDataSimple(primitiveType, rawValue, ref data); } - String nameCopy = new String(name); - _ownedString.Add(nameCopy); - - Fields.Add(nameCopy, (primitiveType, data)); + AddField(name, primitiveType, data); } - - public void GetField(StringView fieldName, SerializationType expectedType, uint8* target) + + /*private void ConvertType(SerializationType fieldType, in FieldData fieldData, SerializationType expectedType, uint8* target) { + #unwarn + void* rawData = &fieldData.RawData; + + void SimpleConversion() where TExpected: operator explicit TActual + { + TActual actualValue = *(TActual*)rawData; + *(TExpected*)target = (TExpected)actualValue; + } + + if (fieldType.IsNumber && expectedType.IsNumber) + { + Convert.ConvertTo(Variant.Create(fieldType.)); + } + + switch(fieldType) + { + case .Float: + if (expectedType == .Double) + SimpleConversion(); + case .Double: + if (expectedType == .Float) + SimpleConversion(); + default: + Log.EngineLogger.Error($"No field data conversion from {fieldType} to {expectedType}."); + return; + } + }*/ + + public void GetField(StringView fieldName, SerializationType expectedType, uint8* target, out SerializationType actualType) + { + actualType = .None; + if (!Fields.TryGetValue(fieldName, let field)) return; - if (expectedType != field.PrimitiveType) + actualType = field.PrimitiveType; + + /*if (expectedType != field.PrimitiveType) + { + if (field.PrimitiveType.CanConvertTo(expectedType)) + { + ConvertType(field.PrimitiveType, field.Data, expectedType, target); + } + return; + }*/ switch (field.PrimitiveType) { @@ -168,4 +226,396 @@ class SerializedObject if (exception != null) ScriptEngine.[Friend]HandleMonoException(exception, scriptInstance); } + + static this + { + gBonEnv.typeHandlers.Add(typeof(SerializedObject), + ((.)new => AssetSerialize, null)); + } + + static T GetFieldDataAs(FieldData fieldData) + { + return *(T*)&fieldData.RawData; + } + + static void AssetSerialize(BonWriter writer, ValueView value, BonEnvironment environment, SerializeValueState state) + { + Log.EngineLogger.Assert(value.type == typeof(Self)); + + SerializedObject object = value.Get(); + + writer.Type(object.TypeName); + + using (writer.ObjectBlock()) + { + Serialize.Value(writer, "ID", object.Id, environment); + + for (let (fieldName, field) in object.Fields) + { + writer.Identifier(fieldName); + switch (field.PrimitiveType) + { + case .Bool: + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + case .Char: + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + case .String: + if (field.Data.StringView.IsNull) + { + writer.Type("string"); + } + #unwarn + Serialize.Value(writer, ValueView(typeof(StringView), &field.Data.StringView), environment); + + case .Int8: + writer.Type("int8"); + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + case .Int16: + writer.Type("int16"); + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + case .Int32: + writer.Type("int32"); + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + case .Int64: + writer.Type("int64"); + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + case .UInt8: + writer.Type("uint8"); + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + case .UInt16: + writer.Type("uint16"); + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + case .UInt32: + writer.Type("uint32"); + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + case .UInt64: + writer.Type("uint64"); + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + + case .Float: + writer.Type("float"); + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + case .Double: + writer.Type("double"); + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + case .Decimal: + writer.Type("decimal"); + Serialize.Value(writer, GetFieldDataAs(field.Data), environment); + + case .Enum: + writer.Type("Enum"); + #unwarn + Serialize.Value(writer, ValueView(typeof(StringView), &field.Data.StringView), environment); + case .EntityReference: + writer.Type("Entity"); + if (field.Data.EngineObject.FullTypeName != null) + writer.Type(field.Data.EngineObject.FullTypeName); + Serialize.Value(writer, field.Data.EngineObject.ID, environment); + case .ComponentReference: + writer.Type("Component"); + if (field.Data.EngineObject.FullTypeName != null) + writer.Type(field.Data.EngineObject.FullTypeName); + Serialize.Value(writer, field.Data.EngineObject.ID, environment); + case .ObjectReference: + writer.outStr.Append('&'); + GetFieldDataAs(field.Data).ToString(writer.outStr); + writer.EntryEnd(); + default: + Log.EngineLogger.Warning("Unhandled primitive type"); + } + } + } + + + + //AssetHandle.[Friend]AssetSerialize(writer, ValueView(typeof(AssetHandle), &handle), environment, state); + + //writer.String(identifier); + } + + private static Result NestedIdentifier(BonReader reader) + { + let name = ParseNestedName(reader); + if (name.Length == 0) + reader.[Friend]Error!("Expected identifier name"); + + Try!(reader.ConsumeEmpty()); + + if (!reader.[Friend]Check('=')) + reader.[Friend]Error!("Expected equals"); + + Try!(reader.ConsumeEmpty()); + + return name; + } + + private static StringView ParseNestedName(BonReader reader) + { + var nameLen = 0; + for (; nameLen < reader.inStr.Length; nameLen++) + { + let char = reader.inStr[nameLen]; + if (!char.IsLetterOrDigit && char != '_' && char != '.') + break; + } + + let name = reader.inStr.Substring(0, nameLen); + reader.inStr.RemoveFromStart(nameLen); + return name; + } + + public static Result BonDeserialize(BonReader reader, Dictionary allObjects, BonEnvironment environment = gBonEnv) + { + StringView type = Try!(reader.Type()); + + Try!(reader.ObjectBlock()); + + Try!(Deserialize.Value(reader, "ID", let objectId, environment)); + + SerializedObject object = new SerializedObject(allObjects, type, objectId); + + while (reader.ObjectHasMore()) + { + // TODO: Consume until we hit a comma. In case the current field couldn't be deserialized + Try!(reader.EntryEnd()); + + FieldData fieldData = .(); + SerializationType fieldType = .None; + + StringView fieldName = Try!(NestedIdentifier(reader)); + + StringView fieldTypeName = String.Empty; + + if (reader.IsTyped()) + { + fieldTypeName = reader.Type(); + } + + HandleField: do + { + if (!fieldTypeName.IsWhiteSpace) + { + if (fieldTypeName == "float") + { + float floatValue = 0.0f; + + Deserialize.[Friend]Float!(typeof(float), reader, ValueView(typeof(float), &floatValue)); + fieldType = .Float; + object.SetRawData(fieldType, floatValue, ref fieldData); + + break HandleField; + } + else if (fieldTypeName == "double") + { + double doubleValue = 0.0f; + + Deserialize.[Friend]Float!(typeof(double), reader, ValueView(typeof(double), &doubleValue)); + fieldType = .Double; + object.SetRawData(fieldType, doubleValue, ref fieldData); + + break HandleField; + } + else if (fieldTypeName.StartsWith("int")) + { + int64 intValue = 0; + + Deserialize.[Friend]Integer!(typeof(int64), reader, ValueView(typeof(int64), &intValue)); + + if (fieldTypeName.EndsWith("8")) + { + fieldType = .Int8; + object.SetRawData(fieldType, (int8)intValue, ref fieldData); + } + else if (fieldTypeName.EndsWith("16")) + { + fieldType = .Int16; + object.SetRawData(fieldType, (int16)intValue, ref fieldData); + } + else if (fieldTypeName.EndsWith("32")) + { + fieldType = .Int32; + object.SetRawData(fieldType, (int32)intValue, ref fieldData); + } + else if (fieldTypeName.EndsWith("64")) + { + fieldType = .Int64; + object.SetRawData(fieldType, (int64)intValue, ref fieldData); + } + else + { + Log.ClientLogger.Error($"Unknown integer type {fieldTypeName}"); + } + + break HandleField; + } + else if (fieldTypeName.StartsWith("uint")) + { + uint64 intValue = 0; + + Deserialize.[Friend]Integer!(typeof(uint64), reader, ValueView(typeof(uint64), &intValue)); + + if (fieldTypeName.EndsWith("8")) + { + fieldType = .UInt8; + object.SetRawData(fieldType, (uint8)intValue, ref fieldData); + } + else if (fieldTypeName.EndsWith("16")) + { + fieldType = .UInt16; + object.SetRawData(fieldType, (uint16)intValue, ref fieldData); + } + else if (fieldTypeName.EndsWith("32")) + { + fieldType = .UInt32; + object.SetRawData(fieldType, (uint32)intValue, ref fieldData); + } + else if (fieldTypeName.EndsWith("64")) + { + fieldType = .UInt64; + object.SetRawData(fieldType, (uint64)intValue, ref fieldData); + } + else + { + Log.ClientLogger.Error($"Unknown integer type {fieldTypeName}"); + } + + break HandleField; + } + else if (fieldTypeName == "decimal") + { + StringView numberView = Try!(reader.Floating()); + + if (reader.inStr.StartsWith('m')) + reader.inStr.RemoveFromStart(1); + + MonoDecimal decimalValue = Try!(MonoDecimal.Parse(numberView)); + fieldType = .Decimal; + object.SetRawData(fieldType, decimalValue, ref fieldData); + + break HandleField; + } + else if (fieldTypeName == "Enum") + { + String enumValue = new .(); + Deserialize.String!(reader, ref enumValue, environment); + + object._ownedString.Add(enumValue); + fieldData.StringView = enumValue; + fieldType = .Enum; + + break HandleField; + } + else if (fieldTypeName == "Entity" || fieldTypeName == "Component") + { + String entityTypeName = null; + + if (reader.IsTyped()) + { + StringView entityTypeNameView = Try!(reader.Type()); + + entityTypeName = new String(entityTypeNameView); + + object._ownedString.Add(entityTypeName); + } + + // Object Reference + uint64 id = 0; + + Deserialize.[Friend]Integer!(typeof(uint64), reader, ValueView(typeof(uint64), &id)); + + UUID reference = UUID(id); + fieldType = (fieldTypeName == "Entity") ? .EntityReference : .ComponentReference; + fieldData.EngineObject = (FullTypeName: entityTypeName, ID: reference); + + break HandleField; + } + } + + if (Deserialize.IsBool(reader)) + { + bool boolValue = Try!(reader.Bool()); + fieldType = .Bool; + object.SetDataSimple(fieldType, &boolValue, ref fieldData); + } + else if (reader.[Friend]Check('\'', false)) + { + char32 c = Try!(reader.Char()); + + fieldType = .Char; + object.SetDataSimple(fieldType, &c, ref fieldData); + } + else if (reader.[Friend]Check('"', false) || fieldTypeName == "string") + { + if (reader.inStr.StartsWith("null")) + { + fieldData.StringView = null; + fieldType = .String; + reader.inStr.RemoveFromStart(4); + } + else + { + String target = new .(); + Deserialize.String!(reader, ref target, environment); + + object._ownedString.Add(target); + fieldData.StringView = target; + fieldType = .String; + } + } + // else if (Deserialize.IsNumber(reader, let numberType)) + // { + // if (numberType == .Double) + // { + // double double = Try!(Deserialize.ParseFloat()); + // fieldType = .Double; + // object.SetDataSimple(fieldType, &reference, ref fieldData); + // } + // else if (numberType == .Float) + // { + // float double = Try!(Deserialize.ParseFloat()); + // fieldType = .Float; + // object.SetDataSimple(fieldType, &reference, ref fieldData); + // } + // else if (numberType == .Integer) + // { + // bool isNegative = false; + + // if (reader.inStr.StartsWith('-')) + // { + // isNegative = true; + // } + + // float double = Try!(Deserialize.ParseInt()); + // fieldType = .Float; + // object.SetDataSimple(fieldType, &reference, ref fieldData); + // } + // } + else if (reader.IsReference()) + { + // Object Reference + StringView referenceView = Try!(reader.Reference()); + + let referenceResult = uint64.Parse(referenceView); + + if (referenceResult case .Ok(let referenceInt)) + { + UUID reference = UUID(referenceInt); + fieldType = .ObjectReference; + object.SetRawData(fieldType, reference, ref fieldData); + } + } + } + + if (fieldType != .None) + { + object.AddField(fieldName, fieldType, fieldData); + } + + Try!(reader.ConsumeEmpty()); + } + + Try!(reader.ObjectBlockEnd()); + + return .Ok(object); + } } diff --git a/GlitchyEngine/src/World/SceneSerializer.bf b/GlitchyEngine/src/World/SceneSerializer.bf index 35b4723..2bb87e0 100644 --- a/GlitchyEngine/src/World/SceneSerializer.bf +++ b/GlitchyEngine/src/World/SceneSerializer.bf @@ -9,6 +9,7 @@ using System.Collections; using GlitchyEngine.Renderer; using GlitchyEngine.Content; using GlitchyEngine.Scripting; +using GlitchyEngine.Serialization; namespace GlitchyEngine.World; @@ -26,6 +27,12 @@ class SceneSerializer // Maps from ID in the prefab file to the actual ID in the scene. private Dictionary _fileToSceneId; + private Dictionary _serializedObjects ~ DeleteDictionaryAndValues!(_); + + private HashSet _objectsNotWritten; + + public Dictionary SerializedObjects => _serializedObjects; + public this(Scene scene) { _scene = scene; @@ -35,6 +42,17 @@ class SceneSerializer { Debug.Profiler.ProfileResourceFunction!(); + _serializedObjects = new .(); + + ScriptEngine.SerializeScriptInstances(_serializedObjects); + + _objectsNotWritten = new .(_serializedObjects.Count); + + for (UUID key in _serializedObjects.Keys) + { + _objectsNotWritten.Add(key); + } + String buffer = scope String(); let writer = scope BonWriter(buffer, true); var length = Serialize.Start(writer); @@ -63,10 +81,24 @@ class SceneSerializer } writer.EntryEnd(); + + writer.Identifier("ReferencedObjects"); + + using (writer.ArrayBlock()) + { + for (UUID id in _objectsNotWritten) + { + Serialize.Value(writer, _serializedObjects[id]); + } + } + + writer.EntryEnd(); } - + Serialize.End(writer, length); + delete _objectsNotWritten; + String targetDirectory = Path.GetDirectoryPath(filePath, .. scope String()); Directory.CreateDirectory(targetDirectory); @@ -205,10 +237,19 @@ class SceneSerializer { Serialize.Value(writer, "ScriptClass", component.ScriptClassName); + if (_serializedObjects.TryGetValue(entity.UUID, let value)) + { + Serialize.Value(writer, "Fields", value); + + _objectsNotWritten.Remove(entity.UUID); + } + //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) - { + //if (ScriptEngine.GetScriptClass(component.ScriptClassName) != null) + //{ + //Serialize.Value(writer, "Fields", ); + // TODO: Serialize Script Instance! /*let fields = ScriptEngine.GetScriptFieldMap(entity); @@ -236,7 +277,7 @@ class SceneSerializer } } }*/ - } + //} }); } @@ -268,6 +309,8 @@ class SceneSerializer { Debug.Profiler.ProfileResourceFunction!(); + _serializedObjects = new .(); + _parentIdToChild = scope Dictionary(); _entitiesMissingParent = scope List<(Entity Entity, UUID ParentId)>(); _fileToSceneId = scope Dictionary(); @@ -308,6 +351,28 @@ class SceneSerializer Try!(reader.ArrayBlockEnd()); + Try!(reader.EntryEnd()); + + if (Try!(reader.Identifier()) != "ReferencedObjects") + return .Err; + + Try!(reader.ArrayBlock()); + + first = true; + while (reader.ArrayHasMore()) + { + if (!first) + { + Try!(reader.EntryEnd()); + } + + Try!(SerializedObject.BonDeserialize(reader, _serializedObjects, gBonEnv)); + + first = false; + } + + Try!(reader.ArrayBlockEnd()); + Try!(reader.ObjectBlockEnd()); Try!(Deserialize.End(reader)); @@ -605,112 +670,18 @@ class SceneSerializer if (scriptClassName != null) { - //if (ScriptEngine.EntityClasses.TryGetValue(scriptClassName, let scriptClass)) - /*{ - component.ScriptClass = scriptClass; - }*/ - component.ScriptClassName = scriptClassName; delete scriptClassName; } - //if (component.HasScript) - if (ScriptEngine.GetScriptClass(component.ScriptClassName) != null) + + if (reader.ObjectHasMore()) { - // 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. - // The UUID should be unique enough, however if they do overlap (e.g. loading the current scene or simply because we are unlucky) - // we will replace the fields of the active scene, even if deserialization fails... - // But that is a bug for me to rediscover in the distant future, so in case this bug occurred and it took ages for you to - // figure out what happened: You are welcome :) - - // TODO: We need a new way to Serialize/Deserialize these fields! - /*ScriptEngine.CreateScriptFieldMap(entity); - - var fields = ScriptEngine.GetScriptFieldMap(entity); - Try!(reader.EntryEnd()); - + if (Try!(reader.Identifier()) == "Fields") - { - Try!(reader.ArrayBlock()); - - bool dontRemoveComma = true; - - // Remove whitespace before the check - while (reader..ConsumeEmpty().ArrayHasMore()) - { - if (dontRemoveComma) - dontRemoveComma = false; - else - { - if (reader.[Friend]Check(',', false)) - reader.EntryEnd(); - else - reader..FileEntrySkip(1).ConsumeEmpty(); - } - - // TODO: We could think about doing the Try! a little smarter... - // However we always might just fail to deserialize, so it doesn't really matter. - // It does matter... in case of an error we should try to skip to the next entry. - - StringView fieldName = Try!(reader.Identifier()); - - // Allocate a string on the stack, because the dictionary uses a string as key - String fieldNameString = scope .(fieldName); - - Result fieldTypeName = reader.Type(); - - if (fieldTypeName case .Err) - { - 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; - } - - Result fieldType = Enum.Parse(fieldTypeName, true); - - if ((fieldType case .Err)) - { - Log.EngineLogger.Error($"Error deserializing field type (Raw string: \"{fieldTypeName}\" of field: \"{fieldName}\" in script \"{component.ScriptClassName}\" of entity {entity.UUID} (\"{entity.Name}\")"); - reader.FileEntrySkip(1); - dontRemoveComma = true; - continue; - } - - uint8[sizeof(Matrix)] data = .(); - - if (Deserialize.Value(reader, ValueView(fieldType.Value.GetBeefType(), &data), gBonEnv) case .Err) - { - 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; - } - - if (fields.ContainsKey(fieldNameString)) - { - var field = ref fields[fieldNameString]; - - // Make sure the type we deserialized actually is correct. - if (fieldType != field.Type) - { - Log.EngineLogger.Error($"Unexpected field type (\"{fieldTypeName}\" instead of \"{field.Type}\" for field: \"{fieldName}\" in script \"{component.ScriptClassName}\" of entity {entity.UUID} (\"{entity.Name}\")"); - continue; - } - - field.SetData(data); - } - else - { - Log.EngineLogger.Error($"Script \"{component.ScriptClassName}\" doesn't have a field with name \"{fieldName}\". (Entity {entity.UUID} (\"{entity.Name}\"))"); - } - } - - Try!(reader.ArrayBlockEnd()); - }*/ + SerializedObject.BonDeserialize(reader, _serializedObjects, gBonEnv); } return .Ok; diff --git a/ScriptCore/Entity.cs b/ScriptCore/Entity.cs index ced085e..a109fc3 100644 --- a/ScriptCore/Entity.cs +++ b/ScriptCore/Entity.cs @@ -51,6 +51,17 @@ public class Entity : EngineObject Create(name, components); } + protected void PrintDecimal(decimal value) + { + ScriptGlue.Print_Decimal(value); + } + + protected decimal GetDecimal(string value) + { + ScriptGlue.Get_Decimal(value, out var dec); + return dec; + } + private void Create(string name, Type[] components) { ScriptGlue.Entity_Create(this, name, components, out _uuid); diff --git a/ScriptCore/ScriptGlue.cs b/ScriptCore/ScriptGlue.cs index 0eb40e8..6da163c 100644 --- a/ScriptCore/ScriptGlue.cs +++ b/ScriptCore/ScriptGlue.cs @@ -121,6 +121,12 @@ internal static class ScriptGlue [MethodImpl(MethodImplOptions.InternalCall)] internal static extern float4 modf_float4(float4 x, out float4 integerPart); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Print_Decimal(decimal value); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Get_Decimal(string str, out decimal value); #endregion @@ -152,7 +158,7 @@ internal static class ScriptGlue internal static extern void Serialization_CreateObject(IntPtr currentContext, string fullTypeName, out IntPtr context, out UUID id); [MethodImpl(MethodImplOptions.InternalCall)] - public static extern unsafe void Serialization_DeserializeField(IntPtr internalContext, SerializationType expectedType, string fieldName, byte* value); + public static extern unsafe void Serialization_DeserializeField(IntPtr internalContext, SerializationType expectedType, string fieldName, byte* value, out SerializationType actualType); [MethodImpl(MethodImplOptions.InternalCall)] public static extern void Serialization_GetObject(IntPtr internalContext, UUID id, out IntPtr objectContext); diff --git a/ScriptCore/Serialization/DeserializationObject.cs b/ScriptCore/Serialization/DeserializationObject.cs index 552d20b..9913b43 100644 --- a/ScriptCore/Serialization/DeserializationObject.cs +++ b/ScriptCore/Serialization/DeserializationObject.cs @@ -38,7 +38,7 @@ public class DeserializationObject private object _instance; - private Dictionary _fullNameToType = new(); + private Dictionary _fullNameToType = new(); private delegate object? DeserializeMethod(DeserializationObject container, string fieldName, Type fieldType); @@ -47,7 +47,7 @@ public class DeserializationObject /// /// Gets the type that was originally stored in the container, or null, if the type doesn't exist. /// - public Type StoredType + public Type? StoredType { get { @@ -88,7 +88,7 @@ public class DeserializationObject } } - public Type FindType(string fullName) + public Type? FindType(string fullName) { foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies().Reverse()) { @@ -101,19 +101,19 @@ public class DeserializationObject return null; } - public Type GetTypeFromName(string fullName) + public Type? GetTypeFromName(string fullName) { - if (_fullNameToType.TryGetValue(fullName, out Type storedType)) + if (_fullNameToType.TryGetValue(fullName, out Type? storedType)) return storedType; - Type type = FindType(fullName); - + Type? type = FindType(fullName); + _fullNameToType.Add(fullName, type); return type; } - public DeserializationObject GetDeserializedObject(UUID id) + public DeserializationObject? GetDeserializedObject(UUID id) { DeserializationObject context; @@ -121,7 +121,12 @@ public class DeserializationObject return context; ScriptGlue.Serialization_GetObject(_internalContext, id, out IntPtr contextPtr); - + + if (contextPtr == IntPtr.Zero) + { + return null; + } + context = new DeserializationObject(contextPtr, id, DeserializedClasses); DeserializedClasses.Add(id, context); @@ -155,27 +160,42 @@ public class DeserializationObject [FieldOffset(0)] public EngineObjectReferenceHelper EngineObjectReference; + + [FieldOffset(0)] + public ulong UInt; + + [FieldOffset(0)] + public long Int; + + [FieldOffset(0)] + public UUID UUID; } - public T GetFieldValue(string fieldName, SerializationType serializationType) + public T? GetFieldValue(string fieldName, SerializationType expectedType) { - return (T)GetFieldValue(fieldName, serializationType); + return (T?)GetFieldValue(fieldName, expectedType); } - public unsafe object GetFieldValue(string fieldName, SerializationType serializationType) + public unsafe object? GetFieldValue(string fieldName, SerializationType expectedType) { string completeFieldName = $"{_structScopeName}{fieldName}"; // Decimal is the larges primitive we store so we use a decimal as stack allocated memory (because stackalloc doesn't seem to work :( //decimal backingFieldOnStack = 0.0m; - byte* rawData = stackalloc byte[sizeof(DataHelper)]; //byte* rawData = (byte*)&backingFieldOnStack; + + //byte* rawData = stackalloc byte[sizeof(DataHelper)]; + //ref DataHelper dataHelper = ref Unsafe.AsRef(rawData); - ref DataHelper dataHelper = ref Unsafe.AsRef(rawData); + DataHelper dataHelper = new(); + byte* rawData = (byte*)Unsafe.AsPointer(ref dataHelper); - ScriptGlue.Serialization_DeserializeField(_internalContext, serializationType, completeFieldName, rawData); + // //byte* rawData = stackalloc byte[sizeof(DataHelper)]; + // //ref DataHelper dataHelper = ref Unsafe.AsRef(rawData); - string GetString() + ScriptGlue.Serialization_DeserializeField(_internalContext, expectedType, completeFieldName, rawData, out SerializationType actualType); + + string? GetString() { // rawData contains a Pointer and a string length! byte* utf8Ptr = *(byte**)rawData; @@ -191,48 +211,50 @@ public class DeserializationObject return Encoding.UTF8.GetString(utf8Ptr, (int)length); } - switch (serializationType) + object? value = actualType switch { - case SerializationType.Bool: - return *(bool*)rawData; - case SerializationType.Char: - return *(char*)rawData; - case SerializationType.String: - return GetString(); - case SerializationType.Int8: - return *(sbyte*)rawData; - case SerializationType.Int16: - return *(short*)rawData; - case SerializationType.Int32: - return *(int*)rawData; - case SerializationType.Int64: - return *(long*)rawData; - case SerializationType.UInt8: - return *(byte*)rawData; - case SerializationType.UInt16: - return *(ushort*)rawData; - case SerializationType.UInt32: - return *(uint*)rawData; - case SerializationType.UInt64: - return *(ulong*)rawData; + SerializationType.Bool => *(bool*)rawData, + SerializationType.Char => *(char*)rawData, + SerializationType.String => GetString(), + SerializationType.Int8 => *(sbyte*)rawData, + SerializationType.Int16 => *(short*)rawData, + SerializationType.Int32 => *(int*)rawData, + SerializationType.Int64 => *(long*)rawData, + SerializationType.UInt8 => *(byte*)rawData, + SerializationType.UInt16 => *(ushort*)rawData, + SerializationType.UInt32 => *(uint*)rawData, + SerializationType.UInt64 => *(ulong*)rawData, + SerializationType.Float => *(float*)rawData, + SerializationType.Double => *(double*)rawData, + SerializationType.Decimal => *(decimal*)rawData, + SerializationType.Enum => GetString(), + SerializationType.EntityReference => dataHelper.EngineObjectReference, + SerializationType.ComponentReference => dataHelper.EngineObjectReference, + SerializationType.ObjectReference => dataHelper.UUID, + _ => NoValueDeserialized + }; - case SerializationType.Float: - return *(float*)rawData; - case SerializationType.Double: - return *(double*)rawData; - case SerializationType.Decimal: - return *(decimal*)rawData; - - case SerializationType.Enum: - return GetString(); - case SerializationType.EntityReference: - case SerializationType.ComponentReference: - return dataHelper.EngineObjectReference; - case SerializationType.ObjectReference: - return *(UUID*)rawData; - default: + if (actualType != expectedType) + { + if (!actualType.CanConvertTo(expectedType)) + { return NoValueDeserialized; + } + + try + { + Type? targetType = expectedType.GetTypeInstance(); + + return targetType != null ? Convert.ChangeType(value, targetType) : NoValueDeserialized; + } + catch (Exception e) + { + Log.Error($"Failed to convert type {actualType} to {expectedType}: {e}"); + return NoValueDeserialized; + } } + + return value; } public void Deserialize(Entity entity) @@ -284,7 +306,8 @@ public class DeserializationObject if (fieldType.IsGenericType && _customDeserializers.TryGetValue(fieldType.GetGenericTypeDefinition(), out deserializeMethod)) { deserializedValue = deserializeMethod(this, fieldName, fieldType); - } return true; + return true; + } } catch (Exception e) { @@ -407,9 +430,12 @@ public class DeserializationObject return null; // Get serialization container for the instance - DeserializationObject deserializedObject = GetDeserializedObject(id); + DeserializationObject? deserializedObject = GetDeserializedObject(id); - Type type = deserializedObject.StoredType; + if (deserializedObject == null) + return NoValueDeserialized; + + Type? type = deserializedObject.StoredType; if (type == null || !type.IsAssignableTo(fieldType)) return null; diff --git a/ScriptCore/Serialization/EntitySerializer.cs b/ScriptCore/Serialization/EntitySerializer.cs index 0168661..2570273 100644 --- a/ScriptCore/Serialization/EntitySerializer.cs +++ b/ScriptCore/Serialization/EntitySerializer.cs @@ -7,34 +7,79 @@ using GlitchyEngine.Extensions; namespace GlitchyEngine.Serialization; -public enum SerializationType : int +[Flags] +public enum SerializationType : uint { - None, + None = 0, - Bool, + Bool = 1u << 31, - Char, - String, + TextTypes = 1 << 30, - Int8, - Int16, - Int32, - Int64, - UInt8, - UInt16, - UInt32, - UInt64, + Char = TextTypes | 1, + String = TextTypes | 2, - Float, - Double, - Decimal, + Number = 1 << 29, + Integer = Number | 1 << 28, - Enum, + Int8 = Integer | 1, + Int16 = Integer | 2, + Int32 = Integer | 3, + Int64 = Integer | 4, + UInt8 = Integer | 5, + UInt16 = Integer | 6, + UInt32 = Integer | 7, + UInt64 = Integer | 8, - EntityReference, - ComponentReference, + FloatingPoint = Number | 1 << 27, - ObjectReference + Float = FloatingPoint | 1, + Double = FloatingPoint | 2, + Decimal = FloatingPoint | 3, + + Enum = 1 << 26, + + EntityReference = 1 << 25, + ComponentReference = 1 << 24, + + ObjectReference = 1 << 23, +} + +public static class SerializationTypeExtension +{ + public static bool CanConvertTo(this SerializationType currentType, SerializationType targetType) + { + if (currentType.HasFlag(SerializationType.Number) && targetType.HasFlag(SerializationType.Number)) + return true; + + return false; + } + + public static Type? GetTypeInstance(this SerializationType currentType) + { + return currentType switch + { + SerializationType.Bool => typeof(bool), + SerializationType.Char => typeof(char), + SerializationType.String => typeof(string), + SerializationType.Int8 => typeof(sbyte), + SerializationType.Int16 => typeof(short), + SerializationType.Int32 => typeof(int), + SerializationType.Int64 => typeof(long), + SerializationType.UInt8 => typeof(byte), + SerializationType.UInt16 => typeof(ushort), + SerializationType.UInt32 => typeof(uint), + SerializationType.UInt64 => typeof(ulong), + SerializationType.Float => typeof(float), + SerializationType.Double => typeof(double), + SerializationType.Decimal => typeof(decimal), + SerializationType.Enum => typeof(Enum), + SerializationType.EntityReference => typeof(UUID), + SerializationType.ComponentReference => typeof(UUID), + SerializationType.ObjectReference => typeof(UUID), + _ => null + }; + } } internal static class EntitySerializer diff --git a/ScriptCore/Serialization/SerializedObject.cs b/ScriptCore/Serialization/SerializedObject.cs index 3a390c0..e97841f 100644 --- a/ScriptCore/Serialization/SerializedObject.cs +++ b/ScriptCore/Serialization/SerializedObject.cs @@ -40,7 +40,7 @@ public class SerializedObject if (_serializedClasses.TryGetValue(o, out context)) return (context, false); - ScriptGlue.Serialization_CreateObject(_internalContext, o.GetType().FullName, out IntPtr contextPtr, out UUID id); + ScriptGlue.Serialization_CreateObject(_internalContext, o.GetType().ToString(), out IntPtr contextPtr, out UUID id); context = new SerializedObject(contextPtr, id, _serializedClasses); @@ -102,7 +102,7 @@ public class SerializedObject { Type type = obj.GetType(); - foreach (FieldInfo field in type.GetFields()) + foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) { if (!EntitySerializer.SerializeField(field)) continue; @@ -144,7 +144,7 @@ public class SerializedObject public void SerializeField(string fieldName, object fieldValue, Type fieldType) { - if (fieldType.IsPrimitive) + if (fieldType.IsPrimitive || fieldType == typeof(decimal)) { SerializePrimitive(fieldName, fieldValue, fieldType); } @@ -238,8 +238,6 @@ public class SerializedObject public void SerializePrimitive(string fieldName, object fieldValue, Type fieldType) { - Debug.Assert(fieldType.IsPrimitive, $"{fieldType} is not a primitive type."); - SerializationType type = SerializationType.None; if (fieldType == typeof(bool))