mirror of
https://github.com/soarqin/DSP_Mods.git
synced 2026-08-05 09:40:18 +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:
@@ -142,6 +142,7 @@ The sync is implemented as an inline PowerShell `Exec` step inside the `ZipMod`
|
|||||||
## Key Architectural Patterns
|
## Key Architectural Patterns
|
||||||
|
|
||||||
- **Shared library:** `UXAssist` acts as a common library. `CheatEnabler` and `UniverseGenTweaks` reference `UXAssist.csproj` directly to reuse `Common/`, `UI/`, and config panel infrastructure.
|
- **Shared library:** `UXAssist` acts as a common library. `CheatEnabler` and `UniverseGenTweaks` reference `UXAssist.csproj` directly to reuse `Common/`, `UI/`, and config panel infrastructure.
|
||||||
|
- **Centralized mod-feature lifecycle:** `UXAssist.Common.ModFeatures.ModFeatureRegistry` holds shared static lists of mod features discovered across all mods. **Only UXAssist drives the shared deferred lifecycle** (`StartAll`/`UninitAll`/`OnInputUpdateAll`/`OnUpdateAll`); these dispatchers are `internal` so dependent mods (separate assemblies, no `InternalsVisibleTo`) cannot call them and re-trigger other mods' features. A feature's `Init` runs **eagerly** when it 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 finished loading. Dependent mods only call `ModFeatureRegistry.Discover(Assembly.GetExecutingAssembly())` (and optionally `Register<T>()`) in their `Awake`. UXAssist defers `StartAll` to its own `Start`, so all dependents have registered (and initialized) first (BepInEx runs every plugin's `Awake` before any plugin's `Start`). The registry also guards start idempotency per feature (start at most once; uninit resets) and per-frame re-entrancy (`Time.frameCount`) for the update dispatchers, as defense-in-depth.
|
||||||
- **Preloader pattern:** `DustbinPreloader` and `LabOptPreloader` use Mono.Cecil to inject new fields into game assemblies at BepInEx preload time, enabling their corresponding main mods to read/write those fields via normal C# without reflection.
|
- **Preloader pattern:** `DustbinPreloader` and `LabOptPreloader` use Mono.Cecil to inject new fields into game assemblies at BepInEx preload time, enabling their corresponding main mods to read/write those fields via normal C# without reflection.
|
||||||
- **Internationalization:** `UXAssist/Common/I18N.cs` provides bilingual (EN + ZH) string lookup used across UXAssist and CheatEnabler. Localization keys are declared as `public const string` in per-project registration classes (`UXAssist/Common/I18NKeys.cs`, `CheatEnabler/Localization.cs`, `UniverseGenTweaks/Localization.cs`) and registered through a single `Register()` call from each mod's `Awake()`. Do not pass Chinese string literals to `.Translate()` at call sites.
|
- **Internationalization:** `UXAssist/Common/I18N.cs` provides bilingual (EN + ZH) string lookup used across UXAssist and CheatEnabler. Localization keys are declared as `public const string` in per-project registration classes (`UXAssist/Common/I18NKeys.cs`, `CheatEnabler/Localization.cs`, `UniverseGenTweaks/Localization.cs`) and registered through a single `Register()` call from each mod's `Awake()`. Do not pass Chinese string literals to `.Translate()` at call sites.
|
||||||
- **Centralized game constants:** Hard-coded item IDs, tech IDs, logistics capacities, and Dyson sphere geometry defaults live in `UXAssist/Common/GameConstants` (`ItemIds`, `TechIds`, `LogisticsConstants`, `DysonSphereConstants`). Prefer these constants over inline literals in UXAssist patches.
|
- **Centralized game constants:** Hard-coded item IDs, tech IDs, logistics capacities, and Dyson sphere geometry defaults live in `UXAssist/Common/GameConstants` (`ItemIds`, `TechIds`, `LogisticsConstants`, `DysonSphereConstants`). Prefer these constants over inline literals in UXAssist patches.
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
using System;
|
using System.Reflection;
|
||||||
using System.Reflection;
|
|
||||||
using BepInEx;
|
using BepInEx;
|
||||||
using CheatEnabler.Patches;
|
using CheatEnabler.Patches;
|
||||||
using CheatEnabler.Patches.Factory;
|
using CheatEnabler.Patches.Factory;
|
||||||
using HarmonyLib;
|
|
||||||
using UXAssist.Common;
|
using UXAssist.Common;
|
||||||
using UXAssist.Common.ModFeatures;
|
using UXAssist.Common.ModFeatures;
|
||||||
|
|
||||||
@@ -101,25 +99,9 @@ public class CheatEnabler : BaseUnityPlugin
|
|||||||
"Buildings invincible");
|
"Buildings invincible");
|
||||||
Localization.Register();
|
Localization.Register();
|
||||||
UIConfigWindow.Init();
|
UIConfigWindow.Init();
|
||||||
|
// Register features (Init runs eagerly here); UXAssist drives the deferred lifecycle (Start/Uninit/Update).
|
||||||
ModFeatureRegistry.Discover(Assembly.GetExecutingAssembly());
|
ModFeatureRegistry.Discover(Assembly.GetExecutingAssembly());
|
||||||
ModFeatureRegistry.InitAll();
|
|
||||||
|
|
||||||
I18N.Apply();
|
I18N.Apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
ModFeatureRegistry.StartAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDestroy()
|
|
||||||
{
|
|
||||||
ModFeatureRegistry.UninitAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Update()
|
|
||||||
{
|
|
||||||
if (VFInput.inputing) return;
|
|
||||||
ModFeatureRegistry.OnInputUpdateAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,32 +2,71 @@ namespace UXAssist.Common.ModFeatures;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface implemented by instance mod features registered with <see cref="ModFeatureRegistry"/>.
|
/// Interface implemented by instance mod features registered with <see cref="ModFeatureRegistry"/>.
|
||||||
/// All lifecycle methods are invoked by the registry in the order described below.
|
/// The registry drives the lifecycle; individual mods never call these methods directly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <strong>Timing contract.</strong> The registry guarantees the following relative ordering, which
|
||||||
|
/// feature implementations may rely on:
|
||||||
|
/// </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
|
||||||
|
/// <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>
|
||||||
|
/// <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
|
||||||
|
/// registry guarantees at most one invocation per frame even if multiple drivers exist.</item>
|
||||||
|
/// </list>
|
||||||
|
/// <para>
|
||||||
|
/// The same timing contract applies to static features discovered via <see cref="ModFeatureAttribute"/>;
|
||||||
|
/// see that attribute for details.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
public interface IModFeature
|
public interface IModFeature
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called once when the mod is initialized.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Init();
|
void Init();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called once after initialization, when the mod should begin active behavior.
|
/// 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).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Start();
|
void Start();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called once when the mod is being shut down or re-initialized.
|
/// Called during the host's teardown (<c>OnDestroy</c>). Reset any state created in
|
||||||
|
/// <see cref="Init"/>/<see cref="Start"/> so the feature could be re-started. Runs on every
|
||||||
|
/// registered feature.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Uninit();
|
void Uninit();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called every frame for input handling. Should be lightweight.
|
/// Called every frame for input handling. Should be lightweight. The registry guarantees at most
|
||||||
|
/// one invocation per frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void OnInputUpdate();
|
void OnInputUpdate();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called every frame for general updates. Should be lightweight.
|
/// Called every frame for general updates. Should be lightweight. The registry guarantees at most
|
||||||
|
/// one invocation per frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void OnUpdate();
|
void OnUpdate();
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,20 @@ namespace UXAssist.Common.ModFeatures;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks a class as a mod feature so that <see cref="ModFeatureRegistry"/> can discover it.
|
/// 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.
|
/// 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>
|
/// </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)]
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
public sealed class ModFeatureAttribute : Attribute
|
public sealed class ModFeatureAttribute : Attribute
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
namespace UXAssist.Common.ModFeatures;
|
namespace UXAssist.Common.ModFeatures;
|
||||||
|
|
||||||
@@ -10,21 +11,36 @@ namespace UXAssist.Common.ModFeatures;
|
|||||||
/// and holds registered instance mod features. Discovered features invoke static lifecycle methods;
|
/// and holds registered instance mod features. Discovered features invoke static lifecycle methods;
|
||||||
/// static lifecycle methods are optional and are skipped if missing.
|
/// static lifecycle methods are optional and are skipped if missing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The registry uses shared static collections that accumulate features from all mods. To avoid duplicate
|
||||||
|
/// lifecycle execution, <strong>only UXAssist</strong> (the host mod) drives the deferred lifecycle phases
|
||||||
|
/// (<see cref="StartAll"/>, <see cref="UninitAll"/>, <see cref="OnInputUpdateAll"/>,
|
||||||
|
/// <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).
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// The deferred dispatchers are <c>internal</c> to enforce host-only driving at compile time (no
|
||||||
|
/// <c>InternalsVisibleTo</c> is declared, so cross-assembly callers are rejected by the compiler), and
|
||||||
|
/// each carries runtime idempotency / per-frame guards as defense-in-depth.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
public static class ModFeatureRegistry
|
public static class ModFeatureRegistry
|
||||||
{
|
{
|
||||||
private sealed class StaticFeature
|
private sealed class StaticFeature
|
||||||
{
|
{
|
||||||
public Type Type { get; }
|
public Type Type { get; }
|
||||||
public Action Init { get; }
|
|
||||||
public Action Start { get; }
|
public Action Start { get; }
|
||||||
public Action Uninit { get; }
|
public Action Uninit { get; }
|
||||||
public Action OnInputUpdate { get; }
|
public Action OnInputUpdate { get; }
|
||||||
public Action OnUpdate { get; }
|
public Action OnUpdate { get; }
|
||||||
|
public bool Started { get; set; }
|
||||||
|
|
||||||
public StaticFeature(Type type)
|
public StaticFeature(Type type)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
Init = GetDelegate(type, "Init");
|
|
||||||
Start = GetDelegate(type, "Start");
|
Start = GetDelegate(type, "Start");
|
||||||
Uninit = GetDelegate(type, "Uninit");
|
Uninit = GetDelegate(type, "Uninit");
|
||||||
OnInputUpdate = GetDelegate(type, "OnInputUpdate");
|
OnInputUpdate = GetDelegate(type, "OnInputUpdate");
|
||||||
@@ -41,14 +57,30 @@ public static class ModFeatureRegistry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class InstanceFeature
|
||||||
|
{
|
||||||
|
public IModFeature Feature { get; }
|
||||||
|
public bool Started { get; set; }
|
||||||
|
|
||||||
|
public InstanceFeature(IModFeature feature)
|
||||||
|
{
|
||||||
|
Feature = feature;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly List<StaticFeature> _staticFeatures = [];
|
private static readonly List<StaticFeature> _staticFeatures = [];
|
||||||
private static readonly List<IModFeature> _instanceFeatures = [];
|
private static readonly List<InstanceFeature> _instanceFeatures = [];
|
||||||
private static readonly HashSet<Type> _registeredInstanceTypes = [];
|
private static readonly HashSet<Type> _registeredInstanceTypes = [];
|
||||||
private static readonly HashSet<Assembly> _discoveredAssemblies = [];
|
private static readonly HashSet<Assembly> _discoveredAssemblies = [];
|
||||||
|
|
||||||
|
private static int _lastInputUpdateFrame = -1;
|
||||||
|
private static int _lastUpdateFrame = -1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Discovers mod feature classes marked with <see cref="ModFeatureAttribute"/> in the given assembly.
|
/// Discovers mod feature classes marked with <see cref="ModFeatureAttribute"/> in the given assembly,
|
||||||
/// Each assembly is only discovered once.
|
/// 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.).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="assembly">The assembly to scan.</param>
|
/// <param name="assembly">The assembly to scan.</param>
|
||||||
public static void Discover(Assembly assembly)
|
public static void Discover(Assembly assembly)
|
||||||
@@ -63,12 +95,20 @@ public static class ModFeatureRegistry
|
|||||||
foreach (var type in staticTypes.OrderBy(GetOrder))
|
foreach (var type in staticTypes.OrderBy(GetOrder))
|
||||||
{
|
{
|
||||||
if (_staticFeatures.All(f => f.Type != type))
|
if (_staticFeatures.All(f => f.Type != type))
|
||||||
_staticFeatures.Add(new StaticFeature(type));
|
{
|
||||||
|
var feature = new StaticFeature(type);
|
||||||
|
_staticFeatures.Add(feature);
|
||||||
|
// Init eagerly at registration time, preserving the original Awake-phase timing that
|
||||||
|
// keybind registration and other early setup rely on.
|
||||||
|
InitStatic(type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers a new instance mod feature if an instance of the same type is not already registered.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The mod feature type to register.</typeparam>
|
/// <typeparam name="T">The mod feature type to register.</typeparam>
|
||||||
public static void Register<T>() where T : class, IModFeature, new()
|
public static void Register<T>() where T : class, IModFeature, new()
|
||||||
@@ -77,59 +117,87 @@ public static class ModFeatureRegistry
|
|||||||
if (!_registeredInstanceTypes.Add(type)) return;
|
if (!_registeredInstanceTypes.Add(type)) return;
|
||||||
|
|
||||||
var instance = new T();
|
var instance = new T();
|
||||||
_instanceFeatures.Add(instance);
|
_instanceFeatures.Add(new InstanceFeature(instance));
|
||||||
}
|
// Init eagerly at registration time, preserving the original Awake-phase timing.
|
||||||
|
instance.Init();
|
||||||
/// <summary>
|
|
||||||
/// Calls <see cref="IModFeature.Init"/> on all registered instance features
|
|
||||||
/// and invokes the cached static <c>Init</c> methods on all discovered mod feature classes.
|
|
||||||
/// </summary>
|
|
||||||
public static void InitAll()
|
|
||||||
{
|
|
||||||
foreach (var f in _staticFeatures) f.Init?.Invoke();
|
|
||||||
foreach (var f in _instanceFeatures) f.Init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calls <see cref="IModFeature.Start"/> on all registered instance features
|
/// 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.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void StartAll()
|
internal static void StartAll()
|
||||||
{
|
{
|
||||||
foreach (var f in _staticFeatures) f.Start?.Invoke();
|
foreach (var f in _staticFeatures)
|
||||||
foreach (var f in _instanceFeatures) f.Start();
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calls <see cref="IModFeature.Uninit"/> on all registered instance features
|
/// Calls <see cref="IModFeature.Uninit"/> on all registered instance features
|
||||||
/// and invokes the cached static <c>Uninit</c> methods on all discovered mod feature classes.
|
/// and invokes the cached static <c>Uninit</c> methods on all discovered mod feature classes,
|
||||||
|
/// then resets their state so they can be re-started.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void UninitAll()
|
internal static void UninitAll()
|
||||||
{
|
{
|
||||||
foreach (var f in _staticFeatures) f.Uninit?.Invoke();
|
foreach (var f in _staticFeatures)
|
||||||
foreach (var f in _instanceFeatures) f.Uninit();
|
{
|
||||||
|
f.Uninit?.Invoke();
|
||||||
|
f.Started = false;
|
||||||
|
}
|
||||||
|
foreach (var f in _instanceFeatures)
|
||||||
|
{
|
||||||
|
f.Feature.Uninit();
|
||||||
|
f.Started = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calls <see cref="IModFeature.OnInputUpdate"/> on all registered instance features
|
/// Calls <see cref="IModFeature.OnInputUpdate"/> on all registered instance features
|
||||||
/// and invokes the cached static <c>OnInputUpdate</c> delegates on all discovered mod feature classes.
|
/// and invokes the cached static <c>OnInputUpdate</c> delegates on all discovered mod feature classes.
|
||||||
/// This method is meant to be called every frame; no reflection is performed here.
|
/// This method is meant to be called every frame; no reflection is performed here.
|
||||||
|
/// Guarded per-frame to prevent duplicate execution within the same frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void OnInputUpdateAll()
|
internal static void OnInputUpdateAll()
|
||||||
{
|
{
|
||||||
|
var frame = Time.frameCount;
|
||||||
|
if (frame == _lastInputUpdateFrame) return;
|
||||||
|
_lastInputUpdateFrame = frame;
|
||||||
foreach (var f in _staticFeatures) f.OnInputUpdate?.Invoke();
|
foreach (var f in _staticFeatures) f.OnInputUpdate?.Invoke();
|
||||||
foreach (var f in _instanceFeatures) f.OnInputUpdate();
|
foreach (var f in _instanceFeatures) f.Feature.OnInputUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calls <see cref="IModFeature.OnUpdate"/> on all registered instance features
|
/// Calls <see cref="IModFeature.OnUpdate"/> on all registered instance features
|
||||||
/// and invokes the cached static <c>OnUpdate</c> delegates on all discovered mod feature classes.
|
/// and invokes the cached static <c>OnUpdate</c> delegates on all discovered mod feature classes.
|
||||||
/// This method is meant to be called every frame; no reflection is performed here.
|
/// This method is meant to be called every frame; no reflection is performed here.
|
||||||
|
/// Guarded per-frame to prevent duplicate execution within the same frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void OnUpdateAll()
|
internal static void OnUpdateAll()
|
||||||
{
|
{
|
||||||
|
var frame = Time.frameCount;
|
||||||
|
if (frame == _lastUpdateFrame) return;
|
||||||
|
_lastUpdateFrame = frame;
|
||||||
foreach (var f in _staticFeatures) f.OnUpdate?.Invoke();
|
foreach (var f in _staticFeatures) f.OnUpdate?.Invoke();
|
||||||
foreach (var f in _instanceFeatures) f.OnUpdate();
|
foreach (var f in _instanceFeatures) f.Feature.OnUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InitStatic(Type type)
|
||||||
|
{
|
||||||
|
var init = type.GetMethod("Init",
|
||||||
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static,
|
||||||
|
null, Type.EmptyTypes, null);
|
||||||
|
init?.Invoke(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetOrder(Type type)
|
private static int GetOrder(Type type)
|
||||||
|
|||||||
@@ -251,8 +251,11 @@ public class UXAssist : BaseUnityPlugin, IModCanSave
|
|||||||
object[] parameters = [_harmony];
|
object[] parameters = [_harmony];
|
||||||
_compats?.Do(type => type.GetMethod("Init")?.Invoke(null, parameters));
|
_compats?.Do(type => type.GetMethod("Init")?.Invoke(null, parameters));
|
||||||
|
|
||||||
|
// Register UXAssist's own features (Init runs eagerly here, preserving the original Awake timing
|
||||||
|
// that keybind registration relies on). Dependent mods register theirs during their own Awake
|
||||||
|
// phase. Start is deferred to UXAssist.Start below so that all dependents have registered before
|
||||||
|
// any feature starts.
|
||||||
ModFeatureRegistry.Discover(Assembly.GetExecutingAssembly());
|
ModFeatureRegistry.Discover(Assembly.GetExecutingAssembly());
|
||||||
ModFeatureRegistry.InitAll();
|
|
||||||
|
|
||||||
I18N.Apply();
|
I18N.Apply();
|
||||||
}
|
}
|
||||||
@@ -262,6 +265,8 @@ public class UXAssist : BaseUnityPlugin, IModCanSave
|
|||||||
MyWindowManager.InitBaseObjects();
|
MyWindowManager.InitBaseObjects();
|
||||||
MyWindowManager.Enable(true);
|
MyWindowManager.Enable(true);
|
||||||
|
|
||||||
|
// UXAssist is the sole lifecycle driver. All dependents have already registered (and initialized)
|
||||||
|
// their features during their Awake phase (BepInEx runs every plugin's Awake before any plugin's Start).
|
||||||
ModFeatureRegistry.StartAll();
|
ModFeatureRegistry.StartAll();
|
||||||
|
|
||||||
_patches?.Do(type => type.GetMethod("Start")?.Invoke(null, null));
|
_patches?.Do(type => type.GetMethod("Start")?.Invoke(null, null));
|
||||||
|
|||||||
@@ -58,17 +58,12 @@ public class UniverseGenTweaks : BaseUnityPlugin, IModCanSave
|
|||||||
|
|
||||||
Localization.Register();
|
Localization.Register();
|
||||||
UIConfigWindow.Init();
|
UIConfigWindow.Init();
|
||||||
|
// Register features (Init runs eagerly here); UXAssist drives the deferred lifecycle (Start/Uninit/Update).
|
||||||
ModFeatureRegistry.Discover(Assembly.GetExecutingAssembly());
|
ModFeatureRegistry.Discover(Assembly.GetExecutingAssembly());
|
||||||
ModFeatureRegistry.InitAll();
|
|
||||||
|
|
||||||
I18N.Apply();
|
I18N.Apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
|
||||||
{
|
|
||||||
ModFeatureRegistry.UninitAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
#region IModCanSave
|
#region IModCanSave
|
||||||
private const ushort ModSaveVersion = 1;
|
private const ushort ModSaveVersion = 1;
|
||||||
|
|
||||||
|
|||||||
@@ -250,13 +250,21 @@ public interface IModFeature
|
|||||||
public static class ModFeatureRegistry
|
public static class ModFeatureRegistry
|
||||||
```
|
```
|
||||||
|
|
||||||
- `public static void Discover(Assembly assembly)`
|
- `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()`
|
- `public static void Register<T>() where T : class, IModFeature, new()` — register + eagerly `Init` an instance feature
|
||||||
- `public static void InitAll()`
|
- `internal static void StartAll()` — host-only deferred lifecycle driver (UXAssist), idempotent per feature
|
||||||
- `public static void StartAll()`
|
- `internal static void UninitAll()` — host-only lifecycle driver (UXAssist)
|
||||||
- `public static void UninitAll()`
|
- `internal static void OnInputUpdateAll()` — host-only per-frame driver (UXAssist), guarded per-frame
|
||||||
- `public static void OnInputUpdateAll()`
|
- `internal static void OnUpdateAll()` — host-only per-frame driver (UXAssist), guarded per-frame
|
||||||
- `public static void OnUpdateAll()`
|
|
||||||
|
> `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`
|
### `UXAssist.Common.Config`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user