mirror of
https://github.com/soarqin/DSP_Mods.git
synced 2026-08-05 10:00:21 +08:00
fix(UXAssist): centralize mod-feature lifecycle to prevent duplicate execution
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.
This commit is contained in:
@@ -250,13 +250,21 @@ public interface IModFeature
|
||||
public static class ModFeatureRegistry
|
||||
```
|
||||
|
||||
- `public static void Discover(Assembly assembly)`
|
||||
- `public static void Register<T>() where T : class, IModFeature, new()`
|
||||
- `public static void InitAll()`
|
||||
- `public static void StartAll()`
|
||||
- `public static void UninitAll()`
|
||||
- `public static void OnInputUpdateAll()`
|
||||
- `public static void OnUpdateAll()`
|
||||
- `public static void Discover(Assembly assembly)` — register + eagerly `Init` features; called by every mod (host + dependents) in `Awake`
|
||||
- `public static void Register<T>() where T : class, IModFeature, new()` — register + eagerly `Init` an instance feature
|
||||
- `internal static void StartAll()` — host-only deferred lifecycle driver (UXAssist), idempotent per feature
|
||||
- `internal static void UninitAll()` — host-only lifecycle driver (UXAssist)
|
||||
- `internal static void OnInputUpdateAll()` — host-only per-frame driver (UXAssist), guarded per-frame
|
||||
- `internal static void OnUpdateAll()` — host-only per-frame driver (UXAssist), guarded per-frame
|
||||
|
||||
> `Init` runs eagerly when a feature is registered (via `Discover`/`Register`), preserving the original
|
||||
> `Awake`-phase timing that keybind registration and other early setup rely on (the game's
|
||||
> `UIOptionWindow._OnCreate` copies registered keybinds only after all plugins have loaded). `Start` is
|
||||
> the only deferred phase: UXAssist calls `StartAll` in its own `Start` so all dependents have registered
|
||||
> first (BepInEx runs every plugin's `Awake` before any plugin's `Start`). The deferred dispatchers are
|
||||
> `internal` so only UXAssist (the host, same assembly) can drive them; no `InternalsVisibleTo` is
|
||||
> declared, so cross-assembly calls are rejected at compile time. Runtime idempotency (per-feature
|
||||
> start-once) and per-frame re-entrancy guards (`Time.frameCount`) act as defense-in-depth.
|
||||
|
||||
### `UXAssist.Common.Config`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user