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:
2026-06-29 02:56:54 +08:00
parent c2016909ff
commit ab1d20683d
8 changed files with 181 additions and 71 deletions
+2 -20
View File
@@ -1,9 +1,7 @@
using System;
using System.Reflection;
using System.Reflection;
using BepInEx;
using CheatEnabler.Patches;
using CheatEnabler.Patches.Factory;
using HarmonyLib;
using UXAssist.Common;
using UXAssist.Common.ModFeatures;
@@ -101,25 +99,9 @@ public class CheatEnabler : BaseUnityPlugin
"Buildings invincible");
Localization.Register();
UIConfigWindow.Init();
// Register features (Init runs eagerly here); UXAssist drives the deferred lifecycle (Start/Uninit/Update).
ModFeatureRegistry.Discover(Assembly.GetExecutingAssembly());
ModFeatureRegistry.InitAll();
I18N.Apply();
}
private void Start()
{
ModFeatureRegistry.StartAll();
}
private void OnDestroy()
{
ModFeatureRegistry.UninitAll();
}
private void Update()
{
if (VFInput.inputing) return;
ModFeatureRegistry.OnInputUpdateAll();
}
}