Files

18 KiB
Raw Permalink Blame History

Agent Guidelines

Rules

  • Update AGENTS.md after 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 --strip and has all method bodies removed). Locate the original DLL using the same logic as UpdateGameDlls.ps1: read the Steam installation path from the Windows registry (HKCU\Software\Valve\Steam), parse steamapps/libraryfolders.vdf to find the library containing DSP (AppID 1366540), then decompile <game_root>/DSPGAME_Data/Managed/Assembly-CSharp.dll directly.
  • When looking up in-game terminology, use the localization files under <game_root>/Locale/ (located via the same Steam registry + libraryfolders.vdf method). The Names directory contains the dictionary keys; 1033 is the English translation directory; 2052 is 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 .csproj per mod
  • Package Manager: NuGet (standard feed + BepInEx dev feed)
  • Packaging: ZipMod MSBuild target (explicit, not post-build) produces Thunderstore-ready .zip files via powershell.exe Compress-Archive
  • Game DLL references: AssemblyFromGame/Assembly-CSharp.dll and UnityEngine.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 (199,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 — shared PropertyGroup defaults (TargetFramework, AllowUnsafeBlocks, LangVersion, RestoreAdditionalProjectSources) and shared ItemGroups (BepInEx packages, game DLL references, Microsoft.NETFramework.ReferenceAssemblies), and a global ProjectReference to the UpdateGameDlls helper project (ensuring game DLLs are refreshed before any mod project resolves assembly references).
  • Directory.Build.targets — defines the ZipMod and CopyToParentPackage targets (see below).
  • UpdateGameDlls.ps1 — PowerShell script invoked by the UpdateGameDlls helper project; locates the DSP installation via Steam registry and libraryfolders.vdf, compares DLL timestamps, and re-publicizes stale DLLs using assembly-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:

  1. Reads the Steam installation path from the Windows registry (HKCU\Software\Valve\Steam).
  2. Parses steamapps/libraryfolders.vdf to find the library that contains DSP (AppID 1366540).
  3. Locates <game_root>/DSPGAME_Data/Managed/.
  4. For each DLL (Assembly-CSharp.dll, UnityEngine.UI.dll): if the game copy is newer than the local copy, runs assembly-publicizer … --strip --overwrite to 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 ZipMod rather than Pack because Pack is 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:

  1. Update <Version> in the mod's .csproj file (e.g., <Version>1.2.3</Version>)
  2. Run dotnet build -t:ZipMod -c Releasemanifest.json is 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: UXAssist acts as a common library. CheatEnabler and UniverseGenTweaks reference UXAssist.csproj directly to reuse Common/, UI/, and config panel infrastructure.
  • Centralized mod-feature lifecycle: UXAssist.Common.ModFeatures.ModFeatureRegistry holds shared static lists of mod features discovered across all mods. Only UXAssist drives the shared deferred lifecycle (StartAll/UninitAll/OnInputUpdateAll/OnUpdateAll); these dispatchers are internal so dependent mods (separate assemblies, no InternalsVisibleTo) cannot call them and re-trigger other mods' features. A feature's Init runs eagerly when it is registered (via Discover/Register), preserving the original Awake-phase timing that keybind registration and other early setup rely on — the game's UIOptionWindow._OnCreate copies registered keybinds only after all plugins have finished loading. Dependent mods only call ModFeatureRegistry.Discover(Assembly.GetExecutingAssembly()) (and optionally Register<T>()) in their Awake. UXAssist begins the deferred lifecycle from its own Start; if a dependent feature is discovered after that transition, the registry starts it immediately after initialization so it cannot miss Start. 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: DustbinPreloader and LabOptPreloader use 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.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<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 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.
  • 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 ToggleAutoCruise key 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 through GameLogic.OnGameEnd and 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.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. 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.