mirror of
https://github.com/soarqin/DSP_Mods.git
synced 2026-08-05 07:40:12 +08:00
ModFeatureRegistry is a static class with shared collections accumulating features from all mods. Dependent mods (CheatEnabler, UniverseGenTweaks) each independently called InitAll/StartAll/OnInputUpdateAll/OnUpdateAll/ UninitAll, re-running lifecycle for ALL accumulated features including other mods' — per-frame Update ran 2-3x (breaking CheatEnabler key toggles to net no-ops), Init/Start/Uninit ran 2-3x (double RegisterExporter causing save corruption, double SettingChanged subscriptions, double keybind registration). Fix: - Init now runs eagerly at Discover/Register time (Awake-phase), preserving the original timing that keybind registration depends on (game's UIOptionWindow._OnCreate copies keybinds only after all plugins load) - InitAll removed entirely - StartAll/UninitAll/OnInputUpdateAll/OnUpdateAll made internal so only UXAssist (host, same assembly, no InternalsVisibleTo) can drive them - Start deferred to UXAssist.Start (after all dependents' Awake complete; BepInEx runs all Awakes before any Start) - Per-feature Started idempotency + per-frame Time.frameCount guards as defense-in-depth - Dependent mods reduced to Discover-only in Awake Document the Init/Start timing contract on IModFeature and ModFeatureAttribute so future mods can rely on it.
43 lines
1.6 KiB
C#
43 lines
1.6 KiB
C#
using System;
|
|
|
|
namespace UXAssist.Common.ModFeatures;
|
|
|
|
/// <summary>
|
|
/// Marks a class as a mod feature so that <see cref="ModFeatureRegistry"/> can discover it.
|
|
/// Lifecycle methods (<c>Init</c>, <c>Start</c>, <c>Uninit</c>, <c>OnInputUpdate</c>, <c>OnUpdate</c>)
|
|
/// are optional and are skipped if missing.
|
|
/// </summary>
|
|
/// <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.
|
|
/// </para>
|
|
/// </remarks>
|
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
|
public sealed class ModFeatureAttribute : Attribute
|
|
{
|
|
/// <summary>
|
|
/// Optional display name of the feature.
|
|
/// </summary>
|
|
public string Name { get; }
|
|
|
|
/// <summary>
|
|
/// Execution order among discovered static features. Lower values run first.
|
|
/// </summary>
|
|
public int Order { get; set; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ModFeatureAttribute"/> class.
|
|
/// </summary>
|
|
/// <param name="name">Optional display name of the feature.</param>
|
|
public ModFeatureAttribute(string name = null)
|
|
{
|
|
Name = name;
|
|
}
|
|
}
|