Represents a datamodel Attribute (name/value pair) used by the HumanoidRetargeter editor tools. It stores name, owner, value (with deferred decoding via an encoded DMX codec), enforces allowed types and ownership rules, supports override typing for certain value types, and can destub Elements on access.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AttrKVP = System.Collections.Generic.KeyValuePair<string, object?>;
namespace HumanoidRetargeter.EditorTools.Embedded.Datamodel;
/// <summary>
/// A name/value pair associated with an <see cref="Element"/>.
/// </summary>
class Attribute
{
/// <summary>
/// Creates a new Attribute with a specified name and value.
/// </summary>
/// <param name="name">The name of the Attribute, which must be unique to its owner.</param>
/// <param name="value">The value of the Attribute, which must be of a supported Datamodel type.</param>
public Attribute(string name, AttributeList owner, object? value)
{
ArgumentNullException.ThrowIfNull(name);
Name = name;
_Owner = owner;
Value = value;
}
/// <summary>
/// Creates a new Attribute with deferred loading.
/// </summary>
/// <param name="name">The name of the Attribute, which must be unique to its owner.</param>
/// <param name="owner">The AttributeList which owns this Attribute.</param>
/// <param name="defer_offset">The location in the encoded DMX stream at which this Attribute's value can be found.</param>
public Attribute(string name, AttributeList owner, long defer_offset)
: this(name, owner, null)
{
ArgumentNullException.ThrowIfNull(owner);
Offset = defer_offset;
}
#region Properties
/// <summary>
/// Gets the name of this Attribute.
/// </summary>
public string Name { get; private set; }
/// <summary>
/// Gets the Type of this Attribute's Value.
/// </summary>
public Type ValueType { get; private set; } = typeof(Element);
/// <summary>
/// Gets or sets the OverrideType of this Attributes.
/// </summary>
public AttributeList.OverrideType? OverrideType
{
get
{
return _OverrideType;
}
set
{
switch (value)
{
case null:
break;
case AttributeList.OverrideType.Angle:
if (ValueType != typeof(global::System.Numerics.Vector3))
throw new AttributeTypeException("OverrideType.Angle can only be applied to global::System.Numerics.Vector3 attributes");
break;
case AttributeList.OverrideType.Binary:
if (ValueType != typeof(byte[]))
throw new AttributeTypeException("OverrideType.Binary can only be applied to byte[] attributes");
break;
default:
throw new NotImplementedException();
}
_OverrideType = value;
}
}
AttributeList.OverrideType? _OverrideType;
/// <summary>
/// Gets the <see cref="AttributeList"/> which this Attribute is a member of.
/// </summary>
public AttributeList? Owner
{
get { return _Owner; }
internal set
{
if (_Owner == value) return;
if (Deferred && _Owner != null) DeferredLoad();
_Owner = value;
}
}
AttributeList? _Owner;
Datamodel? OwnerHumanoidRetargeterDmx { get { return Owner?.Owner; } }
/// <summary>
/// Gets whether the value of this Attribute has yet to be decoded.
/// </summary>
public bool Deferred { get { return Offset != 0; } }
/// <summary>
/// Loads the value of this Attribute from the encoded source Datamodel.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when the Attribute has already been loaded.</exception>
/// <exception cref="CodecException">Thrown when the deferred load fails.</exception>
public void DeferredLoad()
{
if (Offset == 0) throw new InvalidOperationException("Attribute already loaded.");
if (OwnerHumanoidRetargeterDmx == null || OwnerHumanoidRetargeterDmx.Codec == null)
throw new CodecException("Trying to load a deferred Attribute, but could not find codec.");
try
{
lock (OwnerHumanoidRetargeterDmx.Codec)
{
_Value = OwnerHumanoidRetargeterDmx.Codec.DeferredDecodeAttribute(OwnerHumanoidRetargeterDmx, Offset);
}
}
catch (Exception err)
{
throw new CodecException($"Deferred loading of attribute \"{Name}\" on element {((Element?)Owner)?.ID} using {OwnerHumanoidRetargeterDmx.Codec} codec threw an exception.", err);
}
Offset = 0;
if (_Value is ElementArray elem_array)
elem_array.Owner = Owner;
}
/// <summary>
/// Gets or sets the value held by this Attribute.
/// </summary>
/// <exception cref="CodecException">Thrown when deferred value loading fails.</exception>
/// <exception cref="DestubException">Thrown when Element destubbing fails.</exception>
public object? Value
{
get
{
if (Offset > 0)
DeferredLoad();
if (OwnerHumanoidRetargeterDmx != null)
{
// expand stubs
if (_Value is Element elem && elem.Stub)
{
try { _Value = OwnerHumanoidRetargeterDmx.OnStubRequest(elem.ID) ?? _Value; }
catch (Exception err) { throw new DestubException(this, err); }
}
}
return _Value;
}
set
{
ValueType = value == null ? typeof(Element) : value.GetType();
if (!Datamodel.IsHumanoidRetargeterDmxType(ValueType))
throw new AttributeTypeException(String.Format("{0} is not a valid Datamodel attribute type. (If this is an array, it must implement IList<T>).", ValueType.FullName));
if (value is Element elem)
{
if (elem.Owner == null)
elem.Owner = OwnerHumanoidRetargeterDmx;
else if (elem.Owner != OwnerHumanoidRetargeterDmx)
throw new ElementOwnershipException();
}
if (value is IEnumerable<Element> elem_enumerable)
{
if (elem_enumerable is not ElementArray)
throw new InvalidOperationException("Element array objects must derive from Datamodel.ElementArray");
var elem_array = (ElementArray)value;
if (elem_array.Owner == null)
elem_array.Owner = Owner;
else if (elem_array.Owner != Owner)
throw new InvalidOperationException("ElementArray is already owned by a different Datamodel.");
foreach (var arr_elem in elem_array)
{
if (arr_elem == null) continue;
else if (arr_elem.Owner == null)
arr_elem.Owner = OwnerHumanoidRetargeterDmx;
// todo: remove ownership from values
// this is being printed on a debuggerdisplay output for some reason
// else if (arr_elem.Owner != OwnerHumanoidRetargeterDmx)
// throw new ElementOwnershipException("One or more Elements in the assigned collection are from a different Datamodel. Use ImportElement() to copy them to this one before assigning.");
}
}
_Value = value;
Offset = 0;
}
}
object? _Value = null;
/// <summary>
/// Gets the Attribute's Value without attempting deferred loading or destubbing.
/// </summary>
public object? RawValue { get { return _Value; } }
#endregion
long Offset;
public override string ToString()
{
var type = Value != null ? Value.GetType() : typeof(Element);
var inner_type = Datamodel.GetArrayInnerType(type);
return String.Format("{0} <{1}>", Name, inner_type != null ? inner_type.FullName + "[]" : type.FullName);
}
public AttrKVP ToKeyValuePair()
{
return new AttrKVP(Name, Value);
}
}