Starter kit for SPT 4.x client mods (BepInEx + Harmony via SPT.Reflection.Patching).
This repository is the template and a runnable example mod. Build it to verify your game paths, or run the wizard to scaffold a new project.
-
Copy
Directory.Build.props.user.exampletoDirectory.Build.props.userand setTarkovDirto your SPT install. -
Validate your install (optional):
.\scripts\Validate-Install.ps1 -
Build:
dotnet build -c DebugDebug builds deploy the DLL to
BepInEx/plugins/<AssemblyName>/and remove the mod config file so defaults reload on next launch. -
Launch SPT. You should see a BepInEx log when the plugin loads and a message dialogue once on the main menu.
From this folder:
.\New-SptMod.ps1The wizard asks for mod name, author prefix, GUID prefix, display title, output folder, TarkovDir (
validated), and whether to run git init. It runs Validate-Install on the new project when finished.
Non-interactive:
.\New-SptMod.ps1 -Name MyRaidMod -OutputDir C:\Dev\SPT-MyRaidMod `
-TarkovDir "D:\Games\Escape from Tarkov" -AuthorPrefix Acme -GuidPrefix com.acme -GitInit -NonInteractive| Parameter | Description |
|---|---|
-Name |
PascalCase mod name (e.g. MyRaidMod) |
-AuthorPrefix |
Namespace segment (default Softwyx → Acme.MyRaidMod) |
-GuidPrefix |
Plugin GUID prefix (default com.softwyx → com.acme.mymod) |
-OutputDir |
Folder to create (must not exist) |
-TarkovDir |
SPT install path (required; written to Directory.Build.props.user) |
-DisplayName |
Human-readable plugin title |
-LogPrefix |
2–6 character BepInEx log prefix |
-GitInit |
Run git init and an initial commit |
-NonInteractive |
Skip prompts; requires -Name, -OutputDir, and -TarkovDir |
Version lives in the .csproj only. Each build bumps the patch segment and regenerates src/PluginInfo.Version.g.cs
for [BepInPlugin(..., version)]. Edit major/minor in the .csproj. Disable auto-bump (just invert the default value
in Softwyx.SptModTemplate.csproj to take control over versioning):
dotnet build -p:IncrementVersionOnBuild=falsedotnet build -c PackageProduces Package/<AssemblyName>-<version>.zip (WinRAR or 7-Zip required). Release and Package builds are
Authenticode-signed by default (see Code signing below).
Release and Package configurations sign the DLL after build via Directory.Build.targets → scripts/SignAssembly.ps1.
| Setting | Default |
|---|---|
SignAssembly |
true |
SignAssemblyConfigurations |
Release;Package |
SigningCertSubject |
CN=Softwyx.SptModTemplate (override per mod in Directory.Build.props) |
Certificate resolution: PFX file (if configured) → cert store by subject → auto-create self-signed cert. Set
SignAssembly=false in Directory.Build.props.user to skip. See Directory.Build.props.user.example for PFX
path/password options. Never commit *.pfx or certs/.
- Menu: patch on
EFT.UI.MenuScreen.Show→ welcome dialogue viaItemUiContext.ShowMessageWindow - Raid: patch on
GameWorld.OnGameStarted→ log-only stub (enabled gate onSettings.Enabled) - Dependencies:
[BepInDependency("com.SPT.core", "4.0.0")]plus commented optional examples in the plugin class
Client strings live in locales/<id>.json (e.g. locales/en.json). Keys must use your mod prefix — the template uses
sptmodtemplate_* (see Localization/LocaleKeys.KeyPrefix).
| Type | Use |
|---|---|
LocaleLoader.Get(key) |
Merged game locale + mod pack (dialogs, formatted strings) |
LocaleLoader.TryGet(key) |
Mod-pack lookup only; returns null when the key is missing |
GameLocaleAccess.TryLocalize(key) |
Vanilla / SPT game locale via the game's Localized() string extension (same path as ItemLocale) |
ItemLocale.Name(templateId) / ShortName(...) |
EFT item display names via the game's .Localized() extension |
DisplayLabelResolver.Resolve(...) |
UI labels: game locale → mod fallback → raw id |
DisplayLabelResolver.ResolveFieldName(...) |
PascalCase field names → camelCase game keys → mod fallback |
LocalizedTextView.BindKey(transform, key, labelChild) |
UI with EFT.UI.LocalizedText + TMP |
LocaleApplicationLanguagePatch |
Merges JSON when LocaleManagerClass.UpdateApplicationLanguage runs |
LocaleLoader |
Validates all locales/*.json, builds a catalogue, merges into the game; runtime apply uses ContainsCulture |
On Awake, PreloadDefaultLocale() runs first — if it fails, no patches are enabled. Do not patch UpdateLocales (can
deadlock startup). Debug builds copy locales/ next to the DLL under BepInEx/plugins/<AssemblyName>/locales/.
Add packs as locales/fr.json, etc. Missing keys fall back to en.json. For UI built in code, prefer LocalizedText
with a key over hard-coded strings.
- Persist codenames in saved data (
bigmap,AmmoUsed, exit reason ids). Resolve labels in UI only when rendering player-facing text. - Game keys first via
GameLocaleAccess— uses the game'sLocalized()extension (same strings the client uses, e.g."bigmap"→"Customs"). BrowseSPT/SPT_Data/database/locales/global/en.jsonfor key names. - Mod keys second — your prefixed entries in
locales/*.jsonfor copy the game does not own (feature titles, custom counters, fallbacks). - Logs stay English/raw — do not localize BepInEx output; use
DisplayLabelResolver/LocaleLoaderonly for player UI. This guarantees that most people can read the logs and will save you a lot of unneeded bug reports.
The welcome dialogue demonstrates DisplayLabelResolver.Resolve("bigmap", LocaleKeys.ExampleMapFallback) in
Core/ModLifecycle.cs.
For a readable and maintainable code, never use decompiler names (GClass*, Dictionary_*, …) in feature code. Confine them under src/Interop/.
This template uses the following segregation logic:
| File | Purpose |
|---|---|
GameTypeAliases.cs |
global using aliases for obfuscated types (e.g. MenuScreenController) |
GameAssemblyNames.cs |
Obfuscated field/method name strings for reflection |
GameMemberAccess.cs |
GetField / GetProperty via Harmony AccessTools |
GameLocaleAccess.cs |
Read merged game locale via Localized() (same as ItemLocale) |
BepInEx Configuration Manager custom drawers live in src/Config/:
| Helper | Use for |
|---|---|
ConfigFloatUi.Attributes(...) |
Float sliders with decimal precision and step snapping |
ConfigIntegralUi.IntAttributes(...) |
Int sliders with step snapping |
ConfigIntegralUi.LongAttributes(...) |
Long sliders with step snapping |
See Settings.cs for float/int/long examples. Call ConfigFloatUi.SnapEntries(...) after bind to quantize existing
saved values.
| Script | Purpose |
|---|---|
New-SptMod.ps1 |
Interactive wizard to scaffold a new mod |
scripts/Validate-Install.ps1 |
Check TarkovDir and required game/SPT DLLs |
scripts/IncrementVersion.ps1 |
Patch bump + PluginInfo.Version.g.cs (MSBuild) |
scripts/package.ps1 |
Release ZIP (Package configuration) |
scripts/SignAssembly.ps1 |
Authenticode signing (MSBuild SignAssembly target) |
scripts/ReencodeUtf8.ps1 |
Normalize UTF-8 no BOM + LF line endings |
Scaffolded projects keep TEMPLATE-ATTRIBUTION.md and a README credit line. See LICENSE.