mirror of
https://github.com/soarqin/DSP_Mods.git
synced 2026-08-05 16:50:21 +08:00
fix(UXAssist): start late-discovered mod features
This commit is contained in:
@@ -11,18 +11,17 @@ namespace UXAssist.Common.ModFeatures;
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="Init"/> runs <strong>eagerly</strong> at registration time — synchronously inside
|
||||
/// <see cref="ModFeatureRegistry.Register{T}"/>, during the registering mod's BepInEx <c>Awake</c> phase.
|
||||
/// It therefore completes <em>before</em> the game scene loads, <em>before</em> any game object's
|
||||
/// <c>Start</c>, and <em>before</em> the host mod's <see cref="Start"/>. This is the only phase where
|
||||
/// early setup that must precede game initialization (e.g. keybind registration via CommonAPI's
|
||||
/// <see cref="ModFeatureRegistry.Register{T}"/>, normally during the registering mod's BepInEx
|
||||
/// <c>Awake</c> phase. It is the phase for early setup such as keybind registration via CommonAPI's
|
||||
/// <c>CustomKeyBindSystem</c>, whose registered bindings are copied by the game's
|
||||
/// <c>UIOptionWindow._OnCreate</c> only after all plugins finish loading) can safely run. Implementations
|
||||
/// must not depend on the game being loaded here.</item>
|
||||
/// <item><see cref="Start"/> runs <strong>once</strong> during the host mod's (UXAssist) <c>Start</c>,
|
||||
/// which is guaranteed to occur after <em>every</em> mod's <c>Awake</c> has finished (BepInEx runs all
|
||||
/// plugins' <c>Awake</c> synchronously during load, before Unity dispatches any <c>Start</c>). This is
|
||||
/// the phase for activating behavior that requires the game/runtime to be ready. It is driven solely by
|
||||
/// UXAssist; dependent mods must not start features themselves.</item>
|
||||
/// <c>UIOptionWindow._OnCreate</c> only after all plugins finish loading. A late-discovered feature may
|
||||
/// initialize after the host lifecycle has begun, so implementations must not depend on either the game
|
||||
/// being loaded or unloaded here.</item>
|
||||
/// <item><see cref="Start"/> runs <strong>once</strong> when UXAssist starts the deferred lifecycle.
|
||||
/// This is normally during the host mod's <c>Start</c>; if a feature is registered afterwards, the registry
|
||||
/// starts that feature immediately after <see cref="Init"/>. This is the phase for activating behavior that
|
||||
/// requires the game/runtime to be ready. It is driven solely by UXAssist; dependent mods must not start
|
||||
/// features themselves.</item>
|
||||
/// <item><see cref="Uninit"/> runs during the host's teardown (<c>OnDestroy</c>) and resets the feature
|
||||
/// so it could be started again.</item>
|
||||
/// <item><see cref="OnInputUpdate"/> and <see cref="OnUpdate"/> are called every frame by UXAssist; the
|
||||
@@ -36,18 +35,18 @@ namespace UXAssist.Common.ModFeatures;
|
||||
public interface IModFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// Called eagerly at registration time, during the registering mod's <c>Awake</c> phase, before the
|
||||
/// game scene loads and before any plugin's <see cref="Start"/>. Use this for early setup that must
|
||||
/// precede game initialization (e.g. keybind registration). Do not depend on the game being loaded
|
||||
/// here. Runs at most once per registration.
|
||||
/// Called eagerly at registration time, normally during the registering mod's <c>Awake</c> phase.
|
||||
/// Use this for early setup such as keybind registration. A late-discovered feature may initialize
|
||||
/// after the host lifecycle has begun, so do not depend on the game being loaded or unloaded here.
|
||||
/// Runs at most once per registration.
|
||||
/// </summary>
|
||||
void Init();
|
||||
|
||||
/// <summary>
|
||||
/// Called once during the host mod's (UXAssist) <c>Start</c>, after all mods have finished
|
||||
/// <c>Awake</c> (and thus after every feature's <see cref="Init"/>). Use this to activate behavior
|
||||
/// that requires the game/runtime to be ready. Driven solely by UXAssist; runs at most once
|
||||
/// (a repeated driver call is a no-op for an already-started feature).
|
||||
/// Called once when UXAssist starts the deferred lifecycle. If this feature is registered after the
|
||||
/// lifecycle has already started, the registry invokes this immediately after <see cref="Init"/>. Use
|
||||
/// this to activate behavior that requires the game/runtime to be ready. Driven solely by UXAssist;
|
||||
/// runs at most once (a repeated driver call is a no-op for an already-started feature).
|
||||
/// </summary>
|
||||
void Start();
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ namespace UXAssist.Common.ModFeatures;
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <strong>Timing contract</strong> (same as <see cref="IModFeature"/>): <c>Init</c> runs eagerly at
|
||||
/// discovery time, synchronously inside <see cref="ModFeatureRegistry.Discover"/>, during the
|
||||
/// registering mod's BepInEx <c>Awake</c> phase — before the game scene loads, before any plugin's
|
||||
/// <c>Start</c>. This is the phase where early setup that must precede game initialization (e.g.
|
||||
/// keybind registration) must run. <c>Start</c> runs once during the host mod's (UXAssist) <c>Start</c>,
|
||||
/// after all mods' <c>Awake</c> have completed. The per-frame methods are called by UXAssist with at
|
||||
/// most one invocation per frame.
|
||||
/// discovery time, synchronously inside <see cref="ModFeatureRegistry.Discover"/>, normally during the
|
||||
/// registering mod's BepInEx <c>Awake</c> phase. This is the phase where early setup such as keybind
|
||||
/// registration must run. A late-discovered feature can initialize after the host lifecycle has begun.
|
||||
/// <c>Start</c> runs once during the host mod's (UXAssist) <c>Start</c>.
|
||||
/// If a feature is discovered after that lifecycle has already begun, the registry starts it immediately
|
||||
/// after <c>Init</c>. The per-frame methods are called by UXAssist with at most one invocation per frame.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
|
||||
@@ -19,7 +19,9 @@ namespace UXAssist.Common.ModFeatures;
|
||||
/// <see cref="OnUpdateAll"/>). <see cref="Init"/> runs eagerly when a feature is registered via
|
||||
/// <see cref="Discover"/>/<see cref="Register{T}"/>, preserving the original BepInEx <c>Awake</c> timing
|
||||
/// that keybind registration and other early setup depend on (the game's <c>UIOptionWindow._OnCreate</c>
|
||||
/// copies registered keybinds, which happens only after all plugins have finished loading).
|
||||
/// copies registered keybinds, which happens only after all plugins have finished loading). If a
|
||||
/// dependent feature is discovered after the host has started the deferred lifecycle, the registry starts
|
||||
/// that feature immediately after its eager initialization so it cannot miss the lifecycle.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The deferred dispatchers are <c>internal</c> to enforce host-only driving at compile time (no
|
||||
@@ -73,6 +75,7 @@ public static class ModFeatureRegistry
|
||||
private static readonly HashSet<Type> _registeredInstanceTypes = [];
|
||||
private static readonly HashSet<Assembly> _discoveredAssemblies = [];
|
||||
|
||||
private static bool _deferredLifecycleStarted;
|
||||
private static int _lastInputUpdateFrame = -1;
|
||||
private static int _lastUpdateFrame = -1;
|
||||
|
||||
@@ -80,7 +83,8 @@ public static class ModFeatureRegistry
|
||||
/// Discovers mod feature classes marked with <see cref="ModFeatureAttribute"/> in the given assembly,
|
||||
/// and initializes each one immediately (calling its static <c>Init</c> method if present). Each
|
||||
/// assembly is only discovered once. Dependent mods call this in their <c>Awake</c>; the host
|
||||
/// (UXAssist) drives the deferred lifecycle phases (<see cref="StartAll"/> etc.).
|
||||
/// (UXAssist) drives the deferred lifecycle phases (<see cref="StartAll"/> etc.). If discovery
|
||||
/// occurs after the host has started the deferred lifecycle, the new feature also starts immediately.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to scan.</param>
|
||||
public static void Discover(Assembly assembly)
|
||||
@@ -101,6 +105,7 @@ public static class ModFeatureRegistry
|
||||
// Init eagerly at registration time, preserving the original Awake-phase timing that
|
||||
// keybind registration and other early setup rely on.
|
||||
InitStatic(type);
|
||||
StartIfDeferredLifecycleStarted(feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +113,8 @@ public static class ModFeatureRegistry
|
||||
/// <summary>
|
||||
/// Registers a new instance mod feature, initializing it immediately. If an instance of the same
|
||||
/// type is already registered, this is a no-op. Dependent mods call this in their <c>Awake</c>; the
|
||||
/// host (UXAssist) drives the deferred lifecycle phases.
|
||||
/// host (UXAssist) drives the deferred lifecycle phases. A feature registered after that lifecycle
|
||||
/// has started is also started immediately.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The mod feature type to register.</typeparam>
|
||||
public static void Register<T>() where T : class, IModFeature, new()
|
||||
@@ -117,30 +123,24 @@ public static class ModFeatureRegistry
|
||||
if (!_registeredInstanceTypes.Add(type)) return;
|
||||
|
||||
var instance = new T();
|
||||
_instanceFeatures.Add(new InstanceFeature(instance));
|
||||
var feature = new InstanceFeature(instance);
|
||||
_instanceFeatures.Add(feature);
|
||||
// Init eagerly at registration time, preserving the original Awake-phase timing.
|
||||
instance.Init();
|
||||
StartIfDeferredLifecycleStarted(feature);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="IModFeature.Start"/> on all registered instance features
|
||||
/// and invokes the cached static <c>Start</c> methods on all discovered mod feature classes.
|
||||
/// Each feature is started at most once; subsequent calls are no-ops for already-started features.
|
||||
/// Features registered after this lifecycle has started are started immediately by the registry.
|
||||
/// </summary>
|
||||
internal static void StartAll()
|
||||
{
|
||||
foreach (var f in _staticFeatures)
|
||||
{
|
||||
if (f.Started) continue;
|
||||
f.Start?.Invoke();
|
||||
f.Started = true;
|
||||
}
|
||||
foreach (var f in _instanceFeatures)
|
||||
{
|
||||
if (f.Started) continue;
|
||||
f.Feature.Start();
|
||||
f.Started = true;
|
||||
}
|
||||
_deferredLifecycleStarted = true;
|
||||
foreach (var f in _staticFeatures) Start(f);
|
||||
foreach (var f in _instanceFeatures) Start(f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -150,6 +150,7 @@ public static class ModFeatureRegistry
|
||||
/// </summary>
|
||||
internal static void UninitAll()
|
||||
{
|
||||
_deferredLifecycleStarted = false;
|
||||
foreach (var f in _staticFeatures)
|
||||
{
|
||||
f.Uninit?.Invoke();
|
||||
@@ -192,6 +193,30 @@ public static class ModFeatureRegistry
|
||||
foreach (var f in _instanceFeatures) f.Feature.OnUpdate();
|
||||
}
|
||||
|
||||
private static void StartIfDeferredLifecycleStarted(StaticFeature feature)
|
||||
{
|
||||
if (_deferredLifecycleStarted) Start(feature);
|
||||
}
|
||||
|
||||
private static void StartIfDeferredLifecycleStarted(InstanceFeature feature)
|
||||
{
|
||||
if (_deferredLifecycleStarted) Start(feature);
|
||||
}
|
||||
|
||||
private static void Start(StaticFeature feature)
|
||||
{
|
||||
if (feature.Started) return;
|
||||
feature.Start?.Invoke();
|
||||
feature.Started = true;
|
||||
}
|
||||
|
||||
private static void Start(InstanceFeature feature)
|
||||
{
|
||||
if (feature.Started) return;
|
||||
feature.Feature.Start();
|
||||
feature.Started = true;
|
||||
}
|
||||
|
||||
private static void InitStatic(Type type)
|
||||
{
|
||||
var init = type.GetMethod("Init",
|
||||
|
||||
Reference in New Issue
Block a user