ReactiveComponent.cs
#if SANDBOX
using System.Diagnostics;
using Sandbox.Reactivity.Internals;
using static Sandbox.Reactivity.Reactive;
#if JETBRAINS_ANNOTATIONS
#endif
namespace Sandbox.Reactivity;
/// <summary>
/// The base component used to opt into the reactivity system. This allows usage of the reactive property attributes,
/// and automatically creates an effect root in <see cref="OnActivate" />.
/// </summary>
/// <seealso cref="ReactiveAttribute" />
/// <seealso cref="DerivedAttribute" />
#if JETBRAINS_ANNOTATIONS
#endif
public abstract class ReactiveComponent(ReactiveComponent.ActivationStage activationStage) : Component,
IReactivePropertyContainer
{
/// <summary>
/// Describes at what point during its lifecycle a <see cref="ReactiveComponent" /> will run its activation method.
/// </summary>
public enum ActivationStage
{
/// <summary>
/// Calls <see cref="ReactiveComponent.OnActivate" /> during <see cref="Component.OnEnabled" />. This is the
/// default.
/// </summary>
OnEnabled,
/// <summary>
/// Calls <see cref="ReactiveComponent.OnActivate" /> during <see cref="Component.OnStart" />. Useful for when
/// you want to access data on other components on the same game object that is only available <i>after</i>
/// they're enabled.
/// </summary>
/// <remarks>
/// If the component is disabled and re-enabled, the activation will subsequently run during
/// <see cref="Component.OnEnabled" /> since <see cref="Component.OnStart" /> is only called once ever.
/// </remarks>
OnStart,
}
/// <summary>
/// When to run this component's activation method.
/// </summary>
private readonly ActivationStage _activationStage = activationStage;
/// <summary>
/// The disposable for this component's effect root that exists while it's enabled.
/// </summary>
private Effect? _effectRoot;
/// <summary>
/// Whether this component has ever called <see cref="Component.OnStart" />.
/// </summary>
private bool _hasStarted;
protected ReactiveComponent()
: this(ActivationStage.OnEnabled)
{
}
Dictionary<int, IProducer> IReactivePropertyContainer.Producers { get; } = [];
/// <inheritdoc cref="GameObjectExtensions.SendDirect" />
public void SendDirect<T>(T eventData)
{
GameObject?.SendDirect(eventData);
}
/// <inheritdoc cref="GameObjectExtensions.SendUp" />
public void SendUp<T>(T eventData)
{
GameObject?.SendUp(eventData);
}
/// <inheritdoc cref="GameObjectExtensions.SendDown" />
public void SendDown<T>(T eventData)
{
GameObject?.SendDown(eventData);
}
/// <summary>
/// Runs a function when this component's game object receives an event.
/// </summary>
/// <param name="callback">
/// The function to run when the event is received. If the function returns <c>true</c>, the event will prevent
/// further propagation to ancestor/descendant game objects.
/// </param>
/// <typeparam name="T">
/// The type of event to listen to. Any received game objects that are assignable to this type will run the
/// function.
/// </typeparam>
public void OnEvent<T>(Func<T, bool> callback)
{
if (GameObject is not { } go)
{
return;
}
var manager = GameObjectEventManager.GetOrCreate(go);
var parent = Runtime.EnsureCurrentEffect();
var effect = new Effect([StackTraceHidden] [DebuggerStepThrough]() =>
{
manager.Add(callback);
return () => manager.Remove(callback);
},
parent,
false);
effect.SetDebugInfo($"OnEvent<{typeof(T).ToSimpleString(false)}>",
"electric_bolt",
new CallLocation(1),
parent);
effect.Run();
}
[StackTraceHidden]
private void CreateRootEffect()
{
_effectRoot?.Dispose();
// component lifetimes are managed by their owning game objects, it doesn't make sense to bind it to the
// lifetime of the currently executing effect
_effectRoot = new Effect([StackTraceHidden] [DebuggerStepThrough]() =>
{
OnActivate();
return null;
},
null,
false);
_effectRoot.SetDebugInfo(DisplayInfo.For(this).Name,
DisplayInfo.For(this).Icon,
new CallLocation(GetType(), nameof(OnActivate)),
this);
_effectRoot.Run();
}
protected sealed override void OnEnabled()
{
if (_activationStage != ActivationStage.OnStart || _hasStarted)
{
CreateRootEffect();
}
}
protected sealed override void OnStart()
{
if (_activationStage == ActivationStage.OnStart)
{
_hasStarted = true;
CreateRootEffect();
}
}
protected sealed override void OnDisabled()
{
_effectRoot?.Dispose();
_effectRoot = null;
}
/// <summary>
/// Called inside an effect root when this component is enabled, allowing for effects to be created. When this
/// component is disabled, the effect root (and all of its descendants) are disposed.
/// </summary>
protected virtual void OnActivate()
{
}
}
#endif