diff --git a/AGENTS.md b/AGENTS.md index 822fd68..36367e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -147,6 +147,7 @@ The sync is implemented as an inline PowerShell `Exec` step inside the `ZipMod` - **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. - **Game source facts:** In the original DSP `Assembly-CSharp.dll`, `PlanetFactory.prebuildCount` is computed as `prebuildCursor - prebuildRecycleCursor - 1`, and normal prebuild add/remove paths maintain those cursors. If Auto Construct UI reports zero while visible construction ghosts exist, first suspect the wrong planet/factory, non-prebuild preview state, or a missed UI refresh path before replacing this property with a pool scan. +- **Illegal Dyson shell generation:** Estimated triangle vertex counts mirror the original `DysonShell.GenerateGeometry` rasterization as closely as possible, but can still differ from the final frame-generated shell polygon at numerical boundaries. Max-output generation must treat `QuickAddDysonShell` failure as a candidate rejection and try the next candidate rather than assuming the estimate is exact. - **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. diff --git a/CheatEnabler/Functions/DysonSphere/GeometryHelpers.cs b/CheatEnabler/Functions/DysonSphere/GeometryHelpers.cs index 3218470..5a99d5a 100644 --- a/CheatEnabler/Functions/DysonSphere/GeometryHelpers.cs +++ b/CheatEnabler/Functions/DysonSphere/GeometryHelpers.cs @@ -162,17 +162,18 @@ public static class GeometryHelpers double num = 0.0; for (int i = 0; i < 3; i++) { - double num2 = Vector3.Distance(pos[i], pos[(i + 1) % 3]); + float num2 = Vector3.Distance(pos[i], pos[(i + 1) % 3]); VectorLF3 vectorLF2 = ((VectorLF3)pos[i] + (VectorLF3)pos[(i + 1) % 3]) * 0.5; sum += vectorLF2 * num2; - num += num2; + num += (double)num2; } var radius = Math.Round(polygon[0].magnitude * 10.0) / 10.0; for (int j = 0; j < polygon.Length; j++) { polygon[j] = polygon[j].normalized * radius; } - var center = (sum / num).normalized * radius; + var normalized = (sum / num).normalized; + var center = normalized * radius; float num3 = 0f; for (int k = 0; k < 3; k++) { @@ -189,12 +190,12 @@ public static class GeometryHelpers var cpPerVertex = gridScale * gridScale * DysonSphereConstants.CpPerVertexFactor; var num5 = (int)((double)num3 / factor3 / gridSizeDouble + 2.5); - var xaxis = VectorLF3.Cross(center, Vector3.up).normalized; + var xaxis = VectorLF3.Cross(normalized, Vector3.up).normalized; if (xaxis.magnitude < 0.1) { xaxis = new VectorLF3(0f, 0f, 1f); } - var yaxis = VectorLF3.Cross(xaxis, center).normalized; + var yaxis = VectorLF3.Cross(xaxis, normalized).normalized; var raydir = xaxis * factor0 + yaxis * factor1; var w1axis = xaxis * (0.5 * gridSizeDouble) - yaxis * (factor3 * gridSizeDouble); var w2axis = xaxis * (0.5 * gridSizeDouble) + yaxis * (factor3 * gridSizeDouble); @@ -206,7 +207,9 @@ public static class GeometryHelpers var polynu = new double[3]; for (int l = 0; l < 3; l++) { - polyn[l] = VectorLF3.Cross(polygon[l], polygon[(l + 1) % 3]).normalized; + Vector3 vector = polygon[l]; + Vector3 vector2 = polygon[(l + 1) % 3]; + polyn[l] = VectorLF3.Cross(vector, vector2).normalized; polynu[l] = polyn[l].x * raydir.x + polyn[l].y * raydir.y + polyn[l].z * raydir.z; } var vmap = _vmap.Value; diff --git a/CheatEnabler/Functions/DysonSphere/IllegalShellFunctions.cs b/CheatEnabler/Functions/DysonSphere/IllegalShellFunctions.cs index 68effac..698d6f9 100644 --- a/CheatEnabler/Functions/DysonSphere/IllegalShellFunctions.cs +++ b/CheatEnabler/Functions/DysonSphere/IllegalShellFunctions.cs @@ -362,6 +362,7 @@ public static class IllegalShellFunctions { var sshell = supposedShells[j]; var vertCount = GeometryHelpers.CalculateTriangleVertCount([sshell.posA, sshell.posB, sshell.posC]); + sshell.vertCount = vertCount <= barrier ? vertCount : -1; if (vertCount <= barrier) { lock (mutex) @@ -385,16 +386,32 @@ public static class IllegalShellFunctions if (maxJ >= 0) { ResetLayerPools(layer); - var sshell = supposedShells[maxJ]; - DysonNode[] newNodes = [layer.QuickAddDysonNode(0, sshell.posA), layer.QuickAddDysonNode(0, sshell.posB), layer.QuickAddDysonNode(0, sshell.posC)]; - DysonFrame[] newFrames = [layer.QuickAddDysonFrame(0, newNodes[0], newNodes[1], false), layer.QuickAddDysonFrame(0, newNodes[1], newNodes[2], false), layer.QuickAddDysonFrame(0, newNodes[2], newNodes[0], false)]; - layer.QuickAddDysonShell(0, newNodes, newFrames, false); - foreach (var node in newNodes) + foreach (var sshell in supposedShells.OrderByDescending(shell => shell.vertCount)) { - node.RecalcSpReq(); - node.RecalcCpReq(); + if (sshell.vertCount <= 0) break; + + DysonNode[] newNodes = [layer.QuickAddDysonNode(0, sshell.posA), layer.QuickAddDysonNode(0, sshell.posB), layer.QuickAddDysonNode(0, sshell.posC)]; + DysonFrame[] newFrames = [layer.QuickAddDysonFrame(0, newNodes[0], newNodes[1], false), layer.QuickAddDysonFrame(0, newNodes[1], newNodes[2], false), layer.QuickAddDysonFrame(0, newNodes[2], newNodes[0], false)]; + if (layer.QuickAddDysonShell(0, newNodes, newFrames, false) == 0) + { + foreach (var frame in newFrames) + { + layer.QuickRemoveDysonFrame(frame.id); + } + foreach (var node in newNodes) + { + layer.QuickRemoveDysonNode(node.id); + } + continue; + } + foreach (var node in newNodes) + { + node.RecalcSpReq(); + node.RecalcCpReq(); + } + shellsChanged = true; + break; } - shellsChanged = true; } return shellsChanged; }