18 KiB
Agent Guidelines
Rules
- Update
AGENTS.mdafter completing every task. - Do not record a changelog in
AGENTS.md; modify the document content directly instead. - All documentation and code comments must be written in English.
- When you need to inspect game method implementations, decompile the original game DLL rather than the publicized copy in
AssemblyFromGame/(which was built with--stripand has all method bodies removed). Locate the original DLL using the same logic asUpdateGameDlls.ps1: read the Steam installation path from the Windows registry (HKCU\Software\Valve\Steam), parsesteamapps/libraryfolders.vdfto find the library containing DSP (AppID1366540), then decompile<game_root>/DSPGAME_Data/Managed/Assembly-CSharp.dlldirectly. - When looking up in-game terminology, use the localization files under
<game_root>/Locale/(located via the same Steam registry +libraryfolders.vdfmethod). TheNamesdirectory contains the dictionary keys;1033is the English translation directory;2052is the Simplified Chinese translation directory.
Project Overview
This repository is a collection of BepInEx mods for the game Dyson Sphere Program (DSP), a factory/automation game on Steam. Each subdirectory is an independent mod plugin loaded by the BepInEx framework at game startup. Mods use HarmonyLib to patch the game's compiled C# methods at runtime (prefix, postfix, and transpiler patches).
Tech Stack
- Language: C# (
net472/netstandard2.1, latest LangVersion) - Modding Framework: BepInEx 5.x
- Patching Library: HarmonyLib (runtime IL patching via
[HarmonyPatch]attributes) - Build System: Visual Studio solution (
DSP_Mods.sln), SDK-style.csprojper mod - Package Manager: NuGet (standard feed + BepInEx dev feed)
- Packaging:
ZipModMSBuild target (explicit, not post-build) produces Thunderstore-ready.zipfiles viapowershell.exe Compress-Archive - Game DLL references:
AssemblyFromGame/Assembly-CSharp.dllandUnityEngine.UI.dll - Notable dependencies: DSPModSave, NebulaMultiplayer API, CommonAPI, NLua, obs-websocket-dotnet, Mono.Cecil
Repository Structure
DSP_Mods/
├── DSP_Mods.sln # Visual Studio solution
├── AssemblyFromGame/ # Game DLLs used as compile-time references
├── UXAssist/ # Core UX mod + shared library (largest mod)
├── CheatEnabler/ # Cheat functions mod (depends on UXAssist)
├── Dustbin/ # Storage/tank dustbin mod
├── DustbinPreloader/ # BepInEx preloader for Dustbin
├── HideTips/ # Hides tutorial/tip popups
├── LabOpt/ # Lab performance optimizations
├── LabOptPreloader/ # BepInEx preloader for LabOpt
├── LogisticMiner/ # Logistic stations auto-mine ores
├── LuaScriptEngine/ # Lua scripting support for the game
├── MechaDronesTweaks/ # Mecha drone speed/energy tweaks
├── OverclockEverything/ # Speed/power multipliers for all buildings
├── PoolOpt/ # Memory pool optimization on save loading
├── UniverseGenTweaks/ # Universe generator parameter tweaks
├── UserCloak/ # Hides/fakes Steam account info
├── UpdateGameDlls/ # MSBuild helper project; runs UpdateGameDlls.ps1 before any mod compiles
└── CompressSave/ # Stub only (moved to external repo)
Mods Summary
| Mod | GUID | Description |
|---|---|---|
| UXAssist | org.soardev.uxassist |
Core QoL mod and shared library. Window resize, profile-based saves, FPS control, factory/logistics/navigation/Dyson Sphere tweaks, UI improvements, config panel UI, and Common/ + UI/ widget library shared by other mods. The Factory tab's building-buffer controls include a comparison tip with the original game values. The Logistics tab can push auto-config values to all existing facilities of a type on the current planet (per-setting Apply and per-category Apply All buttons), and can set both product limits of every Orbital Collector across all loaded factories; logic is in LogisticsPatch.Apply*/ForEach* and wired in UIConfigWindow. |
| CheatEnabler | org.soardev.cheatenabler |
Cheat pack (depends on UXAssist). Instant build, architect mode, infinite resources, power boosts, Dyson Sphere cheats with a bounded shell-count input (1–99,999), mecha invincibility, and more. |
| LogisticMiner | — | Makes logistic stations automatically mine ores and water from the current planet. |
| HideTips | — | Suppresses all tutorial popups, random tips, achievement/milestone cards, and skips the prologue cutscene. |
| MechaDronesTweaks | — | Configurable drone speed multiplier, skip stage-1 animation, reduce energy consumption. Successor to FastDrones. |
| OverclockEverything | — | Multiplies speed and power consumption of belts, sorters, assemblers, labs, miners, generators, ejectors, and silos. |
| PoolOpt | — | Shrinks all object pool arrays to actual used size on save load, then forces GC to reduce memory footprint. |
| UniverseGenTweaks | — | Adds Epic difficulty, expands max star count to 1024, allows rare veins and flat terrain on birth planet. |
| UserCloak | — | Prevents Steam leaderboard/achievement uploads; can fake or block Steam user identity. |
| Dustbin | — | Turns storage boxes and tanks into item-destroying dustbins. Supports Nebula multiplayer and DSPModSave. Requires DustbinPreloader. |
| DustbinPreloader | — | Mono.Cecil preloader that injects bool IsDustbin into StorageComponent and TankComponent before game load. |
| LabOpt | — | Optimizes stacked Matrix Lab updates via a rootLabId concept. Temporarily marked obsolete. Requires LabOptPreloader. |
| LabOptPreloader | — | Mono.Cecil preloader that injects int rootLabId into LabComponent before game load. |
| LuaScriptEngine | org.soardev.luascriptengine |
Embeds NLua runtime; loads .lua files from scripts/; exposes game lifecycle hooks and OBS WebSocket integration. |
| CompressSave | — | Stub only; functionality moved to external repository soarqin/DSP_Mods_TO. |
Build System
Shared MSBuild Configuration
Common properties and references are factored into two root-level files that MSBuild automatically imports for every project:
Directory.Build.props— sharedPropertyGroupdefaults (TargetFramework,AllowUnsafeBlocks,LangVersion,RestoreAdditionalProjectSources) and sharedItemGroups (BepInEx packages, game DLL references,Microsoft.NETFramework.ReferenceAssemblies), and a globalProjectReferenceto theUpdateGameDllshelper project (ensuring game DLLs are refreshed before any mod project resolves assembly references).Directory.Build.targets— defines theZipModandCopyToParentPackagetargets (see below).UpdateGameDlls.ps1— PowerShell script invoked by theUpdateGameDllshelper project; locates the DSP installation via Steam registry andlibraryfolders.vdf, compares DLL timestamps, and re-publicizes stale DLLs usingassembly-publicizer.
Individual .csproj files only declare what is unique to that project (GUID, version, extra packages, embedded resources).
Automatic Game DLL Update
AssemblyFromGame/ holds publicized copies of two game DLLs used as compile-time references. They are refreshed automatically before any mod project compiles by the UpdateGameDlls helper project.
The helper project (UpdateGameDlls/UpdateGameDlls.csproj) uses the Microsoft.Build.NoTargets SDK and is declared as a global ProjectReference in Directory.Build.props with ReferenceOutputAssembly=false, SkipGetTargetFrameworkProperties=true, and Private=false. This ensures that MSBuild's dependency graph guarantees the helper project completes before any mod project resolves assembly references, with no need for file locks or conditional triggers.
The UpdateGameDlls.ps1 script:
- Reads the Steam installation path from the Windows registry (
HKCU\Software\Valve\Steam). - Parses
steamapps/libraryfolders.vdfto find the library that contains DSP (AppID1366540). - Locates
<game_root>/DSPGAME_Data/Managed/. - For each DLL (
Assembly-CSharp.dll,UnityEngine.UI.dll): if the game copy is newer than the local copy, runsassembly-publicizer … --strip --overwriteto regenerate the local file and stamps it with the source timestamp.
To explicitly update game DLLs:
dotnet build UpdateGameDlls\UpdateGameDlls.csproj
Prerequisite: assembly-publicizer must be installed as a .NET global tool:
dotnet tool install -g BepInEx.AssemblyPublicizer.Cli
If the tool is missing or DSP is not found, the script prints a warning and continues without failing the build.
Packaging
Packaging is a separate, explicit build target — it does not run on every normal build.
To produce a Thunderstore-ready zip:
dotnet build -t:ZipMod -c Release
The ZipMod target (defined in Directory.Build.targets) uses pure MSBuild tasks (MakeDir, Copy, Delete) plus powershell.exe -NoProfile -Command for Compress-Archive. Calling powershell.exe as an explicit executable path works correctly from any shell environment (cmd, PowerShell, bash/WSL).
Note: the target is named
ZipModrather thanPackbecausePackis a reserved target name in the .NET SDK (used for NuGet packaging) and would be silently intercepted.
Per-project packaging properties (set in the project's PropertyGroup):
| Property | Default | Description |
|---|---|---|
PackHasChangelog |
false |
Include CHANGELOG.md in the zip |
PackUsePluginsLayout |
false |
Use plugins/ + patchers/ folder layout (Dustbin, LabOpt) |
PackPreloaderTargetDir |
(empty) | Preloader projects: destination folder for CopyToParentPackage |
Preloader projects (DustbinPreloader, LabOptPreloader) use CopyToParentPackage instead of ZipMod:
dotnet build -t:CopyToParentPackage -c Release
This copies the preloader DLL into the sibling main mod's package/patchers/ directory, ready to be zipped by the main mod's ZipMod target.
Version Management
Each mod's <Version> property in its .csproj file is the single source of truth for the version number. The version_number field in package/manifest.json is automatically synchronized from <Version> during the ZipMod target — no manual update of manifest.json is needed.
Release workflow:
- Update
<Version>in the mod's.csprojfile (e.g.,<Version>1.2.3</Version>) - Run
dotnet build -t:ZipMod -c Release—manifest.jsonis updated automatically before packaging
The sync is implemented as an inline PowerShell Exec step inside the ZipMod target in Directory.Build.targets. It uses a regex replace that preserves the original UTF-8 BOM encoding and CRLF line endings of manifest.json. Preloader projects (which have no manifest.json) are safely skipped via a Condition="Exists(...)" guard.
Key Architectural Patterns
- Shared library:
UXAssistacts as a common library.CheatEnablerandUniverseGenTweaksreferenceUXAssist.csprojdirectly to reuseCommon/,UI/, and config panel infrastructure. - Centralized mod-feature lifecycle:
UXAssist.Common.ModFeatures.ModFeatureRegistryholds shared static lists of mod features discovered across all mods. Only UXAssist drives the shared deferred lifecycle (StartAll/UninitAll/OnInputUpdateAll/OnUpdateAll); these dispatchers areinternalso dependent mods (separate assemblies, noInternalsVisibleTo) cannot call them and re-trigger other mods' features. A feature'sInitruns eagerly when it is registered (viaDiscover/Register), preserving the originalAwake-phase timing that keybind registration and other early setup rely on — the game'sUIOptionWindow._OnCreatecopies registered keybinds only after all plugins have finished loading. Dependent mods only callModFeatureRegistry.Discover(Assembly.GetExecutingAssembly())(and optionallyRegister<T>()) in theirAwake. UXAssist begins the deferred lifecycle from its ownStart; if a dependent feature is discovered after that transition, the registry starts it immediately after initialization so it cannot missStart. 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:
DustbinPreloaderandLabOptPreloaderuse 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.csprovides bilingual (EN + ZH) string lookup used across UXAssist and CheatEnabler. Localization keys are declared aspublic const stringin per-project registration classes (UXAssist/Common/I18NKeys.cs,CheatEnabler/Localization.cs,UniverseGenTweaks/Localization.cs) and registered through a singleRegister()call from each mod'sAwake(). 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.prebuildCountis computed asprebuildCursor - 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.GenerateGeometryrasterization as closely as possible, but can still differ from the final frame-generated shell polygon at numerical boundaries. Max-output generation must treatQuickAddDysonShellfailure as a candidate rejection and try the next candidate rather than assuming the estimate is exact. - Fail-soft patch application:
PatchImpl<T>.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 callingConfigEntry.SettingChangeddelegate chain, desyncing config UI from config values. On failure it logs aLogErrorwith the feature type name, rolls back viaUnpatchSelf(), and leaves_patchnull so a laterEnable(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 (SettingChangedhandlers, patchOnEnable/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); theAutoConstructPatchpostfix onPlayerAction_Rts.GameTickonly implements the fly-to-target behavior, andAutoConstructPatch.OnEnablelogs its visibility predicate inputs once per enable as a remote-diagnosis aid. - Auto-navigation modes: The legacy auto-navigation and the new navigation algorithm are mutually exclusive; legacy mode wins if both configuration values load as enabled. The shared
ToggleAutoCruisekey dispatches to only the selected implementation. New-navigation teardown is unconditional for configuration, target, capability, and lifecycle failures, while the manual-input option controls only user-override behavior. Its session state and cloned status label are reset throughGameLogic.OnGameEndand patch teardown. - 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.TranspilerGuardprovides a reusableCodeMatcher.Finishhelper that returns original instructions when a matcher becomes invalid. - Mod-compatibility reflection: Use
UXAssist.Common.ModCompat.ModCompatHelperfor 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. UseUXAssist.Common.Utils.DysonSphereReflectionfor the DSPOptimizations-compatibleDysonSphereLayerprivate fields (totalNodeSP,totalFrameSP,totalCP) instead of resolving them locally in each consumer. - Build quality gates: Root
.editorconfigdefines suggestion-only C# style conventions.Directory.Build.propsenablesTreatWarningsAsErrorswithNoWarn>0618for the expected obsolete-API usage, so any new warning fails the build..github/workflows/build.ymlruns a Release build and packages the three main mods on every push/PR. - Save persistence: Mods that need to persist data use the
IModCanSaveinterface from DSPModSave.