Files
DSP_Mods/docs/superpowers/plans/2026-06-23-refactor-uxassist-cheate-universe.md
T

40 KiB

UXAssist / CheatEnabler / UniverseGenTweaks Refactor Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Improve readability and maintainability of UXAssist, CheatEnabler, and UniverseGenTweaks by splitting oversized files, centralizing constants/localization, cleaning static mutable state, and hardening transpilers. Only UXAssist.UI and UXAssist.Common public APIs are treated as a stable contract; everything else may be refactored freely, and downstream projects are updated to consume the new structure.

Architecture: Keep the existing BepInEx + Harmony stack. Introduce a reusable, public mod-feature lifecycle abstraction in UXAssist.Common.ModFeatures that replaces the current namespace-based reflection on static Functions/Patches classes. Each static feature class declares [ModFeature] and exposes Init/Start/Uninit/OnInputUpdate/OnUpdate as needed; ModFeatureRegistry.Discover finds and registers them, then drives their lifecycle. CheatEnabler and UniverseGenTweaks adopt the same abstraction, so they benefit from the same architecture rather than using InternalsVisibleTo.

Split monolithic Patches/*.cs and Functions/*.cs files into focused classes grouped by subsystem (e.g., UXAssist.Patches.Factory.*). Refactor the internals of UXAssist.UI and UXAssist.Common into smaller helpers while keeping the existing public surface intact (old members become thin forwarding facades or are marked [Obsolete]). Introduce ConfigProvider helpers and GameConstants classes to decouple UI from patch internals. Clean static state through explicit ResetState callbacks registered on GameLogic.OnGameEnd. Add version/fallback annotations to transpilers to survive game updates.

Tech Stack: C# (net472), BepInEx 5.x, HarmonyLib, SDK-style MSBuild, PowerShell Compress-Archive.


Phase 1 — Structural Split + Public Lifecycle Abstraction

Task 1: Document the public API surface that must stay stable

Files:

  • Create: docs/PublicApiSurface.md

  • Step 1: Inventory UXAssist.UI and UXAssist.Common public members

    Read every file under UXAssist/UI/ and UXAssist/Common/ and list every public class/struct/enum/delegate/method/property/event/field that is reachable from CheatEnabler or UniverseGenTweaks.

    At minimum, the list must include:

    • UXAssist.Common.I18N (Add, Apply, Translate, Init, OnInitialized)
    • UXAssist.Common.GameLogic (Enable, OnDataLoaded, OnGameBegin, OnGameEnd, OnFactoryFrameBegin)
    • UXAssist.Common.PatchImpl<T> and PatchGuidAttribute
    • UXAssist.Common.Util (GetTypesFiltered, GetTypesInNamespace, LoadEmbeddedResource, LoadEmbeddedTexture, LoadEmbeddedSprite, PluginFolder)
    • UXAssist.UI.MyConfigWindow (OnUICreated, OnUpdateUI, CreateInstance, DestroyInstance)
    • UXAssist.UI.MyWindow (InitBaseObject, Create, AddText, AddText2, AddButton, AddTipsButton, AddTipsButton2, Open, Close, TryClose, AutoFitWindowSize)
    • UXAssist.UI.MyWindowWithTabs (AddTabGroup, AddTab)
    • UXAssist.UI.MyCheckBox.CreateCheckBox
    • UXAssist.UI.MySlider.CreateSlider
    • UXAssist.UI.MyWindowManager (InitBaseObjects, Enable)
  • Step 2: Commit the inventory

    git add docs/PublicApiSurface.md
    git commit -m "docs: inventory UXAssist.UI/Common public API surface"
    

Task 2: Introduce public ModFeature lifecycle abstraction

Files:

  • Create: UXAssist/Common/ModFeatures/ModFeatureAttribute.cs

  • Create: UXAssist/Common/ModFeatures/IModFeature.cs

  • Create: UXAssist/Common/ModFeatures/ModFeatureRegistry.cs

  • Modify: UXAssist/Common/Util.cs

  • Step 1: Add namespace-prefix reflection helper

    In UXAssist/Common/Util.cs:

    public static Type[] GetTypesInNamespacePrefix(Assembly assembly, string prefix)
    {
        return GetTypesFiltered(assembly, t => t.Namespace != null && t.Namespace.StartsWith(prefix, StringComparison.Ordinal));
    }
    
  • Step 2: Define the attribute

    namespace UXAssist.Common.ModFeatures;
    
    [AttributeUsage(AttributeTargets.Class, Inherited = false)]
    public sealed class ModFeatureAttribute : Attribute
    {
        public string Name { get; }
        public int Order { get; set; }
    
        public ModFeatureAttribute(string name = null)
        {
            Name = name;
        }
    }
    
  • Step 3: Define the optional instance interface

    namespace UXAssist.Common.ModFeatures;
    
    public interface IModFeature
    {
        void Init();
        void Start();
        void Uninit();
        void OnInputUpdate();
        void OnUpdate();
    }
    
  • Step 4: Implement the registry

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    
    namespace UXAssist.Common.ModFeatures;
    
    public static class ModFeatureRegistry
    {
        private static readonly List<Type> _staticFeatures = [];
        private static readonly List<IModFeature> _instanceFeatures = [];
        private static readonly HashSet<Assembly> _discoveredAssemblies = [];
    
        public static void Discover(Assembly assembly)
        {
            if (!_discoveredAssemblies.Add(assembly)) return;
    
            var staticTypes = Util.GetTypesFiltered(assembly, t =>
                t.IsClass && t.IsAbstract && t.IsSealed &&
                Attribute.IsDefined(t, typeof(ModFeatureAttribute)));
    
            foreach (var type in staticTypes.OrderBy(GetOrder))
            {
                if (!_staticFeatures.Contains(type))
                    _staticFeatures.Add(type);
            }
        }
    
        public static void Register<T>() where T : class, IModFeature, new()
        {
            var instance = new T();
            _instanceFeatures.Add(instance);
        }
    
        public static void InitAll()
        {
            ForEachStatic("Init");
            foreach (var f in _instanceFeatures) f.Init();
        }
    
        public static void StartAll()
        {
            ForEachStatic("Start");
            foreach (var f in _instanceFeatures) f.Start();
        }
    
        public static void UninitAll()
        {
            ForEachStatic("Uninit");
            foreach (var f in _instanceFeatures) f.Uninit();
        }
    
        public static void OnInputUpdateAll()
        {
            ForEachStatic("OnInputUpdate");
            foreach (var f in _instanceFeatures) f.OnInputUpdate();
        }
    
        public static void OnUpdateAll()
        {
            ForEachStatic("OnUpdate");
            foreach (var f in _instanceFeatures) f.OnUpdate();
        }
    
        private static void ForEachStatic(string methodName)
        {
            foreach (var type in _staticFeatures)
            {
                var method = type.GetMethod(methodName,
                    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
                method?.Invoke(null, null);
            }
        }
    
        private static int GetOrder(Type type)
        {
            return type.GetCustomAttribute<ModFeatureAttribute>()?.Order ?? 0;
        }
    }
    
  • Step 5: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    

    Expected: 0 errors.

  • Step 6: Commit

    git add UXAssist/Common/ModFeatures/ UXAssist/Common/Util.cs
    git commit -m "feat(UXAssist): public ModFeature lifecycle registry"
    

Task 3: Split UXAssist/Patches/FactoryPatch.cs and mark features

Files:

  • Create: UXAssist/Patches/Factory/FactoryPatch.cs (config coordinator)

  • Create: UXAssist/Patches/Factory/ImmediateBuildPatch.cs

  • Create: UXAssist/Patches/Factory/ArchitectModePatch.cs

  • Create: UXAssist/Patches/Factory/BuildToolPatch.cs

  • Create: UXAssist/Patches/Factory/BeltSignalPatch.cs

  • Create: UXAssist/Patches/Factory/BuildingBufferPatch.cs

  • Create: UXAssist/Patches/Factory/VeinProtectionPatch.cs

  • Create: UXAssist/Patches/Factory/PowerGenerationPatch.cs

  • Create: UXAssist/Patches/Factory/RenderingPatch.cs

  • Delete: UXAssist/Patches/FactoryPatch.cs

  • Step 1: Move nested patch classes to new files

    Use git mv semantics: copy the contents of each nested PatchImpl<T> class from the old file into a new file under UXAssist/Patches/Factory/. Keep each class internal unless it must be public. Use namespace UXAssist.Patches.Factory.

  • Step 2: Create the coordinator FactoryPatch

    Mark it as a mod feature and expose the public ConfigEntry<T> fields:

    using UXAssist.Common.ModFeatures;
    
    namespace UXAssist.Patches.Factory;
    
    [ModFeature("Factory", Order = 10)]
    public static class FactoryPatch
    {
        public static ConfigEntry<bool> UnlimitInteractiveEnabled { get; internal set; }
        // ... all other public ConfigEntry fields from the original file
    
        public static void Init()
        {
            ImmediateBuildPatch.Init();
            ArchitectModePatch.Init();
            // ...
        }
    
        public static void Start()
        {
            ImmediateBuildPatch.Start();
            // ...
        }
    
        public static void Uninit()
        {
            ImmediateBuildPatch.Uninit();
            // ...
        }
    
        public static void OnInputUpdate() => BeltSignalPatch.OnInputUpdate();
        public static void Export(BinaryWriter w) => BeltSignalPatch.Export(w);
        public static void Import(BinaryReader r) => BeltSignalPatch.Import(r);
    }
    

    Awake() in UXAssist.cs already assigns every ConfigEntry; leave those assignments untouched.

  • Step 3: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 4: Commit

    git add UXAssist/Patches/Factory/
    git rm UXAssist/Patches/FactoryPatch.cs
    git commit -m "refactor(UXAssist): split FactoryPatch into focused classes"
    

Task 4: Split UXAssist/Patches/LogisticsPatch.cs and mark features

Files:

  • Create: UXAssist/Patches/Logistics/LogisticsPatch.cs

  • Create: UXAssist/Patches/Logistics/AutoConfigPatch.cs

  • Create: UXAssist/Patches/Logistics/CapacityPatch.cs

  • Create: UXAssist/Patches/Logistics/OverflowPatch.cs

  • Create: UXAssist/Patches/Logistics/RealtimeInfoPanelPatch.cs

  • Delete: UXAssist/Patches/LogisticsPatch.cs

  • Step 1: Move nested classes and create coordinator

    Same pattern as Task 3. Coordinator is marked [ModFeature("Logistics", Order = 11)] and forwards OnInputUpdate / OnUpdate to sub-classes.

  • Step 2: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 3: Commit

    git add UXAssist/Patches/Logistics/
    git rm UXAssist/Patches/LogisticsPatch.cs
    git commit -m "refactor(UXAssist): split LogisticsPatch into focused classes"
    

Task 5: Split UXAssist/Functions/UIFunctions.cs and mark features

Files:

  • Create: UXAssist/Functions/UI/StarmapFilterUI.cs

  • Create: UXAssist/Functions/UI/MilkyWayUI.cs

  • Create: UXAssist/Functions/UI/AutoCruiseUI.cs

  • Create: UXAssist/Functions/UI/MenuButtonUI.cs

  • Modify: UXAssist/Functions/UIFunctions.cs (coordinator, marked [ModFeature("UI", Order = 12)])

  • Modify: UXAssist/UXAssist.cs

  • Step 1: Move features to sub-files

    Namespace UXAssist.Functions.UI. Keep internal where possible. Keep public ConfigEntry<T> fields on UIFunctions if bound in UXAssist.Awake().

  • Step 2: Update UXAssist.Awake() export/import calls

    Replace UIFunctions.ExportClusterUploadResults(w) with a coordinator call or direct MilkyWayUI.Export(w).

  • Step 3: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 4: Commit

    git add UXAssist/Functions/UI/ UXAssist/Functions/UIFunctions.cs UXAssist/UXAssist.cs
    git commit -m "refactor(UXAssist): split UIFunctions into UI sub-features"
    

Task 6: Update UXAssist.cs to drive features through ModFeatureRegistry

Files:

  • Modify: UXAssist/UXAssist.cs

  • Step 1: Discover features from UXAssist assembly

    In Awake(), after config binding:

    ModFeatureRegistry.Discover(Assembly.GetExecutingAssembly());
    ModFeatureRegistry.InitAll();
    
  • Step 2: Replace lifecycle calls

    In Start():

    ModFeatureRegistry.StartAll();
    

    In OnDestroy():

    ModFeatureRegistry.UninitAll();
    
  • Step 3: Replace hard-coded update calls

    In Update():

    if (VFInput.inputing) return;
    if (DSPGame.IsMenuDemo)
    {
        ModFeatureRegistry.OnInputUpdateAll();
        return;
    }
    ModFeatureRegistry.OnInputUpdateAll();
    ModFeatureRegistry.OnUpdateAll();
    

    Remove the explicit per-class calls (LogisticsPatch.OnInputUpdate(), UIFunctions.OnInputUpdate(), GamePatch.OnInputUpdate(), FactoryPatch.OnInputUpdate(), PlayerPatch.OnInputUpdate(), LogisticsPatch.OnUpdate()).

  • Step 4: Keep compat initialization separate

    Keep the ModsCompat scan in Awake() and Start(); compat wrappers are not mod features because they receive a Harmony argument.

  • Step 5: Centralize save participants

    Add a simple registry in UXAssist.cs:

    private static readonly List<Action<BinaryWriter>> _exporters = [];
    private static readonly List<Action<BinaryReader>> _importers = [];
    
    public static void RegisterExporter(Action<BinaryWriter> e) => _exporters.Add(e);
    public static void RegisterImporter(Action<BinaryReader> i) => _importers.Add(i);
    

    Update Export/Import to iterate _exporters/_importers. Remove direct FactoryPatch.Export / UIFunctions.ExportClusterUploadResults calls; the coordinators register themselves during Init.

  • Step 6: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 7: Commit

    git add UXAssist/UXAssist.cs
    git commit -m "refactor(UXAssist): drive lifecycle via ModFeatureRegistry"
    

Task 7: Decouple UXAssist.UIConfigWindow from patch internals

Files:

  • Create: UXAssist/Common/Config/FactoryConfigProvider.cs

  • Create: UXAssist/Common/Config/LogisticsConfigProvider.cs

  • Modify: UXAssist/UIConfigWindow.cs

  • Step 1: Create config providers

    Each provider exposes only the ConfigEntry<T> references that the UI needs:

    public static class FactoryConfigProvider
    {
        public static ConfigEntry<bool> NightLightEnabled => Factory.FactoryPatch.NightLightEnabled;
        // ...
    }
    
  • Step 2: Update UIConfigWindow.cs

    Replace direct references like FactoryPatch.NightLightEnabled with FactoryConfigProvider.NightLightEnabled.

  • Step 3: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 4: Commit

    git add UXAssist/Common/Config/ UXAssist/UIConfigWindow.cs
    git commit -m "refactor(UXAssist): introduce config providers between UI and patches"
    

Task 8: Adopt ModFeatureRegistry in CheatEnabler

Files:

  • Modify: CheatEnabler/CheatEnabler.cs

  • Step 1: Replace exact-namespace reflection with feature discovery

    In Awake():

    ModFeatureRegistry.Discover(Assembly.GetExecutingAssembly());
    ModFeatureRegistry.InitAll();
    

    In Start():

    ModFeatureRegistry.StartAll();
    

    In OnDestroy():

    ModFeatureRegistry.UninitAll();
    

    In Update():

    if (VFInput.inputing) return;
    ModFeatureRegistry.OnInputUpdateAll();
    

    Remove _patches and the reflection-based Init/Start/Uninit loop.

  • Step 2: Build CheatEnabler

    dotnet build CheatEnabler/CheatEnabler.csproj -c Release
    
  • Step 3: Commit

    git add CheatEnabler/CheatEnabler.cs
    git commit -m "refactor(CheatEnabler): adopt ModFeatureRegistry from UXAssist"
    

Task 9: Split CheatEnabler/Patches/FactoryPatch.cs

Files:

  • Create: CheatEnabler/Patches/Factory/FactoryPatch.cs

  • Create: CheatEnabler/Patches/Factory/ImmediateBuildPatch.cs

  • Create: CheatEnabler/Patches/Factory/ArchitectModePatch.cs

  • Create: CheatEnabler/Patches/Factory/BeltSignalPatch.cs

  • Create: CheatEnabler/Patches/Factory/PowerBoostPatch.cs

  • Create: CheatEnabler/Patches/Factory/LogisticsControlPatch.cs

  • Delete: CheatEnabler/Patches/FactoryPatch.cs

  • Step 1: Apply the coordinator pattern and [ModFeature]

    Namespace CheatEnabler.Patches.Factory. Coordinator exposes the public ConfigEntry<T> fields so CheatEnabler.Awake() continues to compile without changes. Mark coordinator [ModFeature("CheatFactory", Order = 10)].

  • Step 2: Build CheatEnabler

    dotnet build CheatEnabler/CheatEnabler.csproj -c Release
    
  • Step 3: Commit

    git add CheatEnabler/Patches/Factory/
    git rm CheatEnabler/Patches/FactoryPatch.cs
    git commit -m "refactor(CheatEnabler): split FactoryPatch into focused classes"
    

Task 10: Split CheatEnabler/Functions/DysonSphereFunctions.cs

Files:

  • Create: CheatEnabler/Functions/DysonSphere/DysonSphereResolver.cs

  • Create: CheatEnabler/Functions/DysonSphere/ShellCompletionFunctions.cs

  • Create: CheatEnabler/Functions/DysonSphere/FrameRemovalFunctions.cs

  • Create: CheatEnabler/Functions/DysonSphere/IllegalShellFunctions.cs

  • Create: CheatEnabler/Functions/DysonSphere/GeometryHelpers.cs

  • Modify: CheatEnabler/Functions/DysonSphereFunctions.cs (coordinator, or delete if empty)

  • Modify: CheatEnabler/Patches/DysonSpherePatch.cs if it calls helpers

  • Step 1: Extract repeated "current sphere / star" logic

    [ModFeature("DysonSphereResolver")]
    public static class DysonSphereResolver
    {
        public static (DysonSphere sphere, StarData star)? ResolveCurrent()
        {
            var star = GameMain.localStar;
            if (star == null) return null;
            var sphere = GameMain.data.dysonSpheres[star.index];
            if (sphere == null) return null;
            return (sphere, star);
        }
    }
    
  • Step 2: Move shell actions to dedicated files

    Replace duplicated cleanup blocks with a shared helper:

    public static void NotifyShellChanged(DysonSphere sphere, DysonSphereLayer layer)
    {
        layer?.RecalculateModels();
        sphere?.swarm?.RecalculateModels();
    }
    
  • Step 3: Build CheatEnabler

    dotnet build CheatEnabler/CheatEnabler.csproj -c Release
    
  • Step 4: Commit

    git add CheatEnabler/Functions/DysonSphere/
    git rm CheatEnabler/Functions/DysonSphereFunctions.cs
    git commit -m "refactor(CheatEnabler): split DysonSphereFunctions into focused helpers"
    

Task 11: Adopt ModFeatureRegistry in UniverseGenTweaks

Files:

  • Modify: UniverseGenTweaks/UniverseGenTweaks.cs

  • Step 1: Replace explicit Init/Uninit with feature discovery

    In Awake():

    ModFeatureRegistry.Discover(Assembly.GetExecutingAssembly());
    ModFeatureRegistry.InitAll();
    

    In OnDestroy():

    ModFeatureRegistry.UninitAll();
    

    Remove explicit MoreSettings.Init(), EpicDifficulty.Init(), BirthPlanetPatch.Init() and their Uninit() counterparts.

  • Step 2: Build UniverseGenTweaks

    dotnet build UniverseGenTweaks/UniverseGenTweaks.csproj -c Release
    
  • Step 3: Commit

    git add UniverseGenTweaks/UniverseGenTweaks.cs
    git commit -m "refactor(UniverseGenTweaks): adopt ModFeatureRegistry from UXAssist"
    

Task 12: Split UniverseGenTweaks/MoreSettings.cs

Files:

  • Create: UniverseGenTweaks/Patches/GalaxyGenSettingsPatch.cs

  • Create: UniverseGenTweaks/Patches/GalaxySelectUIPatch.cs

  • Create: UniverseGenTweaks/Patches/CombatSettingsPatch.cs

  • Create: UniverseGenTweaks/Functions/GalaxyGenSave.cs

  • Modify: UniverseGenTweaks/MoreSettings.cs (coordinator, or delete)

  • Step 1: Move UI construction to GalaxySelectUIPatch

    Mark it [ModFeature("GalaxySelectUI")]. Keep slider/text creation helpers there.

  • Step 2: Move transpilers to GalaxyGenSettingsPatch

    Mark it [ModFeature("GalaxyGenSettings")]. Keep the Init()/Uninit() pattern and the Harmony instance local to this class.

  • Step 3: Move combat settings to CombatSettingsPatch

    Mark it [ModFeature("CombatSettings")]. Replace the seven near-identical slider-changed prefix methods with a table-driven mapper.

  • Step 4: Move save serialization to GalaxyGenSave

    Mark it [ModFeature("GalaxyGenSave")]. Implement Export(BinaryWriter) / Import(BinaryReader) and call them from UniverseGenTweaks.Export/Import.

  • Step 5: Build UniverseGenTweaks

    dotnet build UniverseGenTweaks/UniverseGenTweaks.csproj -c Release
    
  • Step 6: Commit

    git add UniverseGenTweaks/Patches/ UniverseGenTweaks/Functions/
    git rm UniverseGenTweaks/MoreSettings.cs
    git commit -m "refactor(UniverseGenTweaks): split MoreSettings into focused classes"
    

Task 13: Phase 1 cross-project build verification

  • Step 1: Clean and build all three projects

    dotnet clean UXAssist/UXAssist.csproj -c Release
    dotnet clean CheatEnabler/CheatEnabler.csproj -c Release
    dotnet clean UniverseGenTweaks/UniverseGenTweaks.csproj -c Release
    dotnet build UXAssist/UXAssist.csproj -c Release
    dotnet build CheatEnabler/CheatEnabler.csproj -c Release
    dotnet build UniverseGenTweaks/UniverseGenTweaks.csproj -c Release
    

    Expected: all three succeed with no compilation errors.

  • Step 2: Produce packages

    dotnet build -t:ZipMod -c Release
    dotnet build -t:CopyToParentPackage -c Release
    

    Expected: UXAssist/package/, CheatEnabler/package/, UniverseGenTweaks/package/, and Dustbin/package/patchers/ (if building the full solution) are generated.

  • Step 3: Public API diff check

    Use dotnet build output or ildasm to verify that every member listed in docs/PublicApiSurface.md still exists with the same signature.

  • Step 4: Commit a Phase 1 checkpoint tag

    git tag refactor-phase1
    

Phase 2 — UI / Common Internal Refactoring (public surface preserved)

Task 14: Split UXAssist/Common/Util.cs into focused helpers

Files:

  • Create: UXAssist/Common/Util/ReflectionUtil.cs

  • Create: UXAssist/Common/Util/ResourceUtil.cs

  • Create: UXAssist/Common/Util/PathUtil.cs

  • Modify: UXAssist/Common/Util.cs

  • Step 1: Move implementations to focused helpers

    ReflectionUtil.cs:

    public static class ReflectionUtil
    {
        public static Type[] GetTypesFiltered(Assembly assembly, Func<Type, bool> predicate) { ... }
        public static Type[] GetTypesInNamespace(Assembly assembly, string nameSpace) { ... }
        public static Type[] GetTypesInNamespacePrefix(Assembly assembly, string prefix) { ... }
    }
    

    ResourceUtil.cs:

    public static class ResourceUtil
    {
        public static byte[] LoadEmbeddedResource(...) { ... }
        public static Texture2D LoadEmbeddedTexture(...) { ... }
        public static Sprite LoadEmbeddedSprite(...) { ... }
    }
    

    PathUtil.cs:

    public static class PathUtil
    {
        public static string PluginFolder(Assembly assembly = null) { ... }
    }
    
  • Step 2: Keep Util as a public forwarding facade

    public static class Util
    {
        [Obsolete("Use ReflectionUtil.GetTypesFiltered")]
        public static Type[] GetTypesFiltered(Assembly assembly, Func<Type, bool> predicate)
            => ReflectionUtil.GetTypesFiltered(assembly, predicate);
    
        [Obsolete("Use ReflectionUtil.GetTypesInNamespace")]
        public static Type[] GetTypesInNamespace(Assembly assembly, string nameSpace)
            => ReflectionUtil.GetTypesInNamespace(assembly, nameSpace);
    
        [Obsolete("Use ResourceUtil.LoadEmbeddedResource")]
        public static byte[] LoadEmbeddedResource(string path, Assembly assembly = null)
            => ResourceUtil.LoadEmbeddedResource(path, assembly);
    
        // ... forward all other existing methods
    }
    

    New code inside UXAssist may call ReflectionUtil/ResourceUtil/PathUtil directly.

  • Step 3: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 4: Commit

    git add UXAssist/Common/Util/
    git commit -m "refactor(UXAssist): split Util into focused helpers with forwarding facade"
    

Task 15: Refactor UXAssist/Common/GameLogic.cs event invocation

Files:

  • Create: UXAssist/Common/GameEvent.cs

  • Modify: UXAssist/Common/GameLogic.cs

  • Step 1: Introduce a small safe-event wrapper

    public static class GameEvent
    {
        public static void InvokeSafe(this Action action, ManualLogSource logger, string name)
        {
            if (action == null) return;
            foreach (var d in action.GetInvocationList())
            {
                try { d.DynamicInvoke(); }
                catch (Exception ex) { logger?.LogWarning($"GameEvent '{name}' handler failed: {ex}"); }
            }
        }
    }
    
  • Step 2: Use the wrapper inside GameLogic

    Keep the public event fields unchanged. Replace manual invocation loops with OnDataLoaded.InvokeSafe(UXAssist.Logger, nameof(OnDataLoaded));.

  • Step 3: Add XML documentation to all public members

  • Step 4: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 5: Commit

    git add UXAssist/Common/GameEvent.cs UXAssist/Common/GameLogic.cs
    git commit -m "refactor(UXAssist): safe GameEvent wrapper and XML docs"
    

Task 16: Refactor UXAssist/Common/I18N.cs internals

Files:

  • Modify: UXAssist/Common/I18N.cs

  • Step 1: Preserve public API

    Keep Add, Apply, Translate, Init, OnInitialized signatures exactly as they are.

  • Step 2: Internal cleanup

    Split the internal storage into a private LocalizedString record and a dictionary keyed by the English key. Add a public convenience overload:

    public static void Add(string key, string en, string zh)
    

    (this is the existing signature; keep it).

  • Step 3: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 4: Commit

    git add UXAssist/Common/I18N.cs
    git commit -m "refactor(UXAssist): clean up I18N internals without changing public API"
    

Task 17: Extract layout helpers from UXAssist/UI/MyWindow.cs

Files:

  • Create: UXAssist/UI/LayoutHelper.cs

  • Modify: UXAssist/UI/MyWindow.cs

  • Step 1: Move static layout helpers

    Move AddText, AddTipsButton, AddButton (static overloads), and AddElement into LayoutHelper.

    public static class LayoutHelper
    {
        public static Text AddText(float x, float y, RectTransform parent, string label, int fontSize = 14, string objName = "label") { ... }
        public static UIButton AddTipsButton(...) { ... }
        public static UIButton AddButton(...) { ... }
    }
    
  • Step 2: Keep MyWindow instance methods as forwarding facades

    public Text AddText2(float x, float y, RectTransform parent, string label, int fontSize = 14, string objName = "label")
    {
        var text = LayoutHelper.AddText(x, y, parent, label, fontSize, objName);
        _maxX = Math.Max(_maxX, x + text.rectTransform.sizeDelta.x);
        MaxY = Math.Max(MaxY, y + text.rectTransform.sizeDelta.y);
        return text;
    }
    

    Mark the public static helpers on MyWindow as [Obsolete("Use LayoutHelper")] if desired, but keep them working.

  • Step 3: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 4: Commit

    git add UXAssist/UI/LayoutHelper.cs UXAssist/UI/MyWindow.cs
    git commit -m "refactor(UXAssist): extract UI layout helpers from MyWindow"
    

Task 18: Refactor UXAssist/UI/MyConfigWindow.cs tab management

Files:

  • Create: UXAssist/UI/ConfigTabGroup.cs

  • Modify: UXAssist/UI/MyConfigWindow.cs

  • Modify: UXAssist/UI/MyWindowWithTabs.cs

  • Step 1: Extract tab group logic

    Move the data structure that tracks tab groups into ConfigTabGroup so MyConfigWindow does not mix tab state with window lifecycle.

  • Step 2: Preserve public events and methods

    Keep OnUICreated, OnUpdateUI, CreateInstance, and DestroyInstance on MyConfigWindow with identical signatures.

  • Step 3: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 4: Commit

    git add UXAssist/UI/ConfigTabGroup.cs UXAssist/UI/MyConfigWindow.cs UXAssist/UI/MyWindowWithTabs.cs
    git commit -m "refactor(UXAssist): split tab management out of MyConfigWindow"
    

Task 19: Phase 2 cross-project build verification

  • Step 1: Build all three projects

    dotnet build UXAssist/UXAssist.csproj -c Release
    dotnet build CheatEnabler/CheatEnabler.csproj -c Release
    dotnet build UniverseGenTweaks/UniverseGenTweaks.csproj -c Release
    
  • Step 2: Public API diff check

    Confirm every member in docs/PublicApiSurface.md still exists with the same signature. New [Obsolete] facades are acceptable.

  • Step 3: Commit a Phase 2 checkpoint tag

    git tag refactor-phase2
    

Phase 3 — Constants & Localization

Task 20: Create centralized constants files

Files:

  • Create: UXAssist/Common/GameConstants/ItemIds.cs

  • Create: UXAssist/Common/GameConstants/TechIds.cs

  • Create: UXAssist/Common/GameConstants/LogisticsConstants.cs

  • Create: UXAssist/Common/GameConstants/DysonSphereConstants.cs

  • Create: UXAssist/Common/GameConstants/UniverseGenConstants.cs

  • Step 1: Extract item IDs

    From CheatEnabler/Patches/Factory/*.cs and UXAssist/Patches/Factory/BeltSignalPatch.cs, collect hard-coded item IDs:

    public static class ItemIds
    {
        public const int IronOre = 1001;
        public const int CopperOre = 1002;
        // ...
    }
    
  • Step 2: Extract tech IDs

    From UXAssist/Patches/TechPatch.cs:

    public static class TechIds
    {
        public const int SorterCargoStacking = 3608;
        public static readonly HashSet<int> CombatTechs = [3301, 3302, ...];
    }
    
  • Step 3: Extract logistics constants

    From UXAssist/Patches/Logistics/CapacityPatch.cs:

    public static class LogisticsConstants
    {
        public const int DefaultLocalStorageMax = 5000;
        public const int DefaultRemoteStorageMax = 10000;
    }
    
  • Step 4: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 5: Commit

    git add UXAssist/Common/GameConstants/
    git commit -m "refactor: centralize game constants"
    

Task 21: Replace magic numbers in UXAssist

Files:

  • Modify: UXAssist/Patches/Factory/*.cs

  • Modify: UXAssist/Patches/Logistics/*.cs

  • Modify: UXAssist/Patches/TechPatch.cs

  • Modify: UXAssist/Patches/DysonSpherePatch.cs

  • Step 1: Replace literal IDs and capacities with constants

    For example, change:

    if (itemId == 1001) { ... }
    

    to:

    if (itemId == ItemIds.IronOre) { ... }
    
  • Step 2: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 3: Commit

    git add UXAssist/Patches/
    git commit -m "refactor(UXAssist): replace magic numbers with constants"
    

Task 22: Replace magic numbers in CheatEnabler and UniverseGenTweaks

Files:

  • Modify: CheatEnabler/Patches/Factory/*.cs

  • Modify: CheatEnabler/Functions/DysonSphere/*.cs

  • Modify: CheatEnabler/Functions/PlanetFunctions.cs

  • Modify: UniverseGenTweaks/Patches/GalaxyGenSettingsPatch.cs

  • Modify: UniverseGenTweaks/Patches/CombatSettingsPatch.cs

  • Modify: UniverseGenTweaks/BirthPlanetPatch.cs

  • Step 1: Reference UXAssist constants where appropriate

    CheatEnabler/UniverseGenTweaks already reference UXAssist.csproj, so they can use UXAssist.Common.GameConstants.ItemIds, etc.

  • Step 2: Build both projects

    dotnet build CheatEnabler/CheatEnabler.csproj -c Release
    dotnet build UniverseGenTweaks/UniverseGenTweaks.csproj -c Release
    
  • Step 3: Commit

    git add CheatEnabler/ UniverseGenTweaks/
    git commit -m "refactor(CheatEnabler/UniverseGen): consume centralized constants"
    

Task 23: Localization key governance

Files:

  • Modify: UXAssist/UIConfigWindow.cs

  • Modify: CheatEnabler/UIConfigWindow.cs

  • Modify: UniverseGenTweaks/UIConfigWindow.cs

  • Modify: CheatEnabler/Functions/PlayerFunctions.cs

  • Modify: UXAssist/Patches/Factory/BeltSignalPatch.cs

  • Step 1: Replace hard-coded Chinese .Translate() keys with I18N.Add entries

    Example:

    // Before
    var btn = MyWindow.AddButton(..., "确定", ...);
    // After
    I18N.Add("OK", "OK", "确定");
    var btn = MyWindow.AddButton(..., "OK", ...);
    
  • Step 2: Translate Chinese comments to English

    Run a search for // followed by CJK characters and translate or delete stale comments.

    grep -RInP '//.*[\x{4e00}-\x{9fff}]' UXAssist/ CheatEnabler/ UniverseGenTweaks/ --include='*.cs' || true
    
  • Step 3: Build all three projects

    dotnet build UXAssist/UXAssist.csproj -c Release
    dotnet build CheatEnabler/CheatEnabler.csproj -c Release
    dotnet build UniverseGenTweaks/UniverseGenTweaks.csproj -c Release
    
  • Step 4: Commit

    git add UXAssist/ CheatEnabler/ UniverseGenTweaks/
    git commit -m "refactor: localize hard-coded strings and translate comments"
    

Phase 4 — Static State & Lifecycle Cleanup

Task 24: Inventory static mutable state

Files:

  • Create: docs/StaticStateInventory.md

  • Step 1: List all static mutable fields in the three projects

    Search:

    grep -RInP 'private static (?!readonly)\S+' UXAssist/Patches/ UXAssist/Functions/ CheatEnabler/Patches/ CheatEnabler/Functions/ UniverseGenTweaks/ --include='*.cs' > docs/StaticStateInventory.md
    grep -RInP 'public static (?!readonly)\S+' UXAssist/Patches/ UXAssist/Functions/ CheatEnabler/Patches/ CheatEnabler/Functions/ UniverseGenTweaks/ --include='*.cs' >> docs/StaticStateInventory.md
    
  • Step 2: Classify each field by subsystem

    Mark each as:

    • LifecycleSafe — read-only after Awake
    • NeedsReset — must be cleared on GameLogic.OnGameEnd
    • CandidateForInstancing — should be owned by a per-game context class
  • Step 3: Commit

    git add docs/StaticStateInventory.md
    git commit -m "docs: inventory static mutable state"
    

Task 25: Add lifecycle reset callbacks

Files:

  • Modify: UXAssist/Patches/Factory/BeltSignalPatch.cs

  • Modify: UXAssist/Patches/Factory/VeinProtectionPatch.cs

  • Modify: UXAssist/Patches/DysonSpherePatch.cs

  • Modify: UXAssist/Functions/UI/MilkyWayUI.cs

  • Modify: CheatEnabler/Patches/Factory/BeltSignalPatch.cs

  • Modify: UniverseGenTweaks/Patches/GalaxySelectUIPatch.cs

  • Step 1: Add ResetState methods

    Example:

    internal static void ResetState()
    {
        _signalBelts = null;
        _someCache.Clear();
    }
    
  • Step 2: Register resets in Init

    public static void Init()
    {
        GameLogic.OnGameEnd += ResetState;
        // ...
    }
    
  • Step 3: Build all three projects

    dotnet build UXAssist/UXAssist.csproj -c Release
    dotnet build CheatEnabler/CheatEnabler.csproj -c Release
    dotnet build UniverseGenTweaks/UniverseGenTweaks.csproj -c Release
    
  • Step 4: Commit

    git add UXAssist/ CheatEnabler/ UniverseGenTweaks/
    git commit -m "refactor: register static-state resets on game end"
    

Phase 5 — Transpiler Robustness & Quality Gates

Task 26: Annotate transpilers with target version and fallback

Files:

  • Modify: all *Transpiler methods in UXAssist/Patches/, CheatEnabler/Patches/, UniverseGenTweaks/Patches/

  • Step 1: Add header comments

    // Target game version: 0.10.34.28505
    // Patches: EjectorComponent.InternalUpdate
    // Falls back to original IL if the pattern is not matched.
    private static IEnumerable<CodeInstruction> ...
    
  • Step 2: Wrap CodeMatcher finalization

    Replace bare .InstructionEnumeration() with:

    return matcher.ReportFailure(original, Logger)?.InstructionEnumeration() ?? instructions;
    

    Implement extension:

    public static CodeMatcher ReportFailure(this CodeMatcher matcher, MethodBase original, ManualLogSource logger)
    {
        if (matcher.IsValid) return matcher;
        logger?.LogWarning($"Transpiler failed for {original.DeclaringType?.Name}.{original.Name}");
        return null;
    }
    
  • Step 3: Build all three projects

    dotnet build UXAssist/UXAssist.csproj -c Release
    dotnet build CheatEnabler/CheatEnabler.csproj -c Release
    dotnet build UniverseGenTweaks/UniverseGenTweaks.csproj -c Release
    
  • Step 4: Commit

    git add UXAssist/ CheatEnabler/ UniverseGenTweaks/
    git commit -m "refactor: add transpiler version/fallback annotations"
    

Task 27: Centralize third-party compat reflection targets

Files:

  • Create: UXAssist/ModsCompat/CompatTargets.cs

  • Modify: UXAssist/ModsCompat/AuxilaryfunctionWrapper.cs

  • Modify: UXAssist/ModsCompat/BulletTimeWrapper.cs

  • Modify: UXAssist/ModsCompat/BlueprintTweaks.cs

  • Step 1: Extract magic strings

    internal static class CompatTargets
    {
        public const string Auxilaryfunction = "auxilaryfunction.Auxilaryfunction";
        public const string SpeedUpPatch = "Auxilaryfunction.SpeedUpPatch";
        // ...
    }
    
  • Step 2: Replace literal strings with constants

  • Step 3: Build UXAssist

    dotnet build UXAssist/UXAssist.csproj -c Release
    
  • Step 4: Commit

    git add UXAssist/ModsCompat/
    git commit -m "refactor(UXAssist): centralize compat reflection targets"
    

Task 28: Add .editorconfig and run dotnet format

Files:

  • Create: .editorconfig

  • Modify: all touched .cs files (format-only changes)

  • Step 1: Create .editorconfig

    Minimal content:

    root = true
    
    [*.cs]
    indent_style = space
    indent_size = 4
    charset = utf-8-bom
    end_of_line = crlf
    insert_final_newline = true
    dotnet_sort_system_directives_first = true
    dotnet_separate_import_directive_groups = false
    
  • Step 2: Run format

    dotnet format UXAssist/UXAssist.csproj
    dotnet format CheatEnabler/CheatEnabler.csproj
    dotnet format UniverseGenTweaks/UniverseGenTweaks.csproj
    
  • Step 3: Build all three projects

    dotnet build UXAssist/UXAssist.csproj -c Release
    dotnet build CheatEnabler/CheatEnabler.csproj -c Release
    dotnet build UniverseGenTweaks/UniverseGenTweaks.csproj -c Release
    
  • Step 4: Commit

    git add .editorconfig
    git add -u UXAssist/ CheatEnabler/ UniverseGenTweaks/
    git commit -m "style: add editorconfig and run dotnet format"
    

Final Verification

  • Step 1: Full solution build

    dotnet build DSP_Mods.sln -c Release
    

    Expected: 0 errors.

  • Step 2: Package all mods

    dotnet build -t:ZipMod -c Release
    dotnet build -t:CopyToParentPackage -c Release
    

    Expected: all package/ outputs generated.

  • Step 3: Public API verification

    Re-run the docs/PublicApiSurface.md checklist and confirm every listed member still exists.

  • Step 4: Update AGENTS.md

    If any project conventions changed (e.g., new folder structure, naming rules, .editorconfig), update AGENTS.md accordingly.

  • Step 5: Final commit / tag

    git tag refactor-complete
    

Self-Review Checklist

  • Every task references exact file paths.
  • No TODO, TBD, or placeholder steps remain.
  • Public API contract (UXAssist.UI + UXAssist.Common) is preserved throughout.
  • CheatEnabler and UniverseGenTweaks compile after each phase.
  • Code snippets use types/methods defined in earlier tasks.