// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.Helper
{
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using Utils;
using WallstopStudios.UnityHelpers.Tags;
///
/// Reflection-driven auto-loader that instantiates opt-in singletons during specific Unity load phases.
///
internal static class SingletonAutoLoader
{
private static readonly Dictionary _cachedLoaders = new(
StringComparer.Ordinal
);
private static readonly HashSet _executedLoadTypes = new();
private static readonly object _executionLock = new();
private static readonly Dictionary _runtimeInstanceProperties = new();
private static readonly Dictionary _scriptableInstanceProperties =
new();
private static readonly object _loaderBuildLock = new();
#if UNITY_INCLUDE_TESTS
private static bool? _testPlayModeOverride;
#endif
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void AutoLoadSubsystemRegistration() =>
ExecuteForLoadType(RuntimeInitializeLoadType.SubsystemRegistration);
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
private static void AutoLoadAfterAssemblies() =>
ExecuteForLoadType(RuntimeInitializeLoadType.AfterAssembliesLoaded);
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)]
private static void AutoLoadBeforeSplashScreen() =>
ExecuteForLoadType(RuntimeInitializeLoadType.BeforeSplashScreen);
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void AutoLoadBeforeSceneLoad() =>
ExecuteForLoadType(RuntimeInitializeLoadType.BeforeSceneLoad);
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void AutoLoadAfterSceneLoad() =>
ExecuteForLoadType(RuntimeInitializeLoadType.AfterSceneLoad);
private static void ExecuteForLoadType(RuntimeInitializeLoadType loadType)
{
AttributeMetadataCache cache = AttributeMetadataCache.Instance;
AttributeMetadataCache.AutoLoadSingletonEntry[] entries =
cache?.AutoLoadSingletons
?? Array.Empty();
ExecuteEntries(entries, loadType, enforceSingleExecution: true, requirePlayMode: true);
}
private static void ExecuteEntries(
IReadOnlyList entries,
RuntimeInitializeLoadType loadType,
bool enforceSingleExecution,
bool requirePlayMode
)
{
if (entries == null || entries.Count == 0)
{
return;
}
bool isPlayMode = Application.isPlaying;
#if UNITY_INCLUDE_TESTS
if (_testPlayModeOverride.HasValue)
{
isPlayMode = _testPlayModeOverride.Value;
}
#endif
if (requirePlayMode && !isPlayMode)
{
return;
}
if (enforceSingleExecution)
{
lock (_executionLock)
{
if (!_executedLoadTypes.Add(loadType))
{
return;
}
}
}
for (int i = 0; i < entries.Count; i++)
{
AttributeMetadataCache.AutoLoadSingletonEntry entry = entries[i];
if (entry == null || entry.loadType != loadType)
{
continue;
}
try
{
Action loader = GetOrCreateLoader(entry);
loader?.Invoke();
}
catch (Exception e)
{
Debug.LogError(
$"SingletonAutoLoader: Failed to auto-load '{entry.typeName}'. {e}"
);
}
}
}
private static Action GetOrCreateLoader(AttributeMetadataCache.AutoLoadSingletonEntry entry)
{
if (entry == null || string.IsNullOrWhiteSpace(entry.typeName))
{
return null;
}
if (_cachedLoaders.TryGetValue(entry.typeName, out Action cached))
{
return cached;
}
lock (_loaderBuildLock)
{
if (_cachedLoaders.TryGetValue(entry.typeName, out cached))
{
return cached;
}
Action loader = BuildLoader(entry);
_cachedLoaders[entry.typeName] = loader;
return loader;
}
}
private static Action BuildLoader(AttributeMetadataCache.AutoLoadSingletonEntry entry)
{
Type singletonType = ReflectionHelpers.TryResolveType(entry.typeName);
if (singletonType == null)
{
Debug.LogWarning(
$"SingletonAutoLoader: Unable to resolve type '{entry.typeName}' for auto-load."
);
return null;
}
switch (entry.kind)
{
case SingletonAutoLoadKind.Runtime:
return BuildRuntimeLoader(singletonType);
case SingletonAutoLoadKind.ScriptableObject:
return BuildScriptableLoader(singletonType);
default:
Debug.LogWarning(
$"SingletonAutoLoader: Unsupported singleton kind '{entry.kind}' for type '{entry.typeName}'."
);
return null;
}
}
private static Action BuildRuntimeLoader(Type singletonType)
{
PropertyInfo instanceProperty = GetRuntimeInstanceProperty(singletonType);
if (instanceProperty == null)
{
Debug.LogWarning(
$"SingletonAutoLoader: {singletonType.FullName} does not derive from RuntimeSingleton<>."
);
return null;
}
return () =>
{
_ = instanceProperty.GetValue(null);
};
}
private static Action BuildScriptableLoader(Type singletonType)
{
PropertyInfo instanceProperty = GetScriptableInstanceProperty(singletonType);
if (instanceProperty == null)
{
Debug.LogWarning(
$"SingletonAutoLoader: {singletonType.FullName} does not derive from ScriptableObjectSingleton<>."
);
return null;
}
return () =>
{
_ = instanceProperty.GetValue(null);
};
}
private static PropertyInfo GetRuntimeInstanceProperty(Type singletonType)
{
lock (_loaderBuildLock)
{
if (_runtimeInstanceProperties.TryGetValue(singletonType, out PropertyInfo cached))
{
return cached;
}
PropertyInfo property = ResolveInstanceProperty(
singletonType,
typeof(RuntimeSingleton<>)
);
_runtimeInstanceProperties[singletonType] = property;
return property;
}
}
private static PropertyInfo GetScriptableInstanceProperty(Type singletonType)
{
lock (_loaderBuildLock)
{
if (
_scriptableInstanceProperties.TryGetValue(
singletonType,
out PropertyInfo cached
)
)
{
return cached;
}
PropertyInfo property = ResolveInstanceProperty(
singletonType,
typeof(ScriptableObjectSingleton<>)
);
_scriptableInstanceProperties[singletonType] = property;
return property;
}
}
private static PropertyInfo ResolveInstanceProperty(
Type singletonType,
Type openGenericBase
)
{
try
{
Type closed = openGenericBase.MakeGenericType(singletonType);
return closed.GetProperty(
nameof(RuntimeSingleton.Instance),
BindingFlags.Public | BindingFlags.Static
);
}
catch
{
return null;
}
}
#if UNITY_INCLUDE_TESTS
internal static void ExecuteEntriesForTests(
bool simulatePlayMode,
RuntimeInitializeLoadType loadType,
params AttributeMetadataCache.AutoLoadSingletonEntry[] entries
)
{
bool? previousOverride = _testPlayModeOverride;
try
{
_testPlayModeOverride = simulatePlayMode;
ExecuteEntries(
entries,
loadType,
enforceSingleExecution: false,
requirePlayMode: true
);
}
finally
{
_testPlayModeOverride = previousOverride;
}
}
#endif
}
}