SDModKit Docs v1.3.3
DE

StellarDrive · MelonLoader · SDModKit 1.3.3

SDModKit Developer Documentation

Everything you need to build your own parts, items, research and logic for StellarDrive (also written “Stellar Drive”) with the StellarDrive Mod Kit (SDModKit) – from your first JSON file to a full C# mod with MelonLoader.

Overview

SDModKit (“StellarDrive Mod Kit”) is a shared MelonLoader library by Krunk that many StellarDrive mods build on. It handles the hard parts of modding for you: cloning new parts into the game, adding build menu entries, sockets and cables, configuration menus, saving into the world save, multiplayer sync, keybinds and its own in-game mod menu.

DisclaimerThis is an unofficial community documentation. SDModKit is not owned by the operator of this site – all rights to the mod belong to its author Krunk. The content was derived from the SDModKit.dll file (version 1.3.3) and may differ in newer versions. StellarDrive is a game by Curious Owl Games; this site is not affiliated with Krunk or Curious Owl Games.
PropertyValue
Mod nameStellarDrive Mod Kit
Version1.3.3
AuthorKrunk (GitHub: Krunk-theduck/SDModKit)
GameMelonGame("CuriousOwlGames", "StellarDrive")
Load priorityMelonPriority(-1000) – loads before all other mods
Target framework.NET Framework 4.7.2 (Mono)
NetworkingFishNet (broadcasts)
DependenciesMelonLoader, 0Harmony, Newtonsoft.Json, Unity InputSystem, TextMeshPro

Read this first

SDModKit does not load Unity AssetBundles

There is not a single call to AssetBundle anywhere in the source. Models are built from OBJ files, a simple JSON mesh format, Unity primitives (cube, cylinder …) or meshes of existing game parts. So for pure SDModKit parts you need no Unity – an OBJ export from Blender is enough.

The key data types

Many errors in Visual Studio come from wrong types. The core fields of a part are:

FieldTypeExampleMeaning
Idushort1200Numeric part ID (0–65535), must be unique. Not a string.
Assetstring"PottedPlant"Internal name of the part (becomes the name of the PartSettings). Not a GameObject.
Donorstring"SignalSplitter"Vanilla part whose prefab is cloned (physics, interaction, menu).

Normally you don't create a ContentPartDef in code yourself – you write a JSON file. SDModKit reads it and builds everything automatically.

Folder structure

All SDModKit files live in MelonLoader's UserData folder inside the game directory. The base path in code is ConfigFile.Folder.

StellarDrive/
├─ Mods/
│  ├─ SDModKit.dll                  ← the Mod Kit itself
│  └─ ExampleMod.dll                   ← your C# mod (optional)
└─ UserData/
   └─ SDModKit/                     ← ConfigFile.Folder
      ├─ modkit.cfg                 ← on/off switches for all mods
      ├─ stellardrive.modkit.cfg    ← settings + keybinds of the kit
      ├─ handshake-skew.cfg         ← version matching (ModVersionSkew)
      ├─ <yourmod>.cfg              ← ConfigFile.For("yourmod")
      ├─ Content/                   ← ContentRegistry.UserFolder
      │  └─ MyPack/
      │     ├─ pack.json            ← every *.json is loaded (recursive)
      │     ├─ models/plant.obj
      │     └─ icons/plant.png
      └─ Dumps/                     ← diagnostic dumps (Dump.Folder)

When saving, SDModKit also writes a file sdmodkit-content.json next to the world save. It holds the settings of your placed parts (see Saving).

If UserData/SDModKit/Content does not exist yet, SDModKit creates it on first launch. Packs are only read at game start; the mod menu also has “Reload user packs”.

Two ways to mod

Content pack (JSON)C# mod with SDModKit
Programming requiredNoYes (C#, Visual Studio)
New parts, items, researchYesYes (pack embedded in the DLL)
Custom logic per partOnly via existing behaviours of other modsYes, via PartBehaviour
Settings, keybinds, menu pagesNoYes
LocationUserData/SDModKit/Content/Mods/YourMod.dll

Both can be combined: the C# mod provides a behaviour, the JSON file references it via "behaviour".

First part without code

Five steps to a potted plant in the build menu – no Unity and no Visual Studio.

  1. Export the model from BlenderFile → Export → Wavefront (.obj). Enable UVs and normals, scale 1 m = 1 unit. Details in OBJ from Blender.
  2. Create the folderStellarDrive/UserData/SDModKit/Content/Plants/models/ and put plant.obj there.
  3. Write pack.jsonCreate a file pack.json in the Plants/ folder with the content below.
  4. Start the gameThe MelonLoader log should show Plant Pack 1.0.0 loaded: 0 items, 1 parts, 0 techs.
  5. Load a worldThe part appears in the build menu right after the signal splitter.
{
  "id": "example.plants",
  "name": "Plant Pack",
  "version": "1.0.0",
  "parts": [
    {
      "id": 1200,
      "asset": "PottedPlant",
      "label": "Potted plant",
      "description": "A bit of green for your ship.",
      "donor": "SignalSplitter",
      "buildMenuAfter": "SignalSplitter",
      "mass": 5,
      "visual": {
        "meshes": [
          { "resource": "models/plant.obj", "material": "PartMetal", "color": [0.35, 0.75, 0.4] }
        ]
      }
    }
  ]
}

No bounds given? Then SDModKit measures the size from your model automatically and also fits the interaction hitbox.

First C# mod

A minimal mod that registers with the kit, creates a setting and a keybind, and loads an embedded content pack. For project setup see Project setup.

using MelonLoader;
using SDModKit;
using SDModKit.Api;
using SDModKit.Configuration;
using SDModKit.Content;
using SDModKit.Input;
using SDModKit.UI;
using SDModKit.Util;
using UnityEngine.InputSystem;

[assembly: MelonInfo(typeof(ExampleMod.Plugin), "Example Mod", "1.0.0", "YourName")]
[assembly: MelonGame("CuriousOwlGames", "StellarDrive")]

namespace ExampleMod
{
    // 1) MelonLoader entry point
    public sealed class Plugin : MelonMod
    {
        public override void OnInitializeMelon()
        {
            // ALWAYS register behaviours before loading the pack
            ContentRegistry.Behaviour("examplemod.plant", () => new PlantBehaviour());

            // pack.json as an EmbeddedResource in the DLL
            ContentRegistry.LoadEmbedded(typeof(Plugin).Assembly, "pack.json");

            // register the mod with the kit (own .cfg file under UserData/SDModKit/)
            ModKit.Register(new ExampleKitMod(), ConfigFile.For("examplemod"));
        }
    }

    // 2) The actual SDModKit mod
    public sealed class ExampleKitMod : SDMod
    {
        public override string Id   => "examplemod";
        public override string Name => "Example Mod";

        private ConfigEntry<float> _speed;
        private Keybind _hello;

        public override void OnRegistered()
        {
            _speed = Config.Bind("General", "Speed", 1.5f, "Speed");
            _hello = Keybinds.Register(Config, "Keys", "Hello", Key.H, "Say hello");
            AddPage("Settings", DrawPage);
        }

        public override void OnWorldEnter() => Log.Info("World loaded");

        public override void OnUpdate()
        {
            if (_hello.Pressed) Toasts.Show("Hello from ExampleMod");
        }

        private void DrawPage()
        {
            ModUi.Section("General", () =>
            {
                ModUi.Row("Speed", () =>
                    _speed.Value = ModUi.Slider(_speed.Value, 0f, 5f));
            });
        }
    }
}

PlantBehaviour is written out in full in Part behaviours.

Pack structure

A content pack is a JSON file. It is read by ContentPack.Parse using Newtonsoft.Json. At the top level there are only seven fields:

FieldTypeDefaultDescription
idrequiredstring–Unique pack ID, e.g. "example.plants". Without an ID the pack is discarded.
namestring= idDisplay name in the log and the mod menu.
versionstring"0.0.0"Recorded as id@version in the world save.
partsarray[]New parts → parts
itemsarray[]New items/resources → items
researcharray[]New research nodes → research
attacharray[]Attach sockets/logic to vanilla parts → attach

Common value formats

FormatNotationNote
Vector3[x, y, z]At least 3 numbers, otherwise the default is used. Units in metres, local to the part, Y = up.
Vector2Int[x, y]Integers, e.g. a position in the research tree.
Colour[r, g, b] or [r, g, b, a]Values from 0 to 1. Alpha defaults to 1.
Cost{ "itemId": amount }The key is the numeric item ID as a string, e.g. { "12": 4 }.
null"field": nullTreated like “not specified”.

If the same pack id appears twice, the pack loaded first wins (log: pack id … loaded twice).

parts – Parts

Each entry in parts becomes a ContentPartDef. SDModKit clones the prefab of the donor (an existing game part), replaces its visuals with your meshes and registers the result as new PartSettings in the game.

FieldTypeDefaultDescription
idrequiredushort–Unique part ID (1–65535). 0 or missing → part is skipped.
assetrequiredstring–Internal name. Must be unique, becomes the object name of the PartSettings and is the key in research.parts and ContentRegistry.PartByAsset.
labelstring= assetDisplay name in game (fullLabel), also the title of the part menu.
descriptionstring""Description text in the build menu.
donorstring"SignalSplitter"Asset name of the vanilla part that is cloned. Determines physics, interaction and menu type.
statestring"SignalSplitterState"State class (internalStateClass). With a different donor, set it to that donor's state class (listed in the dump).
behaviourstringnullRegistered behaviour key or full class name of a PartBehaviour class.
iconstringnullPath to a PNG/JPG relative to the pack. Replaces the build menu thumbnail.
buildMenuAfterstringnullAsset name or label of a vanilla part. The new part is placed right after it in its row (or in a free row if that one is full).
buildTabstringnullCustom build menu tab, e.g. "Machines" or "Logistics". Takes precedence over buildMenuAfter.
buildRowint0Row within buildTab.
massfloat10Mass, clamped to 0.1–2000.
menustring"auto"auto: donor menu, just retitled · config: settings panel beside the menu · panel: panel replaces the menu. See Configuration menu.
snappingstringdonorName of a SnappingStyle value (case-insensitive). See Bounds & snapping.
boundsobjectauto{ "center": [x,y,z], "size": [x,y,z] }. If missing, the size is measured from the model.
visualobject–clearDonor, fitInteraction, meshes → visual & meshes.
socketsarraydonorCustom connectors. If the field is present (even empty), all donor sockets are removed → sockets.
costobject{}Build cost { "itemId": amount }. Unknown items are dropped with a warning.
configobjectnullInitial values for the PartConfig of each placed instance.

What happens during the build

ContentBuilder.Configure processes the definition in this order:

  1. Write label, description, state class and mass to the cloned PartSettings.
  2. Set snapping (only if given) and add the build cost.
  3. Remove the donor visuals (if clearDonor = true) and attach your meshes under a child object SDModKitMeshes.
  4. Rebuild the sockets (if sockets is present).
  5. Measure the bounds from all renderers (if bounds is missing) and set ShipPartBounds.
  6. Fit the interaction collider to the bounds (if fitInteraction = true).
  7. Hook up the hover outline and set the icon.

Bounds & snapping

Bounds define the footprint, the collision check while building and the hitbox. If you give neither center nor size, SDModKit measures all renderers of the part (excluding sockets). Default without a model: centre [0, 0.25, 0], size [0.9, 0.5, 0.9], minimum 0.1 per axis.

Snapping defines how the part snaps when placed. The value is a name from the game enum SnappingStyle. The available names are listed in the dump section snapping or the “Snapping table” dump in the mod menu.

The first six snapping values (enum 0–5) only work on hull parts. On floating parts they crash the build preview – SDModKit then forces PreciseOnFloor (enum 6) and reports it in the log.

Choosing a donor

The donor determines everything you don't override yourself: physics components, network state, interaction and menu. Asset names known from the code:

Asset nameTypical use
SignalSplitterDefault donor. Small, floating logic part.
FluidSplitter, PowerSplitterSmall splitters; provide socket templates for fluid/power.
FluidTank, Battery, MathGateFurther socket template sources.
StorageChestPart with inventory and storage menu – required for menu: "config"/"panel".

To find the exact asset name and state class of any vanilla part, aim at it in game and trigger “Dump aimed part” (F9). The output contains settings <Name> id <ID> and state <Class>.

visual – Meshes

The visual object controls how a part looks. Each element in meshes becomes a child object (MeshDef).

FieldTypeDefaultDescription
clearDonorbooltrueRemoves renderers, mesh filters and child objects of the donor visuals.
fitInteractionbooltrueFits the interaction collider to the bounds and disables overlapping colliders.
meshesarray[]List of mesh definitions (see below).

Mesh fields

The mesh source is resolved in this order: resource → donorPath → shape. Special cases are shape: "text" and shape: "screen".

FieldTypeDefaultDescription
resourcestringnullPath to an .obj file or a JSON mesh file, relative to the pack folder (or name of an embedded resource).
donorPathstringnullTake a mesh from a game prefab: "Child/Path" in the donor or "OtherPart|Child/Path".
shapestringnullcube, sphere, cylinder, capsule, quad, plane (unknown values = cube) plus text and screen.
materialstring"PartMetal"Name of a loaded game material. Not found → PartMetal.
colorcolournullTint, sets _BaseColor/_Color on a material copy.
positionVector3[0,0,0]Local position.
rotationVector3[0,0,0]Euler angles in degrees.
scaleVector3[1,1,1]Scale. For text/screen: x/y = area.
textstringnullFixed text for shape: "text".
textColorcolourwhite / cyanText colour (text: otherwise color or white, screen: light cyan).
fontstringgame fontName of a TextMeshPro font. If omitted, the most common game font is used.
screenstringnullChannel name for dynamic content (turns the mesh into a screen automatically).
columnsint16Character width of the ticker text (4–64).
speedfloat6Ticker speed in characters per second (0–40).
"visual": {
  "clearDonor": true,
  "meshes": [
    { "shape": "cube",     "position": [0, 0.15, 0], "scale": [0.8, 0.3, 0.8], "color": [0.2, 0.25, 0.3] },
    { "shape": "cylinder", "position": [0, 0.40, 0], "scale": [0.3, 0.1, 0.3], "material": "PartMetal" },
    { "shape": "text",     "position": [0, 0.15, -0.41], "scale": [0.6, 0.2, 1], "text": "EX-01", "textColor": [0.3, 0.8, 1] }
  ]
}

Primitives without their own texture automatically get the bundled texture ink_albedo.png so they don't look flat white.

sockets – Connectors

Sockets are the plug points for cables and lines. SDModKit copies real socket objects from vanilla parts as templates.

FieldTypeDefaultDescription
idbyte0Socket number. Must run without gaps 0 … n-1.
typestring"item"power, signal, fluid or item (anything else = item).
positionVector3[0,0,0]Local position on the part.
facingVector3autoEuler rotation. If omitted, the socket is oriented horizontally relative to the bounds centre.
relayedboolfalseAll sockets with relayed: true are linked to each other as pass-through (like a splitter).

Socket types

JSONCableTypeNote
signal0Signal cable
fluid1Fluid line
power2Power cable
item3New cable type “Item Line” added by SDModKit (ItemCables). Tinted orange, uses the fluid socket visuals as a template.
"sockets": [
  { "id": 0, "type": "item",  "position": [-0.45, 0.2, 0] },
  { "id": 1, "type": "item",  "position": [ 0.45, 0.2, 0] },
  { "id": 2, "type": "power", "position": [ 0, 0.2, 0.45], "facing": [0, 180, 0] }
]

Even an empty "sockets": [] removes all donor sockets. Leave the field out if you want to keep the donor sockets.

items – Items

New items (resources, fluids, gases) are added to the game's item list (ContentItems) and can be used in build costs and research.

FieldTypeDefaultDescription
idrequireduint–Unique item ID. Collisions with vanilla or other mod items are skipped.
namestring"item <id>"Display name.
iconstringnullPNG/JPG relative to the pack.
massfloat1Mass per unit.
maxStackint20Stack size, 1–200.
isFluidboolfalseItem is a fluid.
isGasboolfalseItem is a gas.
fluidColorcolourblackColour in tanks/lines.
modelobject–{ "meshes": [ … ] } – same fields as in visual.

An item without a model is drawn invisible and cannot be dropped (log: has no model, it will draw blank and cannot be dropped).

"items": [
  {
    "id": 9001,
    "name": "Coolant",
    "icon": "icons/coolant.png",
    "mass": 0.5,
    "maxStack": 100,
    "isFluid": true,
    "fluidColor": [0.2, 0.7, 1.0, 1.0],
    "model": { "meshes": [ { "shape": "capsule", "scale": [0.15, 0.15, 0.15], "color": [0.2, 0.7, 1] } ] }
  }
]

research – Research

Research nodes are inserted into the tech tree when entering a world (ContentResearch.Apply). They unlock parts and game features.

FieldTypeDefaultDescription
idrequiredushort–Unique tech ID.
namestring"Tech <id>"Name in the research tree.
position[x, y][0, 0]Cell in the tech tree. Required unless hidden. Occupied cells are reported.
requiresushort[][]IDs of the prerequisites.
partsstring[][]Asset names of the parts that get unlocked.
featuresstring[][]Names from the game enum ResearchUnlockableFeature. Unknown names are ignored.
costobject{}Research cost { "itemId": amount }, custom items work too.
hiddenboolfalseNode is not shown in the tree.
grantedByushort0Researched automatically as soon as this other tech is researched (the host checks every second).
"research": [
  {
    "id": 4100,
    "name": "Hydroponics",
    "position": [6, 2],
    "requires": [12],
    "parts": ["PottedPlant"],
    "cost": { "9001": 20 }
  }
]

With "hidden": true and "grantedBy": <VanillaTechId> you can quietly tie your part to an existing research without changing the tree.

attach – Extend vanilla

attach adds an extra socket type (and optionally a behaviour) to existing game parts. SDModKit goes through all vanilla parts that

  • are not hull parts,
  • have an inventory,
  • don't have a socket of that type yet,
  • and are not listed in exclude.

According to the log, two sockets are added per part. The prefab is cloned first, and everything is reverted when leaving the world.

FieldTypeDefaultDescription
idstring"attach"Name for the log.
socket.typestring"item"Socket type as in sockets.
socket.facestring"front"front, back, left, right, top. Existing sockets take priority when choosing the side.
behaviourstringnullBehaviour for all extended parts.
menustring"config"Menu mode for extended parts.
configobjectnullInitial PartConfig values.
excludestring[][]Asset names or labels that are left untouched.
"attach": [
  {
    "id": "item-ports",
    "socket": { "type": "item", "face": "front" },
    "behaviour": "examplemod.itemport",
    "menu": "config",
    "config": { "mode": "push" },
    "exclude": ["StorageChest"]
  }
]

Complete example

A pack with a custom item, a part with a screen, sockets, cost and research.

{
  "id": "example.monitor",
  "name": "Example Monitor",
  "version": "1.0.0",

  "items": [
    { "id": 9101, "name": "Circuit", "icon": "icons/chip.png", "maxStack": 50,
      "model": { "meshes": [ { "shape": "cube", "scale": [0.2, 0.04, 0.15], "color": [0.1, 0.5, 0.2] } ] } }
  ],

  "parts": [
    {
      "id": 1210,
      "asset": "StatusMonitor",
      "label": "Status monitor",
      "description": "Shows live data on a display.",
      "donor": "SignalSplitter",
      "behaviour": "ExampleMod.MonitorBehaviour",
      "buildMenuAfter": "SignalSplitter",
      "mass": 8,
      "icon": "icons/monitor.png",
      "visual": {
        "meshes": [
          { "resource": "models/monitor.obj" },
          { "shape": "screen", "screen": "example.status", "position": [0, 0.32, -0.051],
            "scale": [0.5, 0.28, 1], "columns": 18, "speed": 8 }
        ]
      },
      "sockets": [
        { "id": 0, "type": "signal", "position": [0, 0.05, 0.1] },
        { "id": 1, "type": "power",  "position": [0.2, 0.05, 0.1] }
      ],
      "cost": { "9101": 2 },
      "config": { "mode": 0 }
    }
  ],

  "research": [
    { "id": 4110, "name": "Status displays", "position": [7, 3], "parts": ["StatusMonitor"], "cost": { "9101": 10 } }
  ]
}

OBJ from Blender

SDModKit has its own OBJ parser (MeshData.FromObj). It is used automatically when the file name ends in .obj.

What the parser supports

  • Reads v (vertices), vt (UVs), vn (normals), f (faces) and usemtl.
  • Mirrors the X axis and flips the triangle winding – the usual conversion from Blender (right-handed) to Unity (left-handed). You don't need to rotate anything by hand.
  • Polygons are triangulated as fans. Better triangulate non-convex n-gons in Blender first.
  • If any face lacks normals, all normals are recalculated.
  • Above 65,000 vertices it switches to 32-bit indices automatically.
  • Each usemtl becomes its own submesh. .mtl files are ignored.

Recommended export settings

Blender optionSettingWhy
Forward / Up-Z / Y (default)Matches the parser's axis conversion.
Scale1.0, apply scale first (Ctrl+A)1 Blender unit = 1 m in game.
UV CoordinatesonFor textures/material mapping.
NormalsonOtherwise hard edges are smoothed.
Triangulated Meshon (recommended)Avoids errors with non-convex faces.
Materialsone material if possibleThe renderer only gets one material. Additional submeshes are not rendered visibly (inferred from Unity behaviour).
Originbottom centreThe part origin sits on the floor; Y = 0 is the contact surface.

Need several colours? Split the model into several OBJ files and add a separate meshes entry with its own material/color for each.

JSON mesh format

Every mesh resource that does not end in .obj is read as a JSON mesh (MeshData.FromJson). The format is a direct copy of a Unity mesh – handy if you generate meshes with a script.

{
  "name": "Wedge",
  "vertices":  [0,0,0,  1,0,0,  0,1,0,  0,0,1],
  "normals":   [0,0,-1, 0,0,-1, 0,0,-1, 0,-1,0],
  "uv":        [0,0,  1,0,  0,1,  1,1],
  "submeshes": [ [0,2,1,  0,1,3,  0,3,2,  1,2,3] ]
}
FieldContent
verticesrequiredFlat list x, y, z, … (Unity coordinates, no axis conversion).
normalsSame length as vertices, otherwise calculated automatically.
uvu, v per vertex (length = vertex count × 2).
submeshesrequiredList of index lists (triangles, clockwise = Unity front face).
nameName of the mesh.

Text & screens

Fixed text

"shape": "text" creates a TextMeshPro object with automatic font size. scale.x and scale.y are the width and height of the text box.

{ "shape": "text", "text": "REACTOR 1", "position": [0, 0.5, -0.3], "scale": [0.6, 0.15, 1], "textColor": [1, 0.6, 0.1] }

Dynamic screen

"shape": "screen" (or simply a set "screen" field) creates a display panel with a text box. Your mod supplies the content through a channel. The content is polled every 0.1 seconds.

using SDModKit.Content;
using UnityEngine;

// once, e.g. in OnRegistered()
Screens.Provide("example.status", screenObject => new ScreenFrame
{
    Text      = "Power: " + Mathf.RoundToInt(Time.time % 100) + " kW",
    Scroll    = true,                      // ticker with "columns" characters
    TextColor = new Color(0.4f, 1f, 0.6f), // optional
    Image     = null                       // show a Texture2D instead of text
});
ScreenFrame fieldTypeEffect
TextstringDisplayed text (TextMeshPro rich text possible, except with Scroll).
ScrollboolTicker: shows columns characters and scrolls at speed characters/s in monospace.
TextColorColor?Overrides the text colour.
ImageTexture2DReplaces the text with an image on the panel (_BaseMap).

The callback receives the screen GameObject. Use screenObject.transform to find out which ship/part it belongs to. Errors in the callback are logged once and the screen stays empty.

Textures, icons & materials

Images are loaded via ContentAssets.Texture(pack, path); paths are always relative to the pack folder (forward slashes / work on Windows too). Textures and meshes are cached per pack.

UseFieldEffect
Build menu icon of a partparts[].iconOverrides the rendered thumbnail (IconOverrides.SetPart).
Item iconitems[].iconIcon in the inventory.
Materialmeshes[].materialLooks for an already loaded material with exactly this name (Resources.FindObjectsOfTypeAll<Material>).

The default PartMetal is the normal metal material of game parts. Further material names can be found through a dump of the donor prefabs. With color a tinted copy of the material is created – the original stays unchanged.

Packs inside the DLL

For C# mods you can embed pack.json, models and icons as an EmbeddedResource in your DLL. Then there is only one file in the Mods folder.

<ItemGroup>
  <EmbeddedResource Include="pack.json" />
  <EmbeddedResource Include="models\*.obj" />
  <EmbeddedResource Include="icons\*.png" />
</ItemGroup>
ContentRegistry.LoadEmbedded(typeof(Plugin).Assembly, "pack.json");
// alternatively: generate the JSON yourself
ContentRegistry.LoadJson(typeof(Plugin).Assembly, jsonText, "generated.json");

How resources are found

.NET names embedded files Namespace.folder.file.ext. SDModKit converts models/plant.obj into models.plant.obj and searches in this order:

  1. exact resource name,
  2. a name ending in .models.plant.obj,
  3. a name ending in .plant.obj (file name only).

For embedded packs pack.Folder is empty – all paths in the JSON file are then looked up only in the DLL's resources, not on disk. Avoid identical file names in different folders.

Project setup

An SDModKit mod is a normal MelonLoader mod (class library for .NET Framework 4.7.2) that additionally references SDModKit.dll.

  1. Create the projectVisual Studio → “Class Library (.NET Framework)” or an SDK project with net472. Not .NET 6/8.
  2. Add referencesReference the DLLs from the game folder (see the csproj below), each with Private=false so they are not copied.
  3. Build & copyCopy the finished YourMod.dll to StellarDrive/Mods/ – next to SDModKit.dll.
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net472</TargetFramework>
    <LangVersion>latest</LangVersion>
    <AssemblyName>ExampleMod</AssemblyName>
    <RootNamespace>ExampleMod</RootNamespace>
    <!-- Recorded by SDModKit as "examplemod@1.0.0" in the world save -->
    <Version>1.0.0</Version>
    <!-- Adjust the path to your game folder -->
    <GameDir>C:\Program Files (x86)\Steam\steamapps\common\StellarDrive</GameDir>
    <Managed>$(GameDir)\StellarDrive_Data\Managed</Managed>
  </PropertyGroup>

  <ItemGroup>
    <Reference Include="MelonLoader"><HintPath>$(GameDir)\MelonLoader\net35\MelonLoader.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="0Harmony"><HintPath>$(GameDir)\MelonLoader\net35\0Harmony.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="SDModKit"><HintPath>$(GameDir)\Mods\SDModKit.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="Assembly-CSharp"><HintPath>$(Managed)\Assembly-CSharp.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="UnityEngine.CoreModule"><HintPath>$(Managed)\UnityEngine.CoreModule.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="UnityEngine.IMGUIModule"><HintPath>$(Managed)\UnityEngine.IMGUIModule.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="Unity.InputSystem"><HintPath>$(Managed)\Unity.InputSystem.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="Unity.TextMeshPro"><HintPath>$(Managed)\Unity.TextMeshPro.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="Newtonsoft.Json"><HintPath>$(Managed)\Newtonsoft.Json.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="FishNet.Runtime"><HintPath>$(Managed)\FishNet.Runtime.dll</HintPath><Private>false</Private></Reference>
  </ItemGroup>

  <ItemGroup>
    <EmbeddedResource Include="pack.json" />
    <EmbeddedResource Include="models\**\*.obj" />
    <EmbeddedResource Include="icons\**\*.png" />
  </ItemGroup>

  <!-- Optional: copy to the Mods folder after each build -->
  <Target Name="CopyToMods" AfterTargets="Build">
    <Copy SourceFiles="$(TargetPath)" DestinationFolder="$(GameDir)\Mods" />
  </Target>
</Project>

Depending on the MelonLoader version, MelonLoader.dll and 0Harmony.dll are located directly in MelonLoader/ or in MelonLoader/net35/. Check the path in your game folder. Only reference the DLLs you actually need.

Required usings

NamespaceContent
SDModKitModKit, ModStamp, ModToggles
SDModKit.ApiSDMod, ModPage
SDModKit.ConfigurationConfigFile, ConfigEntry<T>
SDModKit.ContentPacks, behaviours, PartConfig, screens, menus
SDModKit.InputKeybinds, Keybind, MouseBind
SDModKit.UIModUi, Banner, GameModal, KeyTips, Theme
SDModKit.UtilLog, Toasts, Dump, EmbeddedAssets
SDModKit.GameGame helpers: player, items, ships, build menu
SDModKit.PatchingModPatches (Harmony)

SDMod & ModKit

Every mod derives from SDModKit.Api.SDMod and is registered once with ModKit.Register – typically in OnInitializeMelon of your MelonMod.

SDMod – members

MemberTypeDescription
IdabstractstringUnique ID, e.g. "examplemod". Key in modkit.cfg and in the world save.
NameabstractstringDisplay name in the mod menu.
Essentialbooltrue = cannot be switched off in the mod menu.
Devbooltrue enables the kit's developer keys (Insert menu, F9 dump).
HostOnlyboolExists, but is not evaluated anywhere in 1.3.3.
ConfigConfigFileThe file passed to Register.
LogLogLogger prefixed with the mod name.
PagesIReadOnlyList<ModPage>Your own pages in the mod menu.
OnRegistered()virtualRight after Register. Create config entries, keybinds and pages here.
OnWorldEnter()virtualWhen a world has loaded (host and client).
OnWorldExit()virtualWhen leaving the world.
OnUpdate()virtualEvery frame (also in the main menu).
AddPage(title, draw)protectedAdd a page. Overload AddPage(group, title, draw) for sub-groups.

ModKit – static API

MemberDescription
Register(mod, config)Registers the mod. Duplicate IDs produce a warning. If the mod is disabled in modkit.cfg (and not Essential), it goes into Disabled and receives no calls.
Mods / DisabledActive and switched-off mods.
IsMuted(mod)After 5 exceptions in callbacks a mod is muted – until the next world change.
Unmute()Re-enable all muted mods.
SaveConfigs()Write all changed config files immediately.
OpenMenu()Opens the mod menu from code.
DevToolsPresenttrue if a registered mod has Dev => true.
Menu, LogMenu host and kit logger.

Mods are switched on and off via UserData/SDModKit/modkit.cfg, section [Mods], e.g. examplemod = false. Changes only take effect after a restart.

Settings (ConfigFile)

ConfigFile.For("name") opens UserData/SDModKit/name.cfg (only once per name). The format is a simple INI:

[General]
# Speed
Speed = 1.5

[Keys]
Hello = H
ConfigEntry<float> speed   = Config.Bind("General", "Speed", 1.5f, "Speed");
ConfigEntry<bool>  enabled = Config.Bind("General", "Enabled", true, "Feature on/off");
ConfigEntry<Mode>  mode    = Config.Bind("General", "Mode", Mode.Auto, "Operating mode"); // enums work too

float v = speed.Value;   // read
speed.Value = 2f;        // write → saved automatically
float d = speed.DefaultValue;
  • Supports all types that can be converted to text via Convert (int, float, bool, string …) plus enums. Numbers are written culture-invariant (dot as decimal separator).
  • Comments start with # or ;.
  • Changed values are saved 2 seconds after the last change, on world change and on exit. Immediately: file.Save() or ConfigFile.SaveAllDirty().

Keybinds

Keybinds are stored in a config file and automatically appear on the Keybinds page of the mod menu, where players can rebind them.

using SDModKit.Input;
using UnityEngine.InputSystem;

Keybind toggle = Keybinds.Register(Config, "Keys", "Toggle", Key.G, "Toggle feature");
Keybind grab   = Keybinds.RegisterMouse(Config, "Keys", "Grab", MouseBind.Middle, "Grab");

public override void OnUpdate()
{
    if (Keybinds.Typing) return;          // player is typing in a text field
    if (toggle.Pressed)  DoToggle();      // pressed this frame
    if (grab.Held)       DragObject();    // held
    if (grab.Released)   Drop();          // released
}
MemberDescription
Pressed / Held / ReleasedState in the current frame.
DisplayDisplay text of the binding, e.g. “G”.
Key / MouseCurrent key or mouse button.
SetKey(key), SetMouse(btn), Clear(), Reset()Change, clear or reset the binding to default.
Keybinds.All, Keybinds.For(file), Keybinds.SaveAll()All binds, binds of one file, save everything.

MouseBind: None, Left, Right, Middle, Forward, Back.

Part behaviours

A PartBehaviour is the logic behind a content part. There is one instance per part type, not per placed instance – the data of an instance lives in part.Config.

Registering

// BEFORE loading the pack – binding happens at load time
ContentRegistry.Behaviour("examplemod.plant", () => new PlantBehaviour());
ContentRegistry.LoadEmbedded(typeof(Plugin).Assembly, "pack.json");

The pack then contains "behaviour": "examplemod.plant". Alternatively you can give the full class name ("ExampleMod.PlantBehaviour"); SDModKit looks for it in the pack assembly, via Type.GetType and in all loaded assemblies (case-insensitive). The class then needs a parameterless constructor.

Callbacks

MethodWhen / where
OnRegistered()Once after binding to the definition. Def is set.
OnWorldEnter() / OnWorldExit()When entering/leaving a world.
OnPartAdded(part)Host: an instance has newly appeared on a ship.
OnPartRemoved(shipId, partId)Host: an instance has disappeared. Its config is deleted afterwards.
ServerTick(part, dt)Host only, every ship update for each instance. Muted after 5 failures.
DescribeConfig(part, rows)Host and client: describe rows for the part menu (see Configuration menu).
ConfigTitle(part)Title of the menu (default: label).
OnAction(part, player, action)Host: a player action, triggered via ContentActions.Send.
PanelScaleSize factor of the panel in panel mode (min. 0.5).
DrawConfig(part), Status(part)Exist, but are not called in 1.3.3.
Trace / Warn / ErrorLog helpers using the behaviour's scope.

ContentPart

All callbacks receive a ContentPart (struct) with: Ship (IShipStateWrite, null on clients), ShipId, PartId, Part (StatefulPart), Def, Config, Valid, Label and LocalPosition.

Complete example

using System.Collections.Generic;
using SDModKit.Content;
using SDModKit.UI;
using UnityEngine;

namespace ExampleMod
{
    public sealed class PlantBehaviour : PartBehaviour
    {
        // Host: water slowly drains
        public override void ServerTick(ContentPart part, float dt)
        {
            float water = part.Config.GetFloat("water", 1f);
            float rate  = part.Config.GetInt("mode", 0) == 1 ? 0.005f : 0.01f;
            float next  = Mathf.Max(0f, water - rate * dt);
            if (Mathf.Abs(next - water) > 0.001f) part.Config.Set("water", next);
        }

        public override string ConfigTitle(ContentPart part) => "Potted plant";

        // Host + client: menu rows
        public override void DescribeConfig(ContentPart part, List<ConfigRow> rows)
        {
            float water = part.Config.GetFloat("water", 1f);
            int   mode  = part.Config.GetInt("mode", 0);

            rows.Add(new ConfigRow { Section = "Plant" });
            rows.Add(new ConfigRow
            {
                Label      = "Water",
                Value      = Mathf.RoundToInt(water * 100f) + " %",
                ValueColor = water < 0.2f ? Theme.Bad : Theme.Good
            });
            rows.Add(new ConfigRow
            {
                Label    = "Mode",
                Value    = mode == 0 ? "Normal" : "Economy",
                Choices  = new[] { "Normal", "Economy" },
                OnChoose = i => part.Config.Set("mode", i)      // menu refreshes itself afterwards
            });
            rows.Add(new ConfigRow
            {
                Label    = "Water now",
                Choices  = new[] { "Water" },
                OnChoose = _ => ContentActions.Send(part, "water") // runs on the host
            });
        }

        // Host: action from the player
        public override void OnAction(ContentPart part, TrackedPlayerServer player, string action)
        {
            if (action == "water") part.Config.Set("water", 1f);
        }
    }
}
{ "id": 1200, "asset": "PottedPlant", "label": "Potted plant",
  "donor": "StorageChest", "state": "StorageChestState",
  "behaviour": "examplemod.plant", "menu": "config",
  "config": { "water": 1.0, "mode": 0 } }

For menu: "config" or "panel" the donor must open a storage menu on interaction (e.g. StorageChest). The default donor SignalSplitter doesn't. The state class above is an example – the dump gives you the real name.

More runtime helpers

MemberDescription
ContentRuntime.PartsOn(ship)All content parts on a ship.
ContentRuntime.TryResolve(ship, partId, out part)Resolve a single content part.
ContentRuntime.Of(partId)Behaviour for a part ID.
ContentRuntime.ShipServerTickEvent Action<IShipStateWrite, float> – once per ship update on the host.
ContentRuntime.All, TrackedShipsAll behaviours, number of tracked ships.

Configuration menu

When a player interacts with a content part, SDModKit intercepts the opening of the storage menu (StorageMenuActivator.ShowMenu) and builds your menu from the rows returned by DescribeConfig.

menuResult
autoThe donor menu stays, it only gets your title.
configSimple settings panel beside the donor menu (section, label, value, choices, icons).
panelYour own panel replaces the menu completely – with tabs, banner, notes, icons, detail text and colours.

ConfigRow – fields

FieldTypeEffect
SectionstringSub-heading (row with only Section).
LabelstringLabel on the left.
ValuestringValue on the right.
ValueColor / LabelColorColor?Colours, e.g. Theme.Good, Theme.Warn, Theme.Bad.
Choicesstring[]Choice buttons.
ChoiceEnabledbool[]Disable individual buttons (panel).
OnChooseAction<int>Called with the index; the menu rebuilds afterwards.
HighlightboolHighlight the row.
IconsList<ConfigIcon>Icon strip: Image, Name (tooltip), State (tint index), OnClick.
Icon / ValueIconTextureSmall image before the label or value (panel).
Detail / DetailColorstring / Color?Second, smaller text line (panel).
BannerstringNotice bar at the top (panel).
NotestringNote text (panel).
Tabs, ActiveTab, TabColors, Asidestring[], int, Color[], stringTab bar; OnChoose receives the tab index (panel).

Behaviour in multiplayer

  • On the client the menu first shows “Waiting for the host's settings” until the config arrives from the host.
  • If the client changes the config via OnChoose, the whole config is sent to the host.
  • If DescribeConfig returns no rows, the menu shows “Nothing to configure”.
  • Redraw from outside: ContentConfigMenu.Refresh(). Subscribe to changes: ContentConfigMenu.Changed.

Actions

ContentActions.Send(part, "name") sends an action to the host, which then calls OnAction. The host only runs it if the player is currently interacting with exactly that part – ideal for buttons like “Start”, “Empty” or “Water”.

Storing data

PartConfig – per placed part

Each placed instance has its own PartConfig (internally a JObject). Initial values come from "config" in the pack. It is stored in the world save and synced to clients.

int    n    = part.Config.GetInt("count", 0);
float  f    = part.Config.GetFloat("rate", 1f);
bool   on   = part.Config.GetBool("enabled", true);
string name = part.Config.GetString("name", "");
List<uint> filter = part.Config.GetIds("filter");   // e.g. item IDs

part.Config.Set("count", n + 1);
part.Config.SetIds("filter", new uint[] { 12, 15 });
bool exists = part.Config.Has("count");

Direct access via PartData: PartData.For(shipId, partId, def), TryGet, Forget, Count.

WorldData – per world

Simple key/value pairs (strings) stored with the world:

WorldData.Set("examplemod.lastVisit", System.DateTime.Now.ToString("o"));
string last = WorldData.Get("examplemod.lastVisit");   // null if not set

Prefix keys with your mod ID so they don't clash with other mods.

Mod menu pages

The mod menu is IMGUI-based. Your draw function is called every frame while the page is open. ModUi provides building blocks in the kit's style.

private string _filter = "";

public override void OnRegistered()
{
    AddPage("Settings", DrawSettings);
    ModPage dev = AddPage("Debug", "Tools", DrawTools);
    dev.Available = () => SDModKit.Game.GameRefs.InWorld;   // only show inside a world
}

private void DrawSettings()
{
    ModUi.Section("General", () =>
    {
        ModUi.Row("Enabled", () => _enabled.Value = ModUi.Toggle(_enabled.Value, "on"));
        ModUi.Row("Speed",   () => _speed.Value   = ModUi.Slider(_speed.Value, 0f, 5f));
        ModUi.Row("Count",   () => _count.Value   = ModUi.Int(_count.Value, 1, 64));
        ModUi.Note("Changes are saved automatically.");
    });

    ModUi.Fold("Advanced", () =>
    {
        _filter = ModUi.Search(_filter);
        foreach (var it in SDModKit.Game.ItemCatalog.All)
            if (ModUi.Matches(it.itemName, _filter)) ModUi.Label(it.id + "  " + it.itemName);
    });
}

private void DrawTools()
{
    if (ModUi.Button("Write dump")) SDModKit.Util.Dump.Everything();
}
ModUiDescription
Section(title, body), BeginSection/EndSectionBox with a heading.
Fold(title, body)Collapsible area.
Row(label, controls), BeginRow/EndRowRow with a label on the left.
Label, Note, Space, SeparatorText, note, spacing, divider.
Hosting()“Hosting” row: shows whether the mod is running here as host or the host needs it too.
Button(text, width)true on click.
Toggle(value, text)Checkbox.
Text(value, width)Text field.
Int(value, min, max, width)Number text field, result clamped to min–max.
Slider(value, min, max, width)Slider.
Search(value, placeholder), Matches(text, filter)Search field and matching filter.

Harmony patches

SDModKit uses HarmonyDontPatchAll and patches selectively. For your own patches there is ModPatches, which collects failed patches instead of crashing the mod.

using HarmonyLib;
using SDModKit.Patching;

// apply all [HarmonyPatch] classes of your own assembly
bool ok = ModPatches.ApplyAll("examplemod", typeof(Plugin).Assembly, Log);

// or a single patch class
ModPatches.Apply("examplemod", typeof(MyPatch), Log);

// remove all patches of this ID
ModPatches.Remove("examplemod");

ModPatches.For(id) returns the Harmony instance, Handler(owner, method) a HarmonyMethod, BrokenHandlers the list of failed patches (in the mod menu under “Patches that did not bind”).

When your mod is loaded as a MelonMod, MelonLoader applies its [HarmonyPatch] classes automatically by default. To control this through ModPatches, also add [assembly: HarmonyDontPatchAll] to your assembly.

Logging

Log.Info("loaded");                  // SDMod.Log – prefix = mod name
Log.Trace("only with VerboseLog");   // only appears if Log.Verbose = true
Log.Warn("careful");
Log.Error("broken", ex);

// guard code: exception is logged, returns false
Log.Guard("read inventory", () => ReadInventory());

var other = SDModKit.Util.Log.For("ExampleMod.Net");   // separate logger

Log.Verbose is switched by the kit setting Diagnostics.VerboseLog. Content messages (packs, behaviours) go through ContentLog: the last 400 entries are shown in the mod menu, and from Warn upwards also in the MelonLoader console.

In-game UI helpers

APIDescription
Toasts.Show(text)Short popup (namespace SDModKit.Util).
Banner.Show(text, color, seconds)Large banner text in game style.
GameModal.Open(title, subtitle, choices)Dialog in game style with ModalChoice { Label, Bad, OnPick }. Then Section, Line(text, warn), Note for content; Close(), IsOpen.
KeyTips.Mode(text), KeyTips.Add(bind, label), KeyTips.HideVanilla(tip)Key hints in the bottom right. Call every frame for as long as they should be visible.
ModalInput.Capture() / Release() / Hold()Block game controls while your own UI is open.
ThemeKit colours: Accent, Text, TextDim, Good, Warn, Bad, Panel …
GameStyleGame colours and font (Font, Text, Background, Bad …), once Ready.
EmbeddedAssets.Texture(asm, name)Load a PNG from an embedded resource; also Bytes, Text.
GameModal.Open("Reset ship?", "All settings will be lost.", new[]
{
    new ModalChoice { Label = "Cancel" },
    new ModalChoice { Label = "Reset", Bad = true, OnPick = () => ResetAll() }
});

Game helpers

The SDModKit.Game namespace wraps common game access so you don't have to dig through Assembly-CSharp yourself.

ClassKey membersPurpose
GameRefsIsServer, IsClient, InWorld, PlayersServer, PlayerCamera, Service<T>()Basic state and game services.
LocalPlayerStateType, IsPiloting, IsOnFoot, GameMenuOpen, TryEquipLocal player.
PlayerAccessLocalServerPlayer, WorldPosition, TryGive(itemId, qty, out err), TryClearInventory, CurrentCountsInventory and position.
ItemCatalogAll, ById, NameOf, DisplayName, Icon, ThumbnailAll items (incl. mod items).
PartLookupByAssetName, ByIdFind PartSettings.
ItemMoveAccepts, Count, Total, Contents, Transfer, Move, MaxStack, IsMachineMove items between inventories (host).
ServerTradeHas, TryTrade, Split, HeldItemTrading/item in hand.
WorldQueryScanMinables, ScanDropsMinable objects and drops nearby.
ShipAimTryGetAimedPart, TryPart, ShipTransformPart under the crosshair.
LaserTuningSetRange(owner, factor), SetSpeed(owner, factor)Adjust the mining laser.
BuildTabsDeclare(tab, order, icon), Add, AddAll, HideCustom build menu tabs (“Machines” = 10, “Logistics” = 20).
CustomPartsRegister(CustomPartDefinition), Get, All, PartsReadyParts via code instead of JSON (Id, Name, Donor, BuildMenuAfter, BuildTab, BuildRow, Configure, AutoFinish).
IconOverridesSetPart, SetItemReplace icons.
BuildCostTotal, Shortfall, TryCharge, RefundCalculate and charge build costs.
GhostSet, BuildPreview, PartGeometry, GameRay, ToolRefs–Build preview, geometry, raycasts, tools.
ItemCablesCableType 3The “Item Line” cable type.
foreach (var it in SDModKit.Game.ItemCatalog.All)
    Log.Info(it.id + " = " + SDModKit.Game.ItemCatalog.DisplayName(it));

Give items with a key

A small test tool: pressing F6 gives the host player 10 units of an item. Handy for quickly trying out build costs and recipes.

using SDModKit.Api;
using SDModKit.Configuration;
using SDModKit.Game;
using SDModKit.Input;
using SDModKit.Util;
using UnityEngine.InputSystem;

public sealed class ItemGiverMod : SDMod
{
    public override string Id   => "example.itemgiver";
    public override string Name => "Item Giver";

    private ConfigEntry<int> _itemId;
    private ConfigEntry<int> _amount;
    private Keybind _give;

    public override void OnRegistered()
    {
        _itemId = Config.Bind("Give", "ItemId", 1, "Item ID to give");
        _amount = Config.Bind("Give", "Amount", 10, "Amount per key press");
        _give   = Keybinds.Register(Config, "Keys", "Give", Key.F6, "Give item");
    }

    public override void OnUpdate()
    {
        if (!GameRefs.InWorld || Keybinds.Typing || !_give.Pressed) return;

        if (PlayerAccess.TryGive((uint)_itemId.Value, _amount.Value, out string error))
            Toasts.Show(_amount.Value + "× " + ItemCatalog.NameOf((uint)_itemId.Value));
        else
            Toasts.Show("Failed: " + error);   // e.g. "not hosting"
    }
}
  1. RegisterIn OnInitializeMelon: ModKit.Register(new ItemGiverMod(), ConfigFile.For("example.itemgiver"));
  2. Start a world as hostTryGive only works on the host (otherwise the message is not hosting).
  3. TestPress F6 – the item lands in your inventory. Change item ID and amount in UserData/SDModKit/example.itemgiver.cfg.

Counter part with a menu

A part that shows a menu with a counter value and two buttons when you interact with it. Demonstrates how DescribeConfig, ContentActions.Send and OnAction work together – also in multiplayer.

using System.Collections.Generic;
using SDModKit.Content;
using SDModKit.UI;

public sealed class CounterBehaviour : PartBehaviour
{
    public override string ConfigTitle(ContentPart part) => "Counter";

    public override void DescribeConfig(ContentPart part, List<ConfigRow> rows)
    {
        int count = part.Config.GetInt("count", 0);

        rows.Add(new ConfigRow { Section = "Count" });
        rows.Add(new ConfigRow
        {
            Label      = "Value",
            Value      = count.ToString(),
            ValueColor = count > 0 ? Theme.Good : Theme.TextDim
        });
        rows.Add(new ConfigRow
        {
            Label    = "Action",
            Choices  = new[] { "+1", "Reset" },
            OnChoose = i => ContentActions.Send(part, i == 0 ? "inc" : "reset")
        });
    }

    // runs on the host only
    public override void OnAction(ContentPart part, TrackedPlayerServer player, string action)
    {
        int count = part.Config.GetInt("count", 0);
        if (action == "inc")   part.Config.Set("count", count + 1);
        if (action == "reset") part.Config.Set("count", 0);
    }
}
{
  "id": "example.counter",
  "name": "Counter Example",
  "version": "1.0.0",
  "parts": [
    {
      "id": 1300,
      "asset": "ExampleCounter",
      "label": "Counter",
      "donor": "StorageChest",
      "state": "StorageChestState",
      "behaviour": "example.counter",
      "menu": "config",
      "buildMenuAfter": "StorageChest",
      "config": { "count": 0 },
      "visual": { "meshes": [ { "shape": "cube", "position": [0, 0.25, 0], "scale": [0.5, 0.5, 0.5], "color": [0.9, 0.6, 0.2] } ] }
    }
  ]
}
ContentRegistry.Behaviour("example.counter", () => new CounterBehaviour());
ContentRegistry.LoadEmbedded(typeof(Plugin).Assembly, "pack.json");
  1. Check the state classStorageChestState is a placeholder. The dump gives you the real name (aim at a chest and press F9).
  2. BuildPlace the orange cube from the build menu.
  3. TestInteract → the “Counter” menu appears. “+1” increases the value, “Reset” sets it to 0. The value survives saving and reloading.

Live display

A part with a screen that continuously shows the play time and the role (host/client). Demonstrates shape: "screen" and Screens.Provide.

{
  "id": 1301,
  "asset": "ExampleDisplay",
  "label": "Info display",
  "buildMenuAfter": "SignalSplitter",
  "visual": {
    "meshes": [
      { "shape": "cube",   "position": [0, 0.2, 0],      "scale": [0.6, 0.4, 0.1], "color": [0.15, 0.17, 0.2] },
      { "shape": "screen", "screen": "example.clock",    "position": [0, 0.2, -0.051], "scale": [0.52, 0.32, 1] }
    ]
  }
}
Screens.Provide("example.clock", screen =>
{
    int t = (int)UnityEngine.Time.time;
    string role = SDModKit.Game.GameRefs.IsServer ? "HOST" : "CLIENT";
    return new ScreenFrame
    {
        Text = role + "\n" + (t / 60).ToString("00") + ":" + (t % 60).ToString("00")
    };
});
  1. BuildPlace the part – the screen immediately shows role and time.
  2. Try the tickerSet Scroll = true in the ScreenFrame and add "columns": 12 in the JSON.

Settings page with reset

A mod menu page with a toggle, a slider and a “Reset” button that shows a confirmation dialog in game style first.

using SDModKit.Api;
using SDModKit.Configuration;
using SDModKit.UI;
using SDModKit.Util;

public sealed class SettingsExampleMod : SDMod
{
    public override string Id   => "example.settings";
    public override string Name => "Settings Example";

    private ConfigEntry<bool>  _enabled;
    private ConfigEntry<float> _strength;

    public override void OnRegistered()
    {
        _enabled  = Config.Bind("General", "Enabled", true, "Feature on/off");
        _strength = Config.Bind("General", "Strength", 0.5f, "Strength 0–1");
        AddPage("Settings", Draw);
    }

    private void Draw()
    {
        ModUi.Section("General", () =>
        {
            ModUi.Row("Enabled",  () => _enabled.Value  = ModUi.Toggle(_enabled.Value, "on"));
            ModUi.Row("Strength", () => _strength.Value = ModUi.Slider(_strength.Value, 0f, 1f));
            ModUi.Note("Current value: " + _strength.Value.ToString("0.00"));
        });

        if (ModUi.Button("Reset to defaults"))
        {
            GameModal.Open("Reset?", "All values of this mod will be lost.", new[]
            {
                new ModalChoice { Label = "Cancel" },
                new ModalChoice { Label = "Reset", Bad = true, OnPick = () =>
                {
                    _enabled.Value  = _enabled.DefaultValue;
                    _strength.Value = _strength.DefaultValue;
                    Toasts.Show("Settings reset");
                }}
            });
        }
    }
}
  1. Open the mod menuWith a dev mod via Insert, otherwise via ModKit.OpenMenu() (see The mod menu).
  2. TestChange values and restart the game – they are stored in UserData/SDModKit/example.settings.cfg and persist.

Lifecycle

The order in which SDModKit and your mod are called:

Game startSDModKit loads first

MelonPriority(-1000). Kit config, menu host and content hooks are installed, then all JSON packs from UserData/SDModKit/Content are read.

Game startYour OnInitializeMelon

ContentRegistry.Behaviour(…) → LoadEmbedded(…) → ModKit.Register(…) → SDMod.OnRegistered().

Every frameSDMod.OnUpdate()

Also in the main menu. Muted mods are skipped.

World loadedContent is built

Items inserted, parts cloned and configured, attach applied, research inserted, save data (sdmodkit-content.json) loaded. Then SDMod.OnWorldEnter() and PartBehaviour.OnWorldEnter().

In the world (host)Ship updates

OnPartAdded / OnPartRemoved, then ServerTick per instance and the ShipServerTick event.

InteractionMenu

ConfigTitle + DescribeConfig; clicks → OnChoose or ContentActions.Send → OnAction on the host.

SavingWorld save

After GameSaver.SaveGame the kit writes sdmodkit-content.json next to the world save.

Leaving the worldClean-up

OnWorldExit(), configs saved, attach changes and research reverted, mutes cleared.

Multiplayer

StellarDrive uses FishNet. SDModKit cleanly separates host and client:

WhatHostClient
ServerTick, OnPartAdded, OnPartRemoved, ShipServerTickyesno
OnActionyes (triggered by the client)no
DescribeConfigyesyes, with the synced config
ContentPart.Shipsetnull
Changing PartConfigdirectlyvia OnChoose → whole config sent to host
SDMod.OnUpdate, keybinds, UIyesyes

All players need the same mods and packs. Content parts, item IDs and tech IDs must be identical on every machine, otherwise the ship data won't match. ModStamp creates stamps in the format id@version for this (version = assembly version with 3 parts).

Useful for checks: GameRefs.IsServer, GameRefs.IsClient, ModUi.Hosting().

Saving

When the world is saved, ContentSave writes the file sdmodkit-content.json into the save folder. On load (GameLoader.LoadState) it is read back.

{
  "format": 1,
  "game": "0.x.y",
  "mods":  ["modkit@1.3.3", "examplemod@1.0.0"],
  "packs": ["example.plants@1.0.0"],
  "parts": { "…": "PartConfig per ship and part" },
  "world": { "examplemod.lastVisit": "2026-10-13T12:00:00" }
}
DataStored in
PartConfigsdmodkit-content.json → parts
WorldDatasdmodkit-content.json → world
Mod settings, keybindsUserData/SDModKit/<name>.cfg (global, not per world)
Mod on/offUserData/SDModKit/modkit.cfg

Configs of parts that no longer exist are pruned when saving (PartData.Prune). The read/write status is available in ContentSave.LastWritten, LastRead and LastError.

API index

All public classes of SDModKit 1.3.3 with their public members – generated automatically from the source code. Type in the field to filter.

NamespaceTypePublic members
SDModKitModKit classMods, DevToolsPresent, Disabled, Menu, Log, Register(), IsMuted(), SaveConfigs(), Unmute(), OpenMenu()
SDModKitModStamp classSplit, GameVersion, Current(), EnabledAtLaunch(), Of(), Id(), Version()
SDModKitModToggles classFileName, Section, AnyNeedsRestart, Entry(), Enabled(), Set(), NeedsRestart(), WasEnabledAtLaunch()
SDModKitModVersionSkew classFileName, Section, Active, All, Path_, Version(), Forget()
SDModKitPlugin classGuid, OnInitializeMelon(), OnUpdate(), OnDeinitializeMelon()
SDModKit.ApiKitMod classId, Name, OnRegistered(), OnWorldEnter(), OnUpdate()
SDModKit.ApiModPage classTitle, Group, Draw, Available, ModPage(), IsAvailable()
SDModKit.ApiSDMod classId, Name, Essential, HostOnly, Dev, Pages, Log, Config, OnRegistered(), OnWorldEnter(), OnWorldExit(), OnUpdate(), AddPage(), AddPage()
SDModKit.ApiStellarModAttribute classId, Name, Description, Link, Toggleable, HostOnly, StellarModAttribute()
SDModKit.ConfigurationConfigEntry classDefaultValue, ValueType, DefaultBoxed, Boxed, Value
SDModKit.ConfigurationConfigEntryBase classSection, Key, ValueType, Boxed, DefaultBoxed, Text, ConfigEntryBase()
SDModKit.ConfigurationConfigFile className, Path_, Entries, All, Folder, Dirty, AnyDirty, For(), Bind(), SaveAllDirty(), Save()
SDModKit.ContentConfigIcon classImage, Name, State, OnClick
SDModKit.ContentConfigRow classSection, Label, Value, Choices, OnChoose, Icons, Banner, Note, Highlight, ValueColor, Tabs, ActiveTab, Aside, Icon, ValueIcon, TabColors, LabelColor, Detail, DetailColor, ChoiceEnabled
SDModKit.ContentContentActions classEnsureNetworking(), Send()
SDModKit.ContentContentAssets classInk, Text(), Bytes(), Texture(), Mesh(), Material(), ResourceNames()
SDModKit.ContentContentAttach classAttachedParts, Apply(), Revert(), HasInventory()
SDModKit.ContentContentAttachDef classId, Socket, Face, Behaviour, Menu, Config, Exclude, InventoryOnly, Pack, Parse()
SDModKit.ContentContentBuilder classCableTypeOf(), Configure(), MeshChild(), GraftSocket(), Invalidate()
SDModKit.ContentContentConfigMenu classChanged, Root, IsOpen, Current, Refresh(), TryAppend(), TryAppendClient(), Forget(), TryOpen(), UpdateClientConfig(), Close()
SDModKit.ContentContentItemDef classId, Name, Icon, Mass, MaxStack, IsFluid, IsGas, FluidColor, Model, Pack, ContentItemDef()
SDModKit.ContentContentItems classAll, Get(), Register(), EnsureMerged(), WorldEnter()
SDModKit.ContentContentLog classCapacity, Floor, ConsoleFloor, MirrorToConsole, Recent, Trace(), Info(), Warn(), Error(), Error(), Once(), Clear(), Format()
SDModKit.ContentContentLogEntry structTime, Level, Scope, Message
SDModKit.ContentContentLogLevel enum–
SDModKit.ContentContentMenus classLastOpened, LastMenu, OpenPart(), Retitle()
SDModKit.ContentContentPack classId, Name, Version, Origin, Assembly, Folder, Items, Parts, Research, Attach, Problems, Parse()
SDModKit.ContentContentPart structShip, ShipId, PartId, Part, Def, Config, Valid, Label, LocalPosition
SDModKit.ContentContentPartDef classId, Asset, Label, Description, Donor, State, Behaviour, Icon, BuildMenuAfter, BuildTab, BuildRow, Mass, Snapping, BoundsCenter, BoundsSize, ClearDonorVisual, BoundsAuto, SocketsDeclared, SnappingDeclared, FitInteraction, Menu, Attached, Cost, Meshes, Sockets, Config, Pack, ContentPartDef()
SDModKit.ContentContentRegistry classPacks, Problems, UserFolder, PartDef(), PartDefByAsset(), PartByAsset(), LoadEmbedded(), LoadJson(), LoadFile(), LoadUserFolder(), Behaviour(), RegisterAttached(), ForgetAttached(), WorldEnter(), WorldExit()
SDModKit.ContentContentResearch classAll, Apply(), Revert(), Tick()
SDModKit.ContentContentResearchDef classId, Name, Position, Hidden, GrantedBy, Requires, Parts, Features, Cost
SDModKit.ContentContentRuntime classAll, TrackedShips, ShipServerTick, Bind(), Unbind(), Of(), TryResolve(), PartsOn(), WorldEnter(), WorldExit(), PartStillExists(), Unmute()
SDModKit.ContentContentSave classFileName, Format, LastWritten, LastRead, LastError
SDModKit.ContentMenuLayout classMenu, Wrapper, Content, Panel, Grid, Title, Mass, Valid, CanAppend, Resolve(), HideOtherInventories(), SetTitle(), SetSubtitle(), Write(), Reads(), IsText()
SDModKit.ContentMeshDef classShape, Resource, DonorPath, Material, Position, Euler, Scale, Tint, Text, Screen, Font, TextColor, Columns, Speed, MeshDef()
SDModKit.ContentPartBehaviour classDef, Scope, PanelScale, OnRegistered(), OnWorldEnter(), OnWorldExit(), OnPartAdded(), OnPartRemoved(), ServerTick(), DrawConfig(), DescribeConfig(), OnAction(), ConfigTitle(), Status(), Trace(), Warn(), Error()
SDModKit.ContentPartConfig classDirty, Raw, Has(), GetInt(), GetFloat(), GetBool(), GetString(), GetIds(), Set(), Set(), Set(), Set(), SetIds()
SDModKit.ContentPartData classCount, For(), TryGet(), Forget(), Clear(), Prune(), Export(), Import()
SDModKit.ContentPartFinish classInteractionOutset, SocketStandoff, Apply(), Seat(), InteractionBox(), Volume()
SDModKit.ContentResearchLayout classExclude(), Settle()
SDModKit.ContentScreenFrame structText, Scroll, TextColor, Image
SDModKit.ContentScreens classProvide(), Font()
SDModKit.ContentScreenSurface class–
SDModKit.ContentSocketDef classId, Type, Position, Facing, Relayed
SDModKit.ContentWorldData classGet(), Set()
SDModKit.GameBuildCost classTotal(), Shortfall(), TryCharge(), Refund()
SDModKit.GameBuildPreview classCurrent()
SDModKit.GameBuildTabs classMachines, Logistics, Declare(), Add(), AddAll(), Add(), Hide()
SDModKit.GameCustomPartDefinition classId, Name, Donor, BuildMenuAfter, BuildTab, BuildRow, Configure, AutoFinish
SDModKit.GameCustomParts classRowCapacity, PartsReady, BuildMenuReport, All, Holder, RestoreBuildMenu(), CloneIntoHolder(), Register(), EnsureAll(), Get()
SDModKit.GameDropHit structObjectId, ItemId, ItemName, Quantity, Distance, IsDebris
SDModKit.GameGameRay classFromCamera(), From()
SDModKit.GameGameRefs classIsServer, IsClient, NetworkManagerCount, InWorld, PlayersServer, PlayersClient, Preferences, Controls, PlayerCamera, Service(), Invalidate()
SDModKit.GameGhostSet classPlacements, GhostSet(), Begin(), Part(), Part(), Line(), End(), Clear()
SDModKit.GameIconOverrides classSetPart(), ClearParts(), SetItem(), VanillaItemIcon(), RestoreItems()
SDModKit.GameItemCables classItem, Label, Installed, NetworkCount, MapFor(), TryGetGroup(), Forget(), RetryVisualsFor(), ReloadCableVisuals()
SDModKit.GameItemCatalog classItemAngle, All, Count, Icon(), ById(), NameOf(), Thumbnail(), DisplayName(), Invalidate()
SDModKit.GameItemMove classAccepts(), IsMachine(), Count(), Total(), Contents(), Transfer(), Move(), MaxStack()
SDModKit.GameLaserTuning classBaseLength, RangeScale, SpeedScale, SpeedAvailable, SetRange(), SetSpeed()
SDModKit.GameLocalPlayer classBuildToolItem, StateType, IsPiloting, IsOnFoot, GameMenuOpen, TryEquip()
SDModKit.GameMeshData classIsObj(), FromText(), Load(), FromObj(), FromJson()
SDModKit.GameMinableHit structLabel, ItemName, ItemId, AmountMin, AmountMax, Health, MaxHealth, Distance, Position
SDModKit.GamePartBox structCenter, Rotation, Half, Overlaps()
SDModKit.GamePartGeometry classTouchDistance, Sockets(), Invalidate(), SocketLocal(), Box(), Footprint(), Radius()
SDModKit.GamePartLookup classByAssetName(), ById(), Invalidate()
SDModKit.GamePartSocketInfo structId, Type, Local
SDModKit.GamePlayerAccess classLocalNetworkState, LocalInputs, LocalServerPlayer, WorldPosition, TryGetLocalSpace(), TryGive(), TryClearInventory(), CurrentCounts(), Invalidate()
SDModKit.GameRecycleGuard classRecycling, EnsurePatched()
SDModKit.GameServerTrade classStateOf(), HeldItem(), Has(), TryTrade(), Split()
SDModKit.GameShipAim classTryGetAimedPart(), TryPart(), ShipTransform()
SDModKit.GameSocketPositions class–
SDModKit.GameTabFitter classMenu, Refit()
SDModKit.GameToolRefs classBuild, Recycler, BuildActive, RecyclerActive
SDModKit.GameWorldQuery classScanMinables(), ScanDrops()
SDModKit.InputKeybind classLabel, File, Section, IsDefault, Key, Mouse, Display, Pressed, Released, Held, Owns(), Reset(), Clear(), SetKey(), SetMouse(), Button()
SDModKit.InputKeybinds classAll, Typing, AnyKeyboard, For(), Bound(), Register(), RegisterMouse(), SaveAll()
SDModKit.InputMouseBind enum–
SDModKit.PatchingModPatches classBrokenHandlers, Handler(), For(), ApplyAll(), Apply(), Remove()
SDModKit.UIBanner classShow(), ShowTagged()
SDModKit.UIContentPage classDraw()
SDModKit.UIDiagnosticsPage classDraw()
SDModKit.UIGameModal classIsOpen, Open(), Close(), Section(), Line(), Note(), Settle()
SDModKit.UIGameStyle classReady, Palette, Font, Background, Line, Text, TextStrong, TextDim, TextInvalid, Bad, Forget()
SDModKit.UIInteractionTips classCentreOnCrosshair, Wrapper
SDModKit.UIKeybindsPage classDraw()
SDModKit.UIKeyTips classMode(), HideVanilla(), Add(), Add(), Add(), ShortName()
SDModKit.UIMenuHost classIsOpen, Toggle(), Open(), Close(), MenuHost()
SDModKit.UIModalChoice classLabel, Bad, OnPick
SDModKit.UIModalInput classHeld, Capture(), Release(), Hold()
SDModKit.UIModUi classBeginSection(), EndSection(), Section(), Fold(), BeginRow(), EndRow(), Row(), Label(), Hosting(), Note(), Space(), Separator(), Button(), Toggle(), Text(), Int(), Slider(), Search(), Matches()
SDModKit.UISlotCounts class–
SDModKit.UITheme classWindowWidth, WindowHeight, NavWidth, LabelWidth, Panel, Section, Raised, Accent, AccentDim, Text, TextDim, SubGroup, Good, Warn, Bad, ToastNormal, ToastError, Window, Header, NavItem, NavItemActive, NavGroup, NavSubGroup, SectionBox, SectionTitle, RowLabel, Note, Status, GoodText, WarnText, BadText, Toast, Prompt, ListItem, ListItemActive, Button, Separator, Invalidate(), Solid()
SDModKit.UtilDump classMaxDepth, MaxChildren, Folder, LastPath, Write(), Everything(), Tags(), Layers(), SnapDetectors(), CableConfigs(), MenuPrefabs(), ContentParts(), DonorParts(), AimedPart(), Tree()
SDModKit.UtilEmbeddedAssets classBytes(), Text(), Texture(), TextureFromBytes()
SDModKit.UtilLog classVerbose, For(), Info(), Trace(), Warn(), Error(), Error(), Guard()
SDModKit.UtilSeal classOpen()
SDModKit.UtilToasts classShow(), Error()

Constants & defaults

ValueContent
Plugin.Guid"stellardrive.modkit"
ModToggles.FileName / Section"modkit" / "Mods"
ModVersionSkew.FileName / Section"handshake-skew" / "Versions"
ContentSave.FileName / Format"sdmodkit-content.json" / 1
ContentLog.Capacity400 entries
ContentLog.ConsoleFloorWarn (from here into the console)
CustomParts.RowCapacity8 parts per build menu row
PartFinish.InteractionOutset / SocketStandoff0.02 / 0.06 m
Dump.MaxDepth / MaxChildren8 / 64
Failure limit mods / behaviours5 exceptions → muted until world change
Config autosave2 s after the last change
Kit keys (only with a dev mod)Insert menu · F9 dump of the aimed part
Theme.Accent#4FC3E8

The mod menu

SDModKit ships its own in-game menu. Every mod gets its pages there, plus the kit pages Keybinds, Content and Diagnostics.

The Insert key does nothing?

The kit keys (Insert for the menu, F9 for the part dump) are only registered if at least one mod reports Dev => true. Without a dev mod, open the menu from code with ModKit.OpenMenu(), e.g. through your own keybind.

public sealed class DevMod : SDMod
{
    public override string Id   => "examplemod.dev";
    public override string Name => "Example Dev Tools";
    public override bool   Dev  => true;   // unlocks Insert + F9
}

// in OnInitializeMelon:
ModKit.Register(new DevMod(), ConfigFile.For("examplemod.dev"));

Content page

Overview of all loaded packs, parts and items with their problems, plus the latest messages from ContentLog (the “Clear” button empties the log).

ButtonEffect
Reload user packsRe-read packs from UserData/SDModKit/Content.
Rebuild world contentRebuild the content parts of the current world.
Unmute behavioursRe-enable muted behaviours.

Diagnostics page

Shows the part under the crosshair (“Aiming at”), the list Patches that did not bind (failed Harmony patches, e.g. after game updates) and the Write a dump area with individual dump buttons: Everything, Aimed part, Menu prefabs, Snapping table, Cables, Content parts and Vanilla part.

Kit settings in stellardrive.modkit.cfg: Diagnostics.VerboseLog (trace messages) and Diagnostics.DumpOnWorldEnter (dump on every world start).

Dumps

Dumps are text files in UserData/SDModKit/Dumps/. They are the most important tool for finding asset names, state classes, child paths (for donorPath) and materials.

SectionContent
tags, layersUnity tags and layers.
snappingAll snapping detectors/styles.
cablesCable configurations per type.
menusMenu prefabs.
content partsYour cloned parts with bounds, sockets, renderers.
donor partsVanilla parts with their object tree (up to depth 8, 64 children each).
aimed partThe part under the crosshair: settings name, ID, state class, object tree.
string path = SDModKit.Util.Dump.Everything();   // everything
SDModKit.Util.Dump.Write("myDump", sb => sb.AppendLine("custom content"));
// last path: SDModKit.Util.Dump.LastPath

Log messages

The messages appear in the MelonLoader log (MelonLoader/Latest.log) and on the Content page of the mod menu. Filter by a piece of the text:

Message (excerpt)CauseFix
pack has no idThe id field is missing at the top of the pack.Add a unique "id".
not valid jsonSyntax error (comma, bracket, quotes).Check the file in a JSON validator.
pack id … loaded twiceTwo packs with the same ID (e.g. a copy in the content folder and an embedded one).Remove the duplicate file.
part with no id or asset name skippedid = 0 or asset missing.Set both fields.
part id … already used byID collision with vanilla or another mod.Choose a different id.
failed to build, donor … was not foundWrong donor name.Use the exact asset name from the dump.
behaviour … is neither a registered key nor a type in any loaded assemblyBehaviour registered too late or wrong name.Call ContentRegistry.Behaviour before loading, or use the full class name.
does not derive from PartBehaviourThe class found does not inherit from PartBehaviour.Check the base class.
mesh not foundWrong path in resource or file not embedded.Check the path relative to the pack; for DLLs set EmbeddedResource.
mesh skipped, nothing resolved from shape/resource/donorPathNo valid mesh entry.Specify one of the three sources.
donorPath part … not foundThe part before | is not a known part.Check the asset name.
material … not found and PartMetal missingMaterial not loaded.Omit material or take the name from a dump.
texture not foundWrong icon/image path.Check path and capitalisation.
snapping style … only works on hull partsHull snapping on a normal part.Automatically set to PreciseOnFloor.
unknown snapping styleName not in the enum.See the “Snapping table” dump on the Diagnostics page.
socket ids must run 0..n-1Gap or duplicate in socket IDs.Number IDs consecutively from 0.
no socket template forNo template found for the socket type.Check the type (power, signal, fluid, item).
cost item … not found, dropped from the recipeUnknown item ID in cost.Check the ID via ItemCatalog.All.
item id … is already declaredDuplicate item ID.Choose a different ID.
has no model, it will draw blank and cannot be droppedItem without model.Add model.meshes.
position must be [x, y]Research without a valid position."position": [x, y] or hidden.
tech tree cell … already usedCell occupied.Choose a different position.
required tech … not foundUnknown ID in requires.Check the tech ID.
unlocked part … not foundUnknown asset name in research.parts.Match it with parts[].asset.
unknown featureName not in ResearchUnlockableFeature.Check the spelling.
muted after 5 failuresBehaviour/mod throws exceptions repeatedly.Read the stack trace in the log, fix the error, “Unmute behaviours”.
TrackedShipServer.UpdateParts not foundA game update changed the method.Wait for a new SDModKit version.
GameSaver.SaveGame(string) not foundGame update; saving part data stops working.Wait for a new SDModKit version.
Nothing to configure (in the menu)DescribeConfig returns no rows.Add rows or use menu: "auto".
Waiting for the host's settings (in the menu)Client is waiting for the config from the host.Normal; if it stays, the host is missing the mod.

FAQ

Do I need Unity for my own parts?

No. SDModKit does not load AssetBundles. Models come as OBJ (e.g. from Blender), as JSON meshes, as primitives or from existing game parts. You only need Unity if you want to work with bundles outside of SDModKit.

Why is Id = "mypart" underlined in red?

Id is a ushort (number 0–65535). The name belongs in Asset. In JSON: "id": 1200, "asset": "MyPart".

Which part IDs should I use?

A unique number not used by vanilla or other mods – e.g. your own block starting at 1200 per mod. Collisions are reported in the log as part id … already used by.

How do I find donor names and state classes?

Enable a dev mod (Dev => true), aim at the part in game and press F9. The dump in UserData/SDModKit/Dumps contains the settings name, ID and state class.

How do I find item IDs for build costs?

Use foreach (var it in ItemCatalog.All) Log.Info(it.id + " = " + ItemCatalog.DisplayName(it)); to write all IDs to the log.

My part doesn't show up in the build menu.

Check the log to see whether the pack loaded and the part could be built. buildMenuAfter must match an existing asset name or label; alternatively set buildTab. If the part is listed in a research, that research has to be unlocked first.

My menu (config/panel) doesn't appear.

The donor must open a storage menu (e.g. StorageChest) and a behaviour must be bound. Without a behaviour there are no rows.

I changed pack.json but nothing happens.

Packs are read at game start. Restart the game, or choose “Reload user packs” followed by “Rebuild world content” in the mod menu. Embedded packs only change with a newly built DLL.

Unofficial documentation for SDModKit 1.3.3 by Krunk, derived from SDModKit.dll. The mod is not owned by the operator of this site; all rights belong to Krunk. StellarDrive is a game by Curious Owl Games. Not affiliated with Krunk or Curious Owl Games.