// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.DataStructure.Adapters
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
using ProtoBuf;
using UnityEngine;
using WallstopStudios.UnityHelpers.Core.Attributes;
using WallstopStudios.UnityHelpers.Core.Helper;
///
/// Unity-serializable alternative to that supports ProtoBuf and JSON.
/// Enables authoring optional value types in the inspector without custom drawer code.
///
///
/// respawnDelay = new SerializableNullable(5f);
///
/// public bool TryGetRespawnDelay(out float seconds)
/// {
/// return respawnDelay.TryGet(out seconds);
/// }
/// }
/// ]]>
///
/// The underlying value type.
[Serializable]
[ProtoContract]
[JsonConverter(typeof(SerializableNullableJsonConverterFactory))]
public struct SerializableNullable
: IEquatable>,
IEquatable,
IEquatable,
ISerializationCallbackReceiver,
ISerializable
where T : struct
{
[SerializeField]
[ProtoMember(1)]
private bool _hasValue;
[SerializeField]
[ProtoMember(2, IsRequired = false)]
[WShowIf(nameof(_hasValue))]
private T _value;
///
/// Initializes a new instance of the struct.
///
/// Value to assign.
public SerializableNullable(T value)
{
_hasValue = true;
_value = value;
}
///
/// Initializes a new instance of the struct.
///
/// Nullable value to copy.
public SerializableNullable(T? value)
{
_hasValue = value.HasValue;
_value = value.GetValueOrDefault();
}
private SerializableNullable(SerializationInfo info, StreamingContext context)
{
_hasValue = info.GetBoolean(nameof(_hasValue));
if (_hasValue)
{
object boxed = info.GetValue(nameof(_value), typeof(T));
_value = boxed != null ? (T)boxed : default;
}
else
{
_value = default;
}
}
///
/// Gets a value indicating whether the instance currently stores a value.
///
public bool HasValue => _hasValue;
///
/// Gets the stored value, throwing when the value is absent.
///
public T Value
{
get
{
if (!_hasValue)
{
throw new InvalidOperationException("Nullable object must have a value.");
}
return _value;
}
}
///
/// Attempts to copy the stored value.
///
/// Receives the stored value when present.
/// True when a value is available.
public bool TryGetValue(out T result)
{
if (_hasValue)
{
result = _value;
return true;
}
result = default;
return false;
}
///
/// Clears the stored value.
///
public void Clear()
{
_hasValue = false;
_value = default;
}
///
/// Assigns a new value.
///
/// Value to assign.
public void SetValue(T newValue)
{
_hasValue = true;
_value = newValue;
}
///
/// Returns the stored value or the underlying type default.
///
public T GetValueOrDefault()
{
return _value;
}
///
/// Returns the stored value or a provided default.
///
/// Fallback value.
public T GetValueOrDefault(T defaultValue)
{
return _hasValue ? _value : defaultValue;
}
///
public override string ToString()
{
return _hasValue ? _value.ToString() : string.Empty;
}
///
public override int GetHashCode()
{
if (!_hasValue)
{
return 0;
}
EqualityComparer comparer = EqualityComparer.Default;
return comparer.GetHashCode(_value);
}
///
public override bool Equals(object obj)
{
if (obj is SerializableNullable otherNullable)
{
return Equals(otherNullable);
}
if (obj is T otherValue)
{
return Equals(otherValue);
}
return obj == null && !_hasValue;
}
///
public bool Equals(SerializableNullable other)
{
if (_hasValue != other._hasValue)
{
return false;
}
if (!_hasValue)
{
return true;
}
EqualityComparer comparer = EqualityComparer.Default;
return comparer.Equals(_value, other._value);
}
///
public bool Equals(T? other)
{
if (_hasValue != other.HasValue)
{
return false;
}
if (!_hasValue)
{
return true;
}
EqualityComparer comparer = EqualityComparer.Default;
return comparer.Equals(_value, other.GetValueOrDefault());
}
///
public bool Equals(T other)
{
if (!_hasValue)
{
return false;
}
EqualityComparer comparer = EqualityComparer.Default;
return comparer.Equals(_value, other);
}
///
/// Implicit conversion from value to nullable wrapper.
///
public static implicit operator SerializableNullable(T value)
{
SerializableNullable wrapper = new(value);
return wrapper;
}
///
/// Implicit conversion from to wrapper.
///
public static implicit operator SerializableNullable(T? value)
{
SerializableNullable wrapper = new(value);
return wrapper;
}
///
/// Implicit conversion to .
///
public static implicit operator T?(SerializableNullable value)
{
if (!value._hasValue)
{
return null;
}
return value._value;
}
///
/// Explicit conversion to the underlying value.
///
public static explicit operator T(SerializableNullable value)
{
return value.Value;
}
///
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
if (!_hasValue)
{
_value = default;
}
}
///
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
if (!_hasValue)
{
_value = default;
}
}
///
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue(nameof(_hasValue), _hasValue);
if (_hasValue)
{
info.AddValue(nameof(_value), _value, typeof(T));
}
}
internal static class SerializedPropertyNames
{
internal const string HasValue = nameof(_hasValue);
internal const string Value = nameof(_value);
}
internal void ForceStateForTesting(bool hasValue, T rawValue)
{
_hasValue = hasValue;
_value = rawValue;
}
internal static bool TryGetValueFieldAttribute(out WShowIfAttribute attribute)
{
attribute = ValueFieldAttribute ??= ResolveValueFieldAttribute();
return attribute != null;
}
private static WShowIfAttribute ValueFieldAttribute;
private static WShowIfAttribute ResolveValueFieldAttribute()
{
FieldInfo valueField = typeof(SerializableNullable).GetField(
SerializedPropertyNames.Value,
BindingFlags.Instance | BindingFlags.NonPublic
);
return ReflectionHelpers.TryGetAttributeSafe(
valueField,
out WShowIfAttribute attribute,
inherit: false
)
? attribute
: null;
}
}
internal static class SerializableNullableSerializedPropertyNames
{
internal const string HasValue = SerializableNullable.SerializedPropertyNames.HasValue;
internal const string Value = SerializableNullable.SerializedPropertyNames.Value;
}
internal sealed class SerializableNullableJsonConverterFactory : JsonConverterFactory
{
private static readonly ConcurrentDictionary ConverterCache = new();
///
/// Determines whether the provided type is a wrapper.
///
/// The type requested by .
/// true when the factory can create a converter.
///
/// )) != null;
/// ]]>
///
public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsGenericType)
{
return false;
}
Type genericType = typeToConvert.GetGenericTypeDefinition();
return genericType == typeof(SerializableNullable<>);
}
///
/// Creates a concrete converter for the inner value type.
///
/// The requested type.
/// Serializer options requesting the converter.
/// A converter instance that can read and write the wrapper.
///
/// ));
/// ]]>
///
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
Type[] arguments = type.GetGenericArguments();
Type valueType = arguments[0];
return ConverterCache.GetOrAdd(
valueType,
value =>
{
Type converterType =
typeof(SerializableNullableJsonConverter<>).MakeGenericType(value);
Func