Editor/TypeLibraryFixes/TypeSerializedPropertyFixed.cs
using Sandbox;
using Sandbox.Diagnostics;
using Sandbox.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace ExtendedEditor.TypeLibraryFixes;
public class TypeSerializedPropertyFixed : SerializedProperty
{
public override bool IsProperty => true;
public override SerializedObject Parent => _typeSerializedObject;
public override string DisplayName => _displayInfo.Name;
public override string Name { get; }
public override string Description => _displayInfo.Description;
public override Type PropertyType { get; }
public override string SourceFile { get; } = null!;
public override int SourceLine { get; } = 0;
public override string GroupName => _displayInfo.Group;
public override bool IsEditable => !_displayInfo.ReadOnly && _canWrite;
public override bool IsPublic { get; }
public override int Order => _displayInfo.Order;
private Sandbox.DisplayInfo _displayInfo;
private TypeSerializedObjectFixed _typeSerializedObject;
private readonly MemberInfo _memberInfo;
private readonly PropertyInfo? _propertyInfo;
private readonly FieldInfo? _fieldInfo;
private readonly bool _canWrite;
private readonly object? _defaultValue;
public TypeSerializedPropertyFixed(TypeSerializedObjectFixed typeSerializedObject, PropertyInfo propertyInfo)
{
ArgumentNullException.ThrowIfNull(typeSerializedObject, nameof(typeSerializedObject));
ArgumentNullException.ThrowIfNull(propertyInfo, nameof(propertyInfo));
_typeSerializedObject = typeSerializedObject;
_memberInfo = _propertyInfo = propertyInfo;
_displayInfo = Sandbox.DisplayInfo.ForMember(propertyInfo, inherit: true);
var isGetMethodPublic = propertyInfo.GetMethod?.IsPublic ?? false;
var isSetMethodPublic = propertyInfo.SetMethod?.IsPublic ?? false;
IsPublic = isGetMethodPublic || isSetMethodPublic;
PropertyType = propertyInfo.PropertyType;
Name = propertyInfo.Name;
_canWrite = propertyInfo.CanWrite;
var sourceLocationAttribute = propertyInfo.GetCustomAttribute<SourceLocationAttribute>();
if(sourceLocationAttribute is not null)
{
SourceLine = sourceLocationAttribute.Line;
SourceFile = sourceLocationAttribute.Path;
}
_defaultValue = PropertyType.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic, [])?.Invoke(null) ?? null;
}
public TypeSerializedPropertyFixed(TypeSerializedObjectFixed typeSerializedObject, FieldInfo fieldInfo)
{
ArgumentNullException.ThrowIfNull(typeSerializedObject, nameof(typeSerializedObject));
ArgumentNullException.ThrowIfNull(fieldInfo, nameof(fieldInfo));
_typeSerializedObject = typeSerializedObject;
_memberInfo = _fieldInfo = fieldInfo;
_displayInfo = Sandbox.DisplayInfo.ForMember(fieldInfo, inherit: true);
IsPublic = fieldInfo.IsPublic;
PropertyType = fieldInfo.FieldType;
Name = fieldInfo.Name;
_canWrite = true;
var sourceLocationAttribute = fieldInfo.GetCustomAttribute<SourceLocationAttribute>();
if(sourceLocationAttribute is not null)
{
SourceLine = sourceLocationAttribute.Line;
SourceFile = sourceLocationAttribute.Path;
}
_defaultValue = PropertyType.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic, [])?.Invoke(null) ?? null;
}
public override void SetValue<T>(T value)
{
try
{
NotePreChange();
SetValueInternal(value);
NoteChanged();
}
catch(System.Exception e)
{
var l = new Logger("TypeSerializedPropertyFixed");
l.Warning(e, $"Error setting {PropertyType} to {value} ({value?.GetType()})");
}
}
/// <summary>
/// When setting because a child property changed, we don't trigger NoteChanged
/// because the expectation is that the NoteChanged from setting that property
/// will instead propogate up, and will be more accurate.
/// </summary>
public override void SetValue<T>(T value, SerializedProperty source)
{
try
{
SetValueInternal(value);
}
catch(System.Exception e)
{
var l = new Logger("TypeSerializedPropertyFixed");
l.Warning(e, $"Error setting {PropertyType} to {value} ({value?.GetType()})");
}
}
private void SetValueInternal(object? value)
{
value = SerializedProperty.ValueToTypeFixed(PropertyType, value, _defaultValue);
if(_propertyInfo is not null)
_propertyInfo.SetValue(_typeSerializedObject.GetTargetObject(), value);
else
_fieldInfo!.SetValue(_typeSerializedObject.GetTargetObject(), value);
}
public override T GetValue<T>(T defaultValue)
{
try
{
return ValueToType(GetValueInternal(), defaultValue);
}
catch(System.Exception)
{
return defaultValue;
}
}
private object GetValueInternal()
{
if(_propertyInfo is not null)
return _propertyInfo.GetValue(_typeSerializedObject.GetTargetObject())!;
else
return _fieldInfo!.GetValue(_typeSerializedObject.GetTargetObject())!;
}
/// <inheritdoc />
public override IEnumerable<Attribute> GetAttributes()
{
if(_propertyInfo is not null)
return _propertyInfo.GetCustomAttributes();
else
return _fieldInfo!.GetCustomAttributes();
}
/// <inheritdoc/>
public override bool TryGetAsObject(out SerializedObject obj)
{
obj = null!;
if(Parent is not TypeSerializedObjectFixed szObj)
return false;
if(TryGetAsContainer(out obj))
return true;
var targetType = PropertyType;
if(targetType is null)
return false;
object targetValue = GetValueInternal();
if(targetValue is null)
return false;
targetType = targetValue.GetType();
if(targetType is null)
return false;
obj = new TypeSerializedObjectFixed(GetValueInternal, targetType, this);
return true;
}
private bool TryGetAsContainer(out SerializedObject obj)
{
obj = null!;
var so = CreateSerializedCollection(PropertyType);
if(so is not null)
{
so.ParentProperty = this;
var targetObject = GetValue<object>(null!);
// This is very presumptuous but if we have a null list, create an empty list and modify the original object target
// Otherwise we'll be editing nothing
// Alternatively we could bitch and moan they haven't initialized their list..
if(targetObject is null)
{
targetObject = PropertyType.IsSZArray
? Array.CreateInstance(PropertyType.GetElementType()!, 0)
: Activator.CreateInstance(PropertyType);
// Do not use SetValue, we don't want to propagate NoteChanged consumers
SetValueInternal(targetObject);
_typeSerializedObject.ParentProperty?.SetValue(_typeSerializedObject._targetObject);
}
so.SetTargetObject(targetObject, this);
so.PropertyToObject = PropertyToObjectFixed;
obj = so;
return true;
}
return false;
}
private static readonly MethodInfo _createSerializedCollectionMethod = typeof(SerializedCollection)
.GetMethod("Create", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, [typeof(Type)])!;
public static SerializedObject PropertyToObjectFixed(SerializedProperty property)
{
var targetType = property.PropertyType;
if(targetType is null)
return null!;
var so = CreateSerializedCollection(targetType);
if(so is not null)
{
so.ParentProperty = property;
var targetObject = property.GetValue<object>(null!);
// This is very presumptuous but if we have a null list, create an empty list and modify the original object target
// Otherwise we'll be editing nothing
// Alternatively we could bitch and moan they haven't initialized their list..
if(targetObject is null)
{
targetObject = targetType.IsSZArray
? Array.CreateInstance(targetType.GetElementType()!, 0)
: Activator.CreateInstance(targetType);
// TODO: We shouldn't use SetValue, we don't want to propagate NoteChanged consumers
property.SetValue(targetObject);
property.Parent?.ParentProperty?.SetValue(property.Parent.Targets.First());
}
so.SetTargetObject(targetObject, property);
so.PropertyToObject = PropertyToObjectFixed;
return so;
}
property.CreateObjectValue();
object getTarget() => property.GetValue<object>();
var value = getTarget();
if(value is null)
return null!;
// get type from the object, incase we're a derived type.
targetType = value.GetType();
return new TypeSerializedObjectFixed(getTarget, targetType, property);
}
private static SerializedCollection CreateSerializedCollection(Type propertyType)
{
return (SerializedCollection)_createSerializedCollectionMethod.Invoke(null, [propertyType])!;
}
}