Editor/TypeLibraryExtensions.cs
using Facepunch.ActionGraphs;
using Sandbox;
using Sandbox.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

namespace ExtendedEditor;

public static class TypeLibraryExtensions
{
    extension(TypeLibrary typeLibrary)
    {
        public SerializedProperty CreateProperty(Type type, string title, Func<object> getter,
            Action<object> setter, Attribute[]? attributes = null, SerializedObject? parent = null)
        {
            var property = new ActionBasedSerializedProperty(type, title, title, "", getter, setter, attributes, parent);
            property.PropertyToObject = PropertyToObject!;
            return property;
        }
    }

    private static SerializedObject? PropertyToObject(SerializedProperty property)
    {
        var value = property.GetValue<object>();
        var result = value.GetSerialized();
        result?.ParentProperty = property;
        return result;
    }

    private class ActionBasedSerializedProperty : SerializedProperty
    {
        public Func<SerializedProperty, SerializedObject>? PropertyToObject;

        private readonly Type _type;
        private readonly string _name = string.Empty;
        private readonly string _title = string.Empty;
        private readonly string _description = string.Empty;
        private readonly string _groupName = string.Empty;
        private readonly string _sourceFile = string.Empty;
        private readonly int _sourceLine = -1;
        private readonly Func<object> _get;
        private readonly Action<object> _set;
        private readonly List<Attribute> _attributes;
        private readonly SerializedObject? _parent;

        public ActionBasedSerializedProperty(Type type, string name, string title, string description, Func<object> get,
            Action<object> set, Attribute[]? attributes, SerializedObject? parent)
        {
            _type = type;
            _name = name;
            _title = title;
            _description = description;
            _get = get;
            _set = set;
            _attributes = [.. attributes ?? []];
            _parent = parent;

            _groupName = _attributes.OfType<IGroupAttribute>().FirstOrDefault()?.Value ?? _groupName;
            _groupName = _attributes.OfType<ICategoryProvider>().FirstOrDefault()?.Value ?? _groupName;
            _description = _attributes.OfType<IDescriptionAttribute>().FirstOrDefault()?.Value ?? _description;
            _title = _attributes.OfType<ITitleProvider>().FirstOrDefault()?.Value ?? _title;
            _sourceFile = _attributes.OfType<ISourcePathProvider>().FirstOrDefault()?.Path ?? _sourceFile;
            _sourceLine = _attributes.OfType<ISourceLineProvider>().FirstOrDefault()?.Line ?? _sourceLine;
        }

        public override SerializedObject? Parent => _parent;
        public override bool IsMethod => false;
        public override string Name => _name;
        public override string DisplayName => _title;
        public override string Description => _description;
        public override string GroupName => _groupName;
        public override bool IsEditable => true;
        public override int Order => 0;
        public override Type PropertyType => _type;
        public override string SourceFile => _sourceFile;
        public override int SourceLine => _sourceLine;
        public override bool HasChanges => false;

        public override ref AsAccessor As => ref base.As;

        public override U GetValue<U>(U defaultValue = default!) => ValueToType<U>(_get());
        public override void SetValue<U>(U value) => _set(ValueToType(_type, value)!);
        public override IEnumerable<Attribute> GetAttributes() => _attributes;

        public override bool TryGetAsObject(out SerializedObject obj)
        {
            obj = PropertyToObject?.Invoke(this) ?? null!;
            return obj is not null;
        }

        private static object? ValueToType(Type type, object value, object defaultValue = default!)
        {
            try
            {
                if(value is null)
                    return defaultValue;

                if(value.GetType().IsAssignableTo(type))
                    return value;

                if(type == typeof(string))
                    return (object)$"{value}";

                if(value.GetType() == typeof(string))
                    return JsonSerializer.Deserialize((string)value, type);

                // Convert.ChangeType doesn't support long to enum
                if(type.IsEnum && value is IConvertible)
                {
                    try
                    {
                        return Enum.ToObject(type, Convert.ToInt64(value));
                    }
                    catch
                    {
                        return defaultValue;
                    }
                }

                var converted = Convert.ChangeType(value, type);
                if(converted is not null)
                    return converted;

                var jsonElement = JsonSerializer.SerializeToElement(value);
                return jsonElement.Deserialize(type);
            }
            catch(System.Exception)
            {
                return defaultValue;
            }
        }
    }
}