// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Utils
{
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
using WallstopStudios.UnityHelpers.Core.Attributes;
///
/// Non-generic helper for ScriptableObjectSingleton initialization state tracking.
///
internal static class ScriptableObjectSingletonInitState
{
#if UNITY_EDITOR
private static bool _initialEnsureCompleted;
///
/// Indicates whether the initial singleton asset creation pass has completed globally.
/// When false, metadata-related warnings are suppressed since assets may not exist yet.
///
internal static bool InitialEnsureCompleted
{
get => _initialEnsureCompleted;
set => _initialEnsureCompleted = value;
}
#endif
}
[Serializable]
internal sealed class ScriptableObjectSingletonMetadata : ScriptableObject
{
public const string ResourcePath =
"Wallstop Studios/Unity Helpers/ScriptableObjectSingletonMetadata";
public const string AssetPath =
"Assets/Resources/Wallstop Studios/Unity Helpers/ScriptableObjectSingletonMetadata.asset";
///
/// Legacy path for migration from older versions.
///
internal const string LegacyAssetPath =
"Assets/Resources/ScriptableObjectSingletonMetadata.asset";
[Serializable]
public struct Entry
{
public string assemblyQualifiedTypeName;
public string resourcesLoadPath;
public string resourcesPath;
// ReSharper disable once NotAccessedField.Global
public string assetGuid;
}
[FormerlySerializedAs("entries")]
[SerializeField]
private List _entries = new();
public bool TryGetEntry(Type type, out Entry entry)
{
if (type is null)
{
entry = default;
return false;
}
string key = type.AssemblyQualifiedName;
if (string.IsNullOrEmpty(key) || _entries == null || _entries.Count == 0)
{
entry = default;
return false;
}
foreach (Entry candidate in _entries)
{
if (
string.Equals(
candidate.assemblyQualifiedTypeName,
key,
StringComparison.Ordinal
)
)
{
entry = candidate;
return true;
}
}
entry = default;
return false;
}
#if UNITY_EDITOR
///
/// Adds or updates a singleton metadata entry.
///
/// The entry to store.
/// true when the serialized metadata changed; otherwise, false.
public bool SetOrUpdateEntry(Entry entry)
{
_entries ??= new List();
string key = entry.assemblyQualifiedTypeName;
for (int i = 0; i < _entries.Count; i++)
{
if (
string.Equals(
_entries[i].assemblyQualifiedTypeName,
key,
StringComparison.Ordinal
)
)
{
if (EntriesEqual(_entries[i], entry))
{
return false;
}
_entries[i] = entry;
return true;
}
}
_entries.Add(entry);
return true;
}
private static bool EntriesEqual(Entry left, Entry right)
{
return string.Equals(
left.assemblyQualifiedTypeName,
right.assemblyQualifiedTypeName,
StringComparison.Ordinal
)
&& string.Equals(
left.resourcesLoadPath,
right.resourcesLoadPath,
StringComparison.Ordinal
)
&& string.Equals(left.resourcesPath, right.resourcesPath, StringComparison.Ordinal)
&& string.Equals(left.assetGuid, right.assetGuid, StringComparison.Ordinal);
}
///
/// Removes an entry for the specified type from the metadata.
///
/// The assembly-qualified type name to remove.
/// True if an entry was removed; false otherwise.
public bool RemoveEntry(string assemblyQualifiedTypeName)
{
if (string.IsNullOrEmpty(assemblyQualifiedTypeName) || _entries == null)
{
return false;
}
for (int i = 0; i < _entries.Count; i++)
{
if (
string.Equals(
_entries[i].assemblyQualifiedTypeName,
assemblyQualifiedTypeName,
StringComparison.Ordinal
)
)
{
_entries.RemoveAt(i);
return true;
}
}
return false;
}
///
/// Gets all entries in the metadata. For editor tooling use only.
///
public IReadOnlyList GetAllEntries()
{
return _entries ?? (IReadOnlyList)Array.Empty();
}
///
/// Clears all metadata entries. Available for manual cleanup operations.
/// Note: The Sync operation updates entries incrementally and does not use this method.
///
public void ClearAllEntries()
{
_entries?.Clear();
}
///
/// Delegate that performs the actual sync operation. Set by the Editor assembly.
///
internal static Action SyncImplementation { get; set; }
///
/// Re-scans all assemblies for ScriptableObjectSingleton types and updates their metadata entries.
/// This cleans up stale entries and adds any missing singleton metadata.
///
[WButton]
public void Sync()
{
if (SyncImplementation != null)
{
SyncImplementation(this);
}
else
{
UnityEngine.Debug.LogWarning(
"ScriptableObjectSingletonMetadata.Sync: No sync implementation registered. "
+ "This method should only be called from the Unity Editor."
);
}
}
#endif
}
}