Mod Structure
Mod Structure
Part of the 7DTD Modding Knowledgebase. Covers folder layout, ModInfo.xml, DLL loading, and the modlet system.
Folder Layout
<7DTD Install>/Mods/
└── YourModName/
├── ModInfo.xml ← required — mod identity and metadata
├── YourMod.dll ← compiled C# code (goes in mod root)
├── Config/
│ ├── blocks.xml ← XPath patch for block definitions
│ ├── Localization.txt ← translation strings
│ ├── XUi/
│ │ ├── windows.xml ← XPath patch appending window definitions
│ │ └── xui.xml ← XPath patch registering window groups
│ ├── XUi_Common/ ← shared control templates (available in all UI contexts)
│ │ ├── controls.xml
│ │ └── styles.xml
│ └── XUi_Menu/ ← main menu UI modifications (separate from in-game UI)
│ └── windows.xml
├── Resources/ ← Unity asset bundles (.unity3d) for custom audio/models/textures
├── Textures/ ← Raw PNG/texture files for UI (referenced as @modfolder:Textures/...)
│ └── UILoadingTips/ ← Custom loading screen tip images
├── UIAtlases/
│ ├── ItemIconAtlas/ ← custom item icon PNGs (game auto-discovers by filename)
│ ├── UI/ ← custom UI sprites
│ └── UIAtlas/ ← atlas textures
├── News/ ← (optional) custom news/announcement XML
├── Music/ ← (optional) background music files
└── Images/ ← (mod-specific) runtime assets (PixelPaste-specific)
Notes:
XUi/patches the in-game UI;XUi_Menu/patches the main menu UI — they are separate contextsXUi_Common/is loaded in both contexts; use it for controls/styles shared across bothUIAtlases/ItemIconAtlas/PNG files are named to match item names — the game auto-maps them as item iconsResources/.unity3dbundles are referenced from XML as#@modfolder:Resources/file.unity3d?AssetName
ModInfo.xml
<?xml version="1.0" encoding="UTF-8"?>
<xml>
<Name value="YourModName" />
<DisplayName value="Your Mod Display Name" />
<Description value="Short description." />
<Author value="YourName" />
<Version value="1.0.0" />
<Website value="" />
<SkipWithAntiCheat value="true" />
</xml>
Note:
SkipWithAntiCheat value="true"marks the mod as requiring EAC to be disabled. Set this for any mod that includes a C# DLL. The game will skip loading the mod (rather than crash) when EAC is active.
<Name>must match^[0-9a-zA-Z_\-]+$— letters, digits, underscore, and hyphen only. No spaces. A space (or any other character) makes the loader logERR [MODS] <Folder>/ModInfo.xml does not define a valid non-empty Name … ignoringand skip the ENTIRE mod (no items.xml merged, no DLL loaded, no localization). The mod folder name on disk can still have spaces — only the<Name>element value is regex-checked. Put any pretty/spaced label in<DisplayName>instead. Symptom: "I deployed the mod and nothing appears in-game" with no items.xml / sounds.xml load errors anywhere — they don't appear because the mod was rejected before XML merge ever ran.
DLL Loading
- The compiled
.dllgoes in the mod root (same folder asModInfo.xml) - The game automatically discovers and loads it at startup
- EAC (Easy Anti-Cheat) must be disabled to load C# code mods — launch via the non-EAC shortcut or Steam launch option
-noeac - The DLL targets .NET Framework 4.6.1
- Reference the game's managed DLLs from
7DaysToDie_Data/Managed/:Assembly-CSharp.dll— main game logicUnityEngine.dll— Unity baseUnityEngine.CoreModule.dllUnityEngine.ImageConversionModule.dll— forTexture2D.LoadImageUnityEngine.InputLegacyModule.dll— required for the legacyInputclass (Input.GetKey,Input.GetKeyDown, etc.). Modern Unity splitsInputout of CoreModule into this module;KeyCoderesolves from CoreModule butInputdoes not. Without this reference you getCS0103: The name 'Input' does not exist in the current context.LogLibrary.dll— defines theLogclass (Log.Out/Log.Warning/Log.Error).Logis not inAssembly-CSharp; without this reference you getCS0103: The name 'Log' does not exist in the current context.UnityEngine.IMGUIModule.dll— required for a legacyOnGUIoverlay (GUI,GUIStyle,GUI.DrawTexture,GUI.Label,GUI.Button). Without it:CS1069: 'GUIStyle' … forwarded to assembly 'UnityEngine.IMGUIModule'. See [[IMGUI Tracker Stack]].UnityEngine.TextRenderingModule.dll— required alongside IMGUI forGUIStyletext config:FontStyleandTextAnchorlive here (CS0012: 'FontStyle'/'TextAnchor' is defined in an assembly that is not referenced … UnityEngine.TextRenderingModule).
Target
netstandard2.1when the build machine has no .NET Framework targeting pack (e.g. only the dotnet 8/10 SDK, noReference Assemblies/Microsoft/Framework/.NETFramework). An SDK-style.csprojwith<TargetFramework>netstandard2.1</TargetFramework>, the game DLLs as<Reference>with<Private>false</Private>, builds with plaindotnet buildand the resulting DLL loads in the game's Mono runtime (only touches APIs the runtime provides). Usenetstandard2.1, not2.0: the game DLLs referencenetstandard 2.1.0.0, so anetstandard2.0project fails with CS1705 ("Assembly 'Assembly-CSharp' … uses 'netstandard, Version=2.1.0.0' which has a higher version than referenced assembly 'netstandard, Version=2.0.0.0'"). TheMSB3277version-conflict warnings against Unity module DLLs are noise — ignore them. Verified with Uncover.
Overriding game methods with
ReadOnlySpan<char>params requires compiling against the game's own BCL. Since the ~2026-06-29 game update, virtual signatures likeEntity.AllowActivationCommand(ReadOnlySpan<char>, EntityPlayerLocal)useReadOnlySpan<T>, andAssembly-CSharp's typeref for it points at Unity'smscorlib 4.0.0.0(notnetstandard). Anet48target has no Span at all (CS0234), and a plainnetstandard2.1target resolvesReadOnlySpanfrom the SDK ref pack'snetstandard.dll— a different type identity, so the override still fails with CS0115 "no suitable method found to override". Fix: keep<TargetFramework>netstandard2.1</TargetFramework>but add<NoStdLib>true</NoStdLib>+<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>and reference the game'smscorlib.dll,netstandard.dll,System.dll,System.Core.dllfrom7DaysToDie_Data\Managedas<Reference>items with<Private>false</Private>. Verified with Oppressor (2026-07-09). To check which assembly a typeref points at, read the metadata withSystem.Reflection.Metadata(PEReader→TypeReferences→ResolutionScope) — runtime reflection lies because .NET 8 unifies corlibs on load.
ConsoleCmdAbstractoverrides must bepublic, notprotected. Adding a console command from a mod needs no Harmony —SdtdConsolefinds everyIConsoleCommandimplementor across all loaded assemblies by reflection, so a public class with a parameterless ctor extendingConsoleCmdAbstractauto-registers. But the shippedAssembly-CSharpis publicized:getCommands/getDescription/getHelpcarry[PublicizedFrom(EAccessModifier.Protected)]and their real metadata ispublic. Decompilers printprotected, but declaringprotected overridefails with CS0507 "cannot change access modifiers when overriding" — usepublic override. Commands are typed in the F1 console without a leading slash. See [[Map Fog & Reveal]].
UnityEngine.AnimationModule.dll— forAnimator(e.g. stamping door animator state). General rule:UnityEngine.dllis only a facade that type-forwards to the per-feature module DLLs; the compiler errorCS1069: The type name 'X' could not be found in the namespace 'UnityEngine'. This type has been forwarded to assembly '...Module'names the exact module DLL to add fromManaged/(reference it with<Private>false</Private>like the others).
Numpad / NumLock gotcha: On Windows, when NumLock is off the numeric keypad does not emit
KeyCode.Keypad*at all — the OS sends the navigation keys instead (numpad 8→UpArrow, 2→DownArrow, 4→LeftArrow, 6→RightArrow, 9→PageUp, 7→Home, 1→End, 3→PageDown, 0→Insert,.→Delete; numpad 5 sendsVK_CLEAR, which has no usable UnityKeyCode). SoInput.GetKeyDown(KeyCode.Keypad6)silently never fires for a user with NumLock off — no error, key just does nothing. Symptom: "the numpad controls don't work." Fix: accept both theKeypad*code and its nav-key alias for each binding, and never useKeypad5as a modifier (pickLeftShift/RightShiftinstead). Airstrike's designator laser-origin tuner (AirstrikeLaserOrigin.HandleAdjustInput) does this.
OnHoldingUpdateis a throttled tick, not a per-frame callback:ItemAction.OnHoldingUpdatedoes not fire every rendered frame, so pollingInput.GetKeyDowninside it silently drops most presses —GetKeyDownis true for only one frame per press, and that frame usually lands between the throttled calls. Symptom: "pressing several times only moves once, then a long delay before it'll respond again." Fix: poll input from a real UnityUpdate(). Attach a smallMonoBehaviourto a GameObject you already own (e.g. the laser object) and do theInputpolling there; keepOnHoldingUpdatefor visuals only. Airstrike's designator does this viaAirstrikeLaserTuner : MonoBehaviour. (Input.GetKey/held-state polling tolerates the throttle, but edge detection viaGetKeyDowndoes not.)
Silent-deploy gotcha:
deploy.ps1runsbuild.ps1first and aborts the whole deploy (robocopy never runs) if the build fails. A compile error therefore leaves the old DLL in the Mods folder, and modman'sstatusstaysyellowwithdeployedIdunchanged. Symptom: "I deployed and restarted but my code change does nothing." Always confirm the build actually succeeded — check that the rootAirstrike.dlltimestamp is fresh, or grep the DLL bytes for a new symbol name.
Running-game DLL lock: If 7DTD is running with a mod DLL loaded,
robocopycan fail replacing<GameDir>\Mods\<Mod>\<Mod>.dllwithERROR 1224 (0x000004C8) ... user-mapped section open. Source build may still succeed and non-DLL files may copy, but the deployed DLL remains old. Fully quit the game, redeploy, then relaunch;xui reloadcannot load a new DLL.
Entry Point
Implement IModApi to get a callback when the mod loads:
public class MyMod : IModApi
{
public void InitMod(Mod _modInstance)
{
// _modInstance.Path = absolute path to the mod folder
string modPath = _modInstance.Path;
Log.Out("[MyMod] Loaded from: " + modPath);
}
}
Only one class in the DLL needs to implement IModApi. The game finds and calls it automatically.
Loading Order
The game loads mods in this sequence:
- Discover mods — scans
Mods/directory - Load DLLs — loads each mod's assembly
- Call
InitMod()— invokesIModApi.InitMod()for each mod - Load Config XMLs — parses and merges all
Config/*.xmlpatches (blocks, items, etc.) - Load Localization — reads
Config/Localization.txtfrom each mod - Initialize world — loads or creates the game world
Key insight: Since InitMod() runs before Config XMLs and Localization are loaded, mods can dynamically generate config files during initialization. This enables items, blocks, or localization entries that depend on runtime conditions (e.g., user-provided content files) rather than being hardcoded at build time.
XML Config Patching
All XML files in Config/ are XPath patch files — they modify vanilla game XML rather than replacing it. See [[XML Patching (XPath)]] for the full reference.
Localization
Translation strings live in Config/Localization.txt. See [[Localization]].
Custom UI Sprites (UIAtlases)
A mod can ship XUi sprites without an asset bundle: every PNG dropped in
UIAtlases/UIAtlas/<name>.png is loaded at startup into the in-game UIAtlas
as a sprite named <name>, usable directly as sprite="<name>" in XUi XML.
Plain quads — no slicing metadata, so type="sliced" only makes sense for
sprites drawn with uniform borders. New/changed PNGs are xui-reload kind
(no restart). The Elevator mod generates its panel art deterministically with
tools/gen_ui_sprites.ps1 (run under Windows PowerShell 5.1, not pwsh —
System.Drawing).
Build & Deploy
See build.ps1 in the project root. After any code or XML change, always run:
powershell -ExecutionPolicy Bypass -File build.ps1
The game loads from its own Mods/ directory — the project directory is not used at runtime.
ModForge Deploy Marker
For ModForge-managed workspaces, a new or repaired mod scaffold is not considered ready until it has a root-level deploy.ps1. Codex sessions should not run deploy scripts or launch the game directly. To request deployment, create or touch the mod root marker:
New-Item -ItemType File -Force -Path "<ModRoot>\.modforge-deploy" | Out-Null
ModForge watches for that marker and runs deployment outside the coding session.
If Test-Path is false immediately after a successful marker touch, ModForge may have already consumed/deleted the marker to start queued deployment; do not use Get-Item as the marker existence check.
.modforge-state.json stores ModForge's per-kind drift counters as numeric strings under kinds.restart, kinds.xui, and kinds.assets. If a merge leaves conflict markers inside those values, resolve each conflicted counter to the highest numeric side so the counter remains monotonic and the file stays valid JSON.
Build Script Path Resolution
Root-level build.ps1 wrappers should not assume the process working directory is the mod root. Resolve the root from MODFORGE_MOD_DIR when present, otherwise from $MyInvocation.MyCommand.Path/$PSScriptRoot, and normalize any \\?\ extended-length path prefix before using Push-Location or passing paths to tools.
Build wrappers should also map the common 7DTD install variables before invoking MSBuild:
$gameDir = if ($env:GameDir) { $env:GameDir } elseif ($env:SEVEN_DTD_CLIENT) { $env:SEVEN_DTD_CLIENT } elseif ($env:SEVEN_DAYS_TO_DIE_PATH) { $env:SEVEN_DAYS_TO_DIE_PATH } else { "<default install>" }
dotnet build $project -c Release "/p:GameDir=$gameDir"
Project files can support the same convention by accepting GameDir, SevenDaysToDiePath, SEVEN_DTD_CLIENT, and SEVEN_DAYS_TO_DIE_PATH, then validating that $(GameDir)\7DaysToDie_Data\Managed\Assembly-CSharp.dll and required mod DLLs such as Mods\0_TFP_Harmony\0Harmony.dll exist before ResolveReferences.
Wrong-
GameDirbuild produces a misleading flood of CS0246 errors. If adotnet buildruns with aGameDirthat doesn't exist on the machine, every<Reference>HintPath points at a missing DLL and is silently dropped, so the compiler reportsCS0246: 'Color'/'Vector3'/'GUIStyle'/'Input' could not be foundacross all files — including ones that have always compiled. The errors look like a code problem but the real cause is unresolved game references. Fix: pass the actual install dir. On this workstation the game is on the D: Steam library, not the csproj'sC:\Program Files (x86)default:dotnet build src/FPV.csproj -c Release "-p:GameDir=D:\SteamLibrary\steamapps\common\7 Days To Die"(Find it from
steamapps/libraryfolders.vdf, or just check for<lib>/steamapps/common/7 Days To Die/7DaysToDie_Data/Managed/Assembly-CSharp.dll.) modman's own deploy build already knows the right path; this only bites local command-line builds.
Stale second install produces misleading CS0115 errors. This workstation also has an older copy at
D:\7 Days To Die(not the Steam library one). Building — orilspycmd-decompiling — against it resolves references fine but the oldBlockvirtual signatures don't match, so long-stable files fail withCS0115: no suitable method found to override(OnBlockActivated,GetBlockActivationCommands, …). The errors look like a code regression; the real cause is the wrong Assembly-CSharp. The live install isD:\SteamLibrary\steamapps\common\7 Days To Die(2026-07; confirm againstmcp__modman__status'sdeployedPath).
Mod Gitignore Unity Entries
Always include these Unity workspace ignores in newly-created or repaired mod .gitignore files. The root-level hand-authored mod solution/project files and src/ project are kept; only Unity-generated files under UnityProject/ are ignored. If Unity has also created root-level Library/, ProjectSettings/, or Temp/ folders in a mod whose real Unity project is under UnityProject/, treat those root folders as generated noise and ignore them too.
# Unity editor — all regenerated from Assets/ + ProjectSettings/
UnityProject/Library/
UnityProject/Logs/
UnityProject/Temp/
UnityProject/Obj/
UnityProject/obj/
UnityProject/UserSettings/
UnityProject/MemoryCaptures/
# Unity auto-generated IDE files (the hand-authored mod sln/csproj
# at the repo root + src/ are kept; this only catches UnityProject/)
UnityProject/*.csproj
UnityProject/*.sln
UnityProject/*.user
# Unity build artifacts
UnityProject/*.apk
UnityProject/*.aab
UnityProject/*.unitypackage
UnityProject/headless_build.log
Dedicated Server Deployment
The dedicated server is a separate Steam app with its own install directory (typically 7 Days To Die Dedicated Server/). It has its own Mods/ folder that is independent of the client game's Mods/ folder.
Critical: When connecting to a dedicated server, the server sends the block/item ID map to clients. Clients receive block definitions from the server, not from their own local config files. If a block is defined in the client's mod but not in the server's mod, the client will never see it — it won't appear in the creative menu, giveself won't work, and it can't be placed.
Deploy to Both
Any mod that defines blocks, items, recipes, or localization must be deployed to both locations:
Client: <7DTD Install>/Mods/YourMod/
Server: <7DTD Dedicated Server Install>/Mods/YourMod/
All Config/ XML files and Localization.txt must match between client and server. DLLs should also be deployed to both (server needs them for block classes, Harmony patches, etc.).
Starting the Dedicated Server
The dedicated server must be started from its own install directory — startdedicated.bat resolves paths relative to its working directory:
cd "<7DTD Dedicated Server Install>"
./startdedicated.bat
Starting it from a different working directory will cause config loading failures.