ModDeduplication¶
When the same mod is present in more than one source (Workshop subscription, local deploy under Documents\My Games\Stationeers\mods\, or a Repo source) and both copies are enabled in the in-game mod list, StationeersLaunchPad deduplicates them before handing the winning copy to BepInEx. Which copy wins depends on a priority table configurable via the mod config; the default is Repo (3) > Local (2) > Workshop (1). The losing copy has Enabled = false set on its ModInfo and is skipped at plugin-load time. This page documents the match key, the priority rule, the side effects on the mod-list UI, and the failure mode when the match key does not match.
Match key: Workshop handle, then ModID, then reference¶
StationeersLaunchPad.Metadata.ModSet.TryGetExisting(ModInfo mod, out ModInfo existing) is the single routine that decides whether a newly-enumerated ModInfo duplicates an already-known ModInfo. The check runs in order:
public bool TryGetExisting(ModInfo mod, out ModInfo existing)
{
if (mod.WorkshopHandle > 1 && byWorkshopHandle.TryGetValue(mod.WorkshopHandle, out existing))
{
return true;
}
if (!string.IsNullOrEmpty(mod.ModID) && byModID.TryGetValue(mod.ModID, out existing))
{
return true;
}
return all.TryGetValue(mod, out existing);
}
Rules that follow from the code:
- Workshop handle is checked first. A local deploy whose
About.xmlcarries the real numeric<WorkshopHandle>will be recognized as a duplicate of its Workshop-subscribed sibling even before theModIDpass runs. Setting<WorkshopHandle>0</WorkshopHandle>on un-published copies (per the repo-rootCLAUDE.md"Content: About.xml structure and XML safety" rule) keeps the handle test from matching; this is correct behavior forPlans/mods that have no Workshop presence. - ModID is the dedup key that catches developer workflows. A local dev copy and a Workshop-subscribed copy of the same mod share the
<ModID>inAbout.xml(both built from the same source) and therefore dedup reliably on that key even if the local copy carries<WorkshopHandle>0</WorkshopHandle>. - Folder name is not a dedup key. Two copies in different folders with different
ModIDvalues will both load, even if the folders are identically named. - Reference-equality fallback (
all.TryGetValue) is a hash-set contains-check against the sameModInfoinstance; it never matches two distinct enumerations.
Priority table and the winner¶
StationeersLaunchPad.Configs reads the DedupeMode config value at init and populates a four-slot priority array used by DisableDuplicates:
int[] array = ((value == "KeepLocal") ? new int[4] { 1, 2, 1, 3 }
: ((!(value == "KeepWorkshop")) ? new int[4] { 0, 2, 1, 3 }
: new int[4] { 1, 1, 2, 3 }));
// indices: [0]=dedupe-enabled, [1]=Local priority, [2]=Workshop priority, [3]=Repo priority
Three modes:
| DedupeMode | Local | Workshop | Repo | Winner order |
|---|---|---|---|---|
| (default) | 2 | 1 | 3 | Repo > Local > Workshop |
KeepLocal |
2 | 1 | 3 | Repo > Local > Workshop (same numbers; slot [0] flips from 0 to 1) |
KeepWorkshop |
1 | 2 | 3 | Repo > Workshop > Local |
Higher number wins. A Workshop subscription of SprayPaintPlus and a local deploy of the same mod both enabled, with default config, resolves to the local copy running and the Workshop copy disabled.
Disable step on the losing copy¶
ModList.DisableDuplicates() walks the mod list, calls ModSet.TryGetExisting for each enabled mod, and when a match is found, swaps in whichever ModInfo has the higher priority score. The excerpt:
int num = source switch
{
ModSourceType.Local => value, // Local priority
ModSourceType.Workshop => value2, // Workshop priority
ModSourceType.Repo => value3, // Repo priority
_ => int.MinValue,
};
int num3 = num; // computed the same way for the new ModInfo
if (num3 > num2)
{
ModInfo modInfo2 = modInfo;
modInfo = existing;
existing = modInfo2;
}
modSet.Remove(modInfo);
modSet.Add(existing);
modInfo.Enabled = false;
Logger.Global.LogWarning($"{item.Source} {item.Name} disabled in favor of {existing2.Source} {existing2.Name}");
Effects:
- The losing
ModInfohas itsEnabledfield set tofalsein memory. The decision is not written back tomodconfig.xml; it is recomputed at the next config load. - A
LogWarningis emitted toBepInEx/LogOutput.logof the formWorkshop SprayPaintPlus disabled in favor of Local SprayPaintPlus. This line is the first-stop diagnostic for "which copy actually ran." - The in-game mod-list UI still shows both entries. The losing copy appears with
Enabled = false(greyed-out in the UI). The player sees two rows; only one plugin DLL is handed to BepInEx for load. DisableDuplicatesis called fromLaunchPadConfig.StageConfiguring()at startup and fromHandleChange()whenever the player edits the mod list in the UI, so the decision refreshes on every mod-list change.
Failure mode: ModID mismatch defeats dedup¶
The developer-facing footgun: if the Workshop build and the local dev build carry different ModID values (typo, renamed mid-development, Workshop copy still on an old ModID, local copy on a new one) AND neither carries the same non-zero WorkshopHandle, TryGetExisting returns false for both and both are left Enabled = true.
Two plugin DLLs are then passed to BepInEx. BepInEx has its own dedup layer keyed on [BepInPlugin] GUID:
- If both DLLs declare the same BepInEx GUID, BepInEx loads only the first one it enumerates (plugin-path enumeration order is filesystem-dependent and not guaranteed between runs).
- If the two DLLs declare different BepInEx GUIDs, both load. Harmony patches from both are installed. The result is typically a double-patch (Postfixes run twice, Prefixes both gate the original) and visible misbehavior.
Rule for developers working on a mod in this monorepo while also subscribed to its Workshop listing: keep the <ModID> in About.xml identical between the committed source and any unreleased local tweaks. The same holds for the BepInEx GUID constant in the plugin class. A mismatch on either key is the cause when "my local changes aren't taking" despite both mods appearing enabled in the UI.
Diagnostic recipe¶
When a developer suspects the wrong copy ran:
- Open
BepInEx/LogOutput.log. Grep fordisabled in favor of. Each matching line is a dedup decision with source labels (Local,Workshop,Repo) on both sides. - If no such line exists for the mod in question, dedup did not fire. Confirm the two copies share
<ModID>inAbout.xml. If they do not, fix the source of the mismatch; do not rely onDedupeModeto paper over it. - For a live runtime check, InspectorPlus can dump
ModList.AllMods(types=[ModInfo], fields=[Name, ModID, Source, Enabled, WorkshopHandle]); compare the expected single enabled entry against the snapshot.
Verification history¶
- 2026-04-21: page created from decompiled
StationeersLaunchPad.dll(ModSet.TryGetExisting,ModList.DisableDuplicates,Configspriority init), in response to the question "if I'm subscribed on Workshop AND have a local deploy, what runs?" No prior central page on this topic.
Open questions¶
None at creation.