diff --git a/AGENTS.md b/AGENTS.md index 3f9475a..822fd68 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -150,6 +150,6 @@ The sync is implemented as an inline PowerShell `Exec` step inside the `ZipMod` - **Fail-soft patch application:** `PatchImpl.Enable(true)` applies Harmony patches inside a try/catch. Runtime patching can fail through no fault of ours (Harmony re-runs other mods' transpilers on shared target methods), and an escaping exception would abort the calling `ConfigEntry.SettingChanged` delegate chain, desyncing config UI from config values. On failure it logs a `LogError` with the feature type name, rolls back via `UnpatchSelf()`, and leaves `_patch` null so a later `Enable(true)` can retry. - **Convergent in-game UI state:** In-game overlay widgets whose visibility depends on game state (e.g. `AutoConstructUI`) must not rely solely on one-shot event-driven refreshes (`SettingChanged` handlers, patch `OnEnable`/`OnDisable`), because a thrown exception earlier in a delegate chain or a failed patch application silently drops the refresh. `AutoConstructUI.OnUpdate()` reconciles button visibility and the pending-construction count with actual game state every 30 frames (also effective while paused); the `AutoConstructPatch` postfix on `PlayerAction_Rts.GameTick` only implements the fly-to-target behavior, and `AutoConstructPatch.OnEnable` logs its visibility predicate inputs once per enable as a remote-diagnosis aid. - **Transpiler patches:** Performance-critical mods (LabOpt, MechaDronesTweaks) use `[HarmonyTranspiler]` to rewrite IL instructions directly for maximum efficiency. All transpilers in UXAssist, CheatEnabler, and UniverseGenTweaks carry a standard header comment (`// Harmony transpiler:`, `// Target:`, `// Fallback:`) documenting the target method and fallback behavior. `UXAssist.Common.Patching.TranspilerGuard` provides a reusable `CodeMatcher.Finish` helper that returns original instructions when a matcher becomes invalid. -- **Mod-compatibility reflection:** Use `UXAssist.Common.ModCompat.ModCompatHelper` for BepInEx plugin detection, external mod type/method/field resolution, and property-setter lookup. Use `UXAssist.Common.Utils.DysonSphereReflection` for the DSPOptimizations-compatible `DysonSphereLayer` private fields (`totalNodeSP`, `totalFrameSP`, `totalCP`) instead of resolving them locally in each consumer. +- **Mod-compatibility reflection:** Use `UXAssist.Common.ModCompat.ModCompatHelper` for BepInEx plugin detection, external mod type/method/field resolution, and property-setter lookup. Preserve the old public type identity with a forwarding or inherited compatibility facade when a refactor moves a type that external mods may locate through reflection; build and verify that the legacy reflection target forwards to the refactored implementation. Use `UXAssist.Common.Utils.DysonSphereReflection` for the DSPOptimizations-compatible `DysonSphereLayer` private fields (`totalNodeSP`, `totalFrameSP`, `totalCP`) instead of resolving them locally in each consumer. - **Build quality gates:** Root `.editorconfig` defines suggestion-only C# style conventions. `Directory.Build.props` enables `TreatWarningsAsErrors` with `NoWarn>0618` for the expected obsolete-API usage, so any new warning fails the build. `.github/workflows/build.yml` runs a Release build and packages the three main mods on every push/PR. - **Save persistence:** Mods that need to persist data use the `IModCanSave` interface from DSPModSave. diff --git a/CheatEnabler/CheatEnabler.cs b/CheatEnabler/CheatEnabler.cs index 56aec96..f71b31f 100644 --- a/CheatEnabler/CheatEnabler.cs +++ b/CheatEnabler/CheatEnabler.cs @@ -3,6 +3,7 @@ using BepInEx; using BepInEx.Configuration; using CheatEnabler.Patches; using CheatEnabler.Patches.Factory; +using FactoryPatch = CheatEnabler.Patches.Factory.FactoryPatch; using UXAssist.Common; using UXAssist.Common.ModFeatures; diff --git a/CheatEnabler/Patches/Factory/ArchitectModePatch.cs b/CheatEnabler/Patches/Factory/ArchitectModePatch.cs index 65aad12..d3b5039 100644 --- a/CheatEnabler/Patches/Factory/ArchitectModePatch.cs +++ b/CheatEnabler/Patches/Factory/ArchitectModePatch.cs @@ -12,7 +12,7 @@ internal class ArchitectMode : PatchImpl var factory = GameMain.mainPlayer?.factory; if (factory?.planet?.data != null) { - FactoryPatch.ArrivePlanet(factory); + global::CheatEnabler.Patches.FactoryPatch.ArrivePlanet(factory); } } diff --git a/CheatEnabler/Patches/Factory/FactoryPatch.cs b/CheatEnabler/Patches/Factory/FactoryPatch.cs index 95cfac5..4785938 100644 --- a/CheatEnabler/Patches/Factory/FactoryPatch.cs +++ b/CheatEnabler/Patches/Factory/FactoryPatch.cs @@ -188,7 +188,7 @@ public class FactoryPatch : PatchImpl var main = GameMain.instance; if (main != null && main._running && __instance.factory?.planet?.data != null) { - ArrivePlanet(__instance.factory); + global::CheatEnabler.Patches.FactoryPatch.ArrivePlanet(__instance.factory); } } @@ -198,7 +198,7 @@ public class FactoryPatch : PatchImpl var factory = GameMain.mainPlayer?.factory; if (factory?.planet?.data != null) { - ArrivePlanet(factory); + global::CheatEnabler.Patches.FactoryPatch.ArrivePlanet(factory); } GameMain.data?.warningSystem?.UpdateCriticalWarningText(); } diff --git a/CheatEnabler/Patches/Factory/ImmediateBuildPatch.cs b/CheatEnabler/Patches/Factory/ImmediateBuildPatch.cs index c6d96c5..8421321 100644 --- a/CheatEnabler/Patches/Factory/ImmediateBuildPatch.cs +++ b/CheatEnabler/Patches/Factory/ImmediateBuildPatch.cs @@ -121,7 +121,7 @@ internal class ImmediateBuild : PatchImpl var factory = GameMain.mainPlayer?.factory; if (factory?.planet?.data != null) { - FactoryPatch.ArrivePlanet(factory); + global::CheatEnabler.Patches.FactoryPatch.ArrivePlanet(factory); } } // Harmony transpiler: Transpiler @@ -157,7 +157,7 @@ internal class ImmediateBuild : PatchImpl matcher.Insert( new CodeInstruction(OpCodes.Ldarg_0).WithLabels(labels), new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(BuildTool), nameof(BuildTool.factory))), - new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(FactoryPatch), nameof(FactoryPatch.ArrivePlanet))) + new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(global::CheatEnabler.Patches.FactoryPatch), nameof(global::CheatEnabler.Patches.FactoryPatch.ArrivePlanet))) ); return matcher.InstructionEnumeration(); } @@ -255,7 +255,7 @@ internal class ImmediateBuild : PatchImpl var factory = GameMain.mainPlayer?.factory; if (factory?.planet?.data != null) { - FactoryPatch.ArrivePlanet(factory); + global::CheatEnabler.Patches.FactoryPatch.ArrivePlanet(factory); } } } diff --git a/CheatEnabler/Patches/FactoryPatchCompat.cs b/CheatEnabler/Patches/FactoryPatchCompat.cs new file mode 100644 index 0000000..f9c148c --- /dev/null +++ b/CheatEnabler/Patches/FactoryPatchCompat.cs @@ -0,0 +1,12 @@ +namespace CheatEnabler.Patches; + +/// +/// Preserves the pre-refactor type and method identity used by mods that locate FactoryPatch through reflection. +/// +public static class FactoryPatch +{ + public static void ArrivePlanet(PlanetFactory factory) + { + Factory.FactoryPatch.ArrivePlanet(factory); + } +} diff --git a/CheatEnabler/UIConfigWindow.cs b/CheatEnabler/UIConfigWindow.cs index 4ac3ede..4bac4dd 100644 --- a/CheatEnabler/UIConfigWindow.cs +++ b/CheatEnabler/UIConfigWindow.cs @@ -1,6 +1,7 @@ using CheatEnabler.Functions; using CheatEnabler.Patches; using CheatEnabler.Patches.Factory; +using FactoryPatch = CheatEnabler.Patches.Factory.FactoryPatch; using UnityEngine; using UnityEngine.UI; using UXAssist.UI;