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.
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.Content pack
A part made from just a JSON file and an OBJ model from Blender.
C# / .NET 4.7.2Your own mod
Logic, menus, keybinds and settings through the SDModKit API.
ReferenceJSON fields
Every field of parts, items, research and attach.
Log messages
What the warnings in the MelonLoader log mean and how to fix them.
| Property | Value |
|---|---|
| Mod name | StellarDrive Mod Kit |
| Version | 1.3.3 |
| Author | Krunk (GitHub: Krunk-theduck/SDModKit) |
| Game | MelonGame("CuriousOwlGames", "StellarDrive") |
| Load priority | MelonPriority(-1000) – loads before all other mods |
| Target framework | .NET Framework 4.7.2 (Mono) |
| Networking | FishNet (broadcasts) |
| Dependencies | MelonLoader, 0Harmony, Newtonsoft.Json, Unity InputSystem, TextMeshPro |
Read this first
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:
| Field | Type | Example | Meaning |
|---|---|---|---|
Id | ushort | 1200 | Numeric part ID (0–65535), must be unique. Not a string. |
Asset | string | "PottedPlant" | Internal name of the part (becomes the name of the PartSettings). Not a GameObject. |
Donor | string | "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 required | No | Yes (C#, Visual Studio) |
| New parts, items, research | Yes | Yes (pack embedded in the DLL) |
| Custom logic per part | Only via existing behaviours of other mods | Yes, via PartBehaviour |
| Settings, keybinds, menu pages | No | Yes |
| Location | UserData/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.
- Export the model from BlenderFile → Export → Wavefront (.obj). Enable UVs and normals, scale 1 m = 1 unit. Details in OBJ from Blender.
- Create the folder
StellarDrive/UserData/SDModKit/Content/Plants/models/and putplant.objthere. - Write pack.jsonCreate a file
pack.jsonin thePlants/folder with the content below. - Start the gameThe MelonLoader log should show
Plant Pack 1.0.0 loaded: 0 items, 1 parts, 0 techs. - 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:
| Field | Type | Default | Description |
|---|---|---|---|
idrequired | string | – | Unique pack ID, e.g. "example.plants". Without an ID the pack is discarded. |
name | string | = id | Display name in the log and the mod menu. |
version | string | "0.0.0" | Recorded as id@version in the world save. |
parts | array | [] | New parts → parts |
items | array | [] | New items/resources → items |
research | array | [] | New research nodes → research |
attach | array | [] | Attach sockets/logic to vanilla parts → attach |
Common value formats
| Format | Notation | Note |
|---|---|---|
| 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": null | Treated 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.
| Field | Type | Default | Description |
|---|---|---|---|
idrequired | ushort | – | Unique part ID (1–65535). 0 or missing → part is skipped. |
assetrequired | string | – | Internal name. Must be unique, becomes the object name of the PartSettings and is the key in research.parts and ContentRegistry.PartByAsset. |
label | string | = asset | Display name in game (fullLabel), also the title of the part menu. |
description | string | "" | Description text in the build menu. |
donor | string | "SignalSplitter" | Asset name of the vanilla part that is cloned. Determines physics, interaction and menu type. |
state | string | "SignalSplitterState" | State class (internalStateClass). With a different donor, set it to that donor's state class (listed in the dump). |
behaviour | string | null | Registered behaviour key or full class name of a PartBehaviour class. |
icon | string | null | Path to a PNG/JPG relative to the pack. Replaces the build menu thumbnail. |
buildMenuAfter | string | null | Asset 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). |
buildTab | string | null | Custom build menu tab, e.g. "Machines" or "Logistics". Takes precedence over buildMenuAfter. |
buildRow | int | 0 | Row within buildTab. |
mass | float | 10 | Mass, clamped to 0.1–2000. |
menu | string | "auto" | auto: donor menu, just retitled · config: settings panel beside the menu · panel: panel replaces the menu. See Configuration menu. |
snapping | string | donor | Name of a SnappingStyle value (case-insensitive). See Bounds & snapping. |
bounds | object | auto | { "center": [x,y,z], "size": [x,y,z] }. If missing, the size is measured from the model. |
visual | object | – | clearDonor, fitInteraction, meshes → visual & meshes. |
sockets | array | donor | Custom connectors. If the field is present (even empty), all donor sockets are removed → sockets. |
cost | object | {} | Build cost { "itemId": amount }. Unknown items are dropped with a warning. |
config | object | null | Initial values for the PartConfig of each placed instance. |
What happens during the build
ContentBuilder.Configure processes the definition in this order:
- Write label, description, state class and mass to the cloned
PartSettings. - Set snapping (only if given) and add the build cost.
- Remove the donor visuals (if
clearDonor= true) and attach your meshes under a child objectSDModKitMeshes. - Rebuild the sockets (if
socketsis present). - Measure the bounds from all renderers (if
boundsis missing) and setShipPartBounds. - Fit the interaction collider to the bounds (if
fitInteraction= true). - 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 name | Typical use |
|---|---|
SignalSplitter | Default donor. Small, floating logic part. |
FluidSplitter, PowerSplitter | Small splitters; provide socket templates for fluid/power. |
FluidTank, Battery, MathGate | Further socket template sources. |
StorageChest | Part 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).
| Field | Type | Default | Description |
|---|---|---|---|
clearDonor | bool | true | Removes renderers, mesh filters and child objects of the donor visuals. |
fitInteraction | bool | true | Fits the interaction collider to the bounds and disables overlapping colliders. |
meshes | array | [] | 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".
| Field | Type | Default | Description |
|---|---|---|---|
resource | string | null | Path to an .obj file or a JSON mesh file, relative to the pack folder (or name of an embedded resource). |
donorPath | string | null | Take a mesh from a game prefab: "Child/Path" in the donor or "OtherPart|Child/Path". |
shape | string | null | cube, sphere, cylinder, capsule, quad, plane (unknown values = cube) plus text and screen. |
material | string | "PartMetal" | Name of a loaded game material. Not found → PartMetal. |
color | colour | null | Tint, sets _BaseColor/_Color on a material copy. |
position | Vector3 | [0,0,0] | Local position. |
rotation | Vector3 | [0,0,0] | Euler angles in degrees. |
scale | Vector3 | [1,1,1] | Scale. For text/screen: x/y = area. |
text | string | null | Fixed text for shape: "text". |
textColor | colour | white / cyan | Text colour (text: otherwise color or white, screen: light cyan). |
font | string | game font | Name of a TextMeshPro font. If omitted, the most common game font is used. |
screen | string | null | Channel name for dynamic content (turns the mesh into a screen automatically). |
columns | int | 16 | Character width of the ticker text (4–64). |
speed | float | 6 | Ticker 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.
| Field | Type | Default | Description |
|---|---|---|---|
id | byte | 0 | Socket number. Must run without gaps 0 … n-1. |
type | string | "item" | power, signal, fluid or item (anything else = item). |
position | Vector3 | [0,0,0] | Local position on the part. |
facing | Vector3 | auto | Euler rotation. If omitted, the socket is oriented horizontally relative to the bounds centre. |
relayed | bool | false | All sockets with relayed: true are linked to each other as pass-through (like a splitter). |
Socket types
| JSON | CableType | Note |
|---|---|---|
signal | 0 | Signal cable |
fluid | 1 | Fluid line |
power | 2 | Power cable |
item | 3 | New 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.
| Field | Type | Default | Description |
|---|---|---|---|
idrequired | uint | – | Unique item ID. Collisions with vanilla or other mod items are skipped. |
name | string | "item <id>" | Display name. |
icon | string | null | PNG/JPG relative to the pack. |
mass | float | 1 | Mass per unit. |
maxStack | int | 20 | Stack size, 1–200. |
isFluid | bool | false | Item is a fluid. |
isGas | bool | false | Item is a gas. |
fluidColor | colour | black | Colour in tanks/lines. |
model | object | – | { "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.
| Field | Type | Default | Description |
|---|---|---|---|
idrequired | ushort | – | Unique tech ID. |
name | string | "Tech <id>" | Name in the research tree. |
position | [x, y] | [0, 0] | Cell in the tech tree. Required unless hidden. Occupied cells are reported. |
requires | ushort[] | [] | IDs of the prerequisites. |
parts | string[] | [] | Asset names of the parts that get unlocked. |
features | string[] | [] | Names from the game enum ResearchUnlockableFeature. Unknown names are ignored. |
cost | object | {} | Research cost { "itemId": amount }, custom items work too. |
hidden | bool | false | Node is not shown in the tree. |
grantedBy | ushort | 0 | Researched 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.
| Field | Type | Default | Description |
|---|---|---|---|
id | string | "attach" | Name for the log. |
socket.type | string | "item" | Socket type as in sockets. |
socket.face | string | "front" | front, back, left, right, top. Existing sockets take priority when choosing the side. |
behaviour | string | null | Behaviour for all extended parts. |
menu | string | "config" | Menu mode for extended parts. |
config | object | null | Initial PartConfig values. |
exclude | string[] | [] | 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) andusemtl. - 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
usemtlbecomes its own submesh..mtlfiles are ignored.
Recommended export settings
| Blender option | Setting | Why |
|---|---|---|
| Forward / Up | -Z / Y (default) | Matches the parser's axis conversion. |
| Scale | 1.0, apply scale first (Ctrl+A) | 1 Blender unit = 1 m in game. |
| UV Coordinates | on | For textures/material mapping. |
| Normals | on | Otherwise hard edges are smoothed. |
| Triangulated Mesh | on (recommended) | Avoids errors with non-convex faces. |
| Materials | one material if possible | The renderer only gets one material. Additional submeshes are not rendered visibly (inferred from Unity behaviour). |
| Origin | bottom centre | The 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] ]
}
| Field | Content |
|---|---|
verticesrequired | Flat list x, y, z, … (Unity coordinates, no axis conversion). |
normals | Same length as vertices, otherwise calculated automatically. |
uv | u, v per vertex (length = vertex count × 2). |
submeshesrequired | List of index lists (triangles, clockwise = Unity front face). |
name | Name 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 field | Type | Effect |
|---|---|---|
Text | string | Displayed text (TextMeshPro rich text possible, except with Scroll). |
Scroll | bool | Ticker: shows columns characters and scrolls at speed characters/s in monospace. |
TextColor | Color? | Overrides the text colour. |
Image | Texture2D | Replaces 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.
| Use | Field | Effect |
|---|---|---|
| Build menu icon of a part | parts[].icon | Overrides the rendered thumbnail (IconOverrides.SetPart). |
| Item icon | items[].icon | Icon in the inventory. |
| Material | meshes[].material | Looks 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:
- exact resource name,
- a name ending in
.models.plant.obj, - 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.
- Create the projectVisual Studio → “Class Library (.NET Framework)” or an SDK project with
net472. Not .NET 6/8. - Add referencesReference the DLLs from the game folder (see the csproj below), each with
Private=falseso they are not copied. - Build & copyCopy the finished
YourMod.dlltoStellarDrive/Mods/– next toSDModKit.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
| Namespace | Content |
|---|---|
SDModKit | ModKit, ModStamp, ModToggles |
SDModKit.Api | SDMod, ModPage |
SDModKit.Configuration | ConfigFile, ConfigEntry<T> |
SDModKit.Content | Packs, behaviours, PartConfig, screens, menus |
SDModKit.Input | Keybinds, Keybind, MouseBind |
SDModKit.UI | ModUi, Banner, GameModal, KeyTips, Theme |
SDModKit.Util | Log, Toasts, Dump, EmbeddedAssets |
SDModKit.Game | Game helpers: player, items, ships, build menu |
SDModKit.Patching | ModPatches (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
| Member | Type | Description |
|---|---|---|
Idabstract | string | Unique ID, e.g. "examplemod". Key in modkit.cfg and in the world save. |
Nameabstract | string | Display name in the mod menu. |
Essential | bool | true = cannot be switched off in the mod menu. |
Dev | bool | true enables the kit's developer keys (Insert menu, F9 dump). |
HostOnly | bool | Exists, but is not evaluated anywhere in 1.3.3. |
Config | ConfigFile | The file passed to Register. |
Log | Log | Logger prefixed with the mod name. |
Pages | IReadOnlyList<ModPage> | Your own pages in the mod menu. |
OnRegistered() | virtual | Right after Register. Create config entries, keybinds and pages here. |
OnWorldEnter() | virtual | When a world has loaded (host and client). |
OnWorldExit() | virtual | When leaving the world. |
OnUpdate() | virtual | Every frame (also in the main menu). |
AddPage(title, draw) | protected | Add a page. Overload AddPage(group, title, draw) for sub-groups. |
ModKit – static API
| Member | Description |
|---|---|
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 / Disabled | Active 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. |
DevToolsPresent | true if a registered mod has Dev => true. |
Menu, Log | Menu 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()orConfigFile.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
}
| Member | Description |
|---|---|
Pressed / Held / Released | State in the current frame. |
Display | Display text of the binding, e.g. “G”. |
Key / Mouse | Current 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
| Method | When / 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. |
PanelScale | Size 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 / Error | Log 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
| Member | Description |
|---|---|
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.ShipServerTick | Event Action<IShipStateWrite, float> – once per ship update on the host. |
ContentRuntime.All, TrackedShips | All behaviours, number of tracked ships. |
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();
}
| ModUi | Description |
|---|---|
Section(title, body), BeginSection/EndSection | Box with a heading. |
Fold(title, body) | Collapsible area. |
Row(label, controls), BeginRow/EndRow | Row with a label on the left. |
Label, Note, Space, Separator | Text, 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
| API | Description |
|---|---|
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. |
Theme | Kit colours: Accent, Text, TextDim, Good, Warn, Bad, Panel … |
GameStyle | Game 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.
| Class | Key members | Purpose |
|---|---|---|
GameRefs | IsServer, IsClient, InWorld, PlayersServer, PlayerCamera, Service<T>() | Basic state and game services. |
LocalPlayer | StateType, IsPiloting, IsOnFoot, GameMenuOpen, TryEquip | Local player. |
PlayerAccess | LocalServerPlayer, WorldPosition, TryGive(itemId, qty, out err), TryClearInventory, CurrentCounts | Inventory and position. |
ItemCatalog | All, ById, NameOf, DisplayName, Icon, Thumbnail | All items (incl. mod items). |
PartLookup | ByAssetName, ById | Find PartSettings. |
ItemMove | Accepts, Count, Total, Contents, Transfer, Move, MaxStack, IsMachine | Move items between inventories (host). |
ServerTrade | Has, TryTrade, Split, HeldItem | Trading/item in hand. |
WorldQuery | ScanMinables, ScanDrops | Minable objects and drops nearby. |
ShipAim | TryGetAimedPart, TryPart, ShipTransform | Part under the crosshair. |
LaserTuning | SetRange(owner, factor), SetSpeed(owner, factor) | Adjust the mining laser. |
BuildTabs | Declare(tab, order, icon), Add, AddAll, Hide | Custom build menu tabs (“Machines” = 10, “Logistics” = 20). |
CustomParts | Register(CustomPartDefinition), Get, All, PartsReady | Parts via code instead of JSON (Id, Name, Donor, BuildMenuAfter, BuildTab, BuildRow, Configure, AutoFinish). |
IconOverrides | SetPart, SetItem | Replace icons. |
BuildCost | Total, Shortfall, TryCharge, Refund | Calculate and charge build costs. |
GhostSet, BuildPreview, PartGeometry, GameRay, ToolRefs | – | Build preview, geometry, raycasts, tools. |
ItemCables | CableType 3 | The “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"
}
}
- RegisterIn
OnInitializeMelon:ModKit.Register(new ItemGiverMod(), ConfigFile.For("example.itemgiver")); - Start a world as host
TryGiveonly works on the host (otherwise the message isnot hosting). - 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");
- Check the state class
StorageChestStateis a placeholder. The dump gives you the real name (aim at a chest and press F9). - BuildPlace the orange cube from the build menu.
- 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")
};
});
- BuildPlace the part – the screen immediately shows role and time.
- Try the tickerSet
Scroll = truein theScreenFrameand add"columns": 12in 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");
}}
});
}
}
}
- Open the mod menuWith a dev mod via Insert, otherwise via
ModKit.OpenMenu()(see The mod menu). - TestChange values and restart the game – they are stored in
UserData/SDModKit/example.settings.cfgand persist.
Lifecycle
The order in which SDModKit and your mod are called:
MelonPriority(-1000). Kit config, menu host and content hooks are installed, then all JSON packs from UserData/SDModKit/Content are read.
OnInitializeMelonContentRegistry.Behaviour(…) → LoadEmbedded(…) → ModKit.Register(…) → SDMod.OnRegistered().
SDMod.OnUpdate()Also in the main menu. Muted mods are skipped.
Items inserted, parts cloned and configured, attach applied, research inserted, save data (sdmodkit-content.json) loaded. Then SDMod.OnWorldEnter() and PartBehaviour.OnWorldEnter().
OnPartAdded / OnPartRemoved, then ServerTick per instance and the ShipServerTick event.
ConfigTitle + DescribeConfig; clicks → OnChoose or ContentActions.Send → OnAction on the host.
After GameSaver.SaveGame the kit writes sdmodkit-content.json next to the world save.
OnWorldExit(), configs saved, attach changes and research reverted, mutes cleared.
Multiplayer
StellarDrive uses FishNet. SDModKit cleanly separates host and client:
| What | Host | Client |
|---|---|---|
ServerTick, OnPartAdded, OnPartRemoved, ShipServerTick | yes | no |
OnAction | yes (triggered by the client) | no |
DescribeConfig | yes | yes, with the synced config |
ContentPart.Ship | set | null |
| Changing PartConfig | directly | via OnChoose → whole config sent to host |
SDMod.OnUpdate, keybinds, UI | yes | yes |
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" }
}
| Data | Stored in |
|---|---|
| PartConfig | sdmodkit-content.json → parts |
| WorldData | sdmodkit-content.json → world |
| Mod settings, keybinds | UserData/SDModKit/<name>.cfg (global, not per world) |
| Mod on/off | UserData/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.
| Namespace | Type | Public members |
|---|---|---|
SDModKit | ModKit class | Mods, DevToolsPresent, Disabled, Menu, Log, Register(), IsMuted(), SaveConfigs(), Unmute(), OpenMenu() |
SDModKit | ModStamp class | Split, GameVersion, Current(), EnabledAtLaunch(), Of(), Id(), Version() |
SDModKit | ModToggles class | FileName, Section, AnyNeedsRestart, Entry(), Enabled(), Set(), NeedsRestart(), WasEnabledAtLaunch() |
SDModKit | ModVersionSkew class | FileName, Section, Active, All, Path_, Version(), Forget() |
SDModKit | Plugin class | Guid, OnInitializeMelon(), OnUpdate(), OnDeinitializeMelon() |
SDModKit.Api | KitMod class | Id, Name, OnRegistered(), OnWorldEnter(), OnUpdate() |
SDModKit.Api | ModPage class | Title, Group, Draw, Available, ModPage(), IsAvailable() |
SDModKit.Api | SDMod class | Id, Name, Essential, HostOnly, Dev, Pages, Log, Config, OnRegistered(), OnWorldEnter(), OnWorldExit(), OnUpdate(), AddPage(), AddPage() |
SDModKit.Api | StellarModAttribute class | Id, Name, Description, Link, Toggleable, HostOnly, StellarModAttribute() |
SDModKit.Configuration | ConfigEntry class | DefaultValue, ValueType, DefaultBoxed, Boxed, Value |
SDModKit.Configuration | ConfigEntryBase class | Section, Key, ValueType, Boxed, DefaultBoxed, Text, ConfigEntryBase() |
SDModKit.Configuration | ConfigFile class | Name, Path_, Entries, All, Folder, Dirty, AnyDirty, For(), Bind(), SaveAllDirty(), Save() |
SDModKit.Content | ConfigIcon class | Image, Name, State, OnClick |
SDModKit.Content | ConfigRow class | Section, Label, Value, Choices, OnChoose, Icons, Banner, Note, Highlight, ValueColor, Tabs, ActiveTab, Aside, Icon, ValueIcon, TabColors, LabelColor, Detail, DetailColor, ChoiceEnabled |
SDModKit.Content | ContentActions class | EnsureNetworking(), Send() |
SDModKit.Content | ContentAssets class | Ink, Text(), Bytes(), Texture(), Mesh(), Material(), ResourceNames() |
SDModKit.Content | ContentAttach class | AttachedParts, Apply(), Revert(), HasInventory() |
SDModKit.Content | ContentAttachDef class | Id, Socket, Face, Behaviour, Menu, Config, Exclude, InventoryOnly, Pack, Parse() |
SDModKit.Content | ContentBuilder class | CableTypeOf(), Configure(), MeshChild(), GraftSocket(), Invalidate() |
SDModKit.Content | ContentConfigMenu class | Changed, Root, IsOpen, Current, Refresh(), TryAppend(), TryAppendClient(), Forget(), TryOpen(), UpdateClientConfig(), Close() |
SDModKit.Content | ContentItemDef class | Id, Name, Icon, Mass, MaxStack, IsFluid, IsGas, FluidColor, Model, Pack, ContentItemDef() |
SDModKit.Content | ContentItems class | All, Get(), Register(), EnsureMerged(), WorldEnter() |
SDModKit.Content | ContentLog class | Capacity, Floor, ConsoleFloor, MirrorToConsole, Recent, Trace(), Info(), Warn(), Error(), Error(), Once(), Clear(), Format() |
SDModKit.Content | ContentLogEntry struct | Time, Level, Scope, Message |
SDModKit.Content | ContentLogLevel enum | – |
SDModKit.Content | ContentMenus class | LastOpened, LastMenu, OpenPart(), Retitle() |
SDModKit.Content | ContentPack class | Id, Name, Version, Origin, Assembly, Folder, Items, Parts, Research, Attach, Problems, Parse() |
SDModKit.Content | ContentPart struct | Ship, ShipId, PartId, Part, Def, Config, Valid, Label, LocalPosition |
SDModKit.Content | ContentPartDef class | Id, 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.Content | ContentRegistry class | Packs, Problems, UserFolder, PartDef(), PartDefByAsset(), PartByAsset(), LoadEmbedded(), LoadJson(), LoadFile(), LoadUserFolder(), Behaviour(), RegisterAttached(), ForgetAttached(), WorldEnter(), WorldExit() |
SDModKit.Content | ContentResearch class | All, Apply(), Revert(), Tick() |
SDModKit.Content | ContentResearchDef class | Id, Name, Position, Hidden, GrantedBy, Requires, Parts, Features, Cost |
SDModKit.Content | ContentRuntime class | All, TrackedShips, ShipServerTick, Bind(), Unbind(), Of(), TryResolve(), PartsOn(), WorldEnter(), WorldExit(), PartStillExists(), Unmute() |
SDModKit.Content | ContentSave class | FileName, Format, LastWritten, LastRead, LastError |
SDModKit.Content | MenuLayout class | Menu, Wrapper, Content, Panel, Grid, Title, Mass, Valid, CanAppend, Resolve(), HideOtherInventories(), SetTitle(), SetSubtitle(), Write(), Reads(), IsText() |
SDModKit.Content | MeshDef class | Shape, Resource, DonorPath, Material, Position, Euler, Scale, Tint, Text, Screen, Font, TextColor, Columns, Speed, MeshDef() |
SDModKit.Content | PartBehaviour class | Def, Scope, PanelScale, OnRegistered(), OnWorldEnter(), OnWorldExit(), OnPartAdded(), OnPartRemoved(), ServerTick(), DrawConfig(), DescribeConfig(), OnAction(), ConfigTitle(), Status(), Trace(), Warn(), Error() |
SDModKit.Content | PartConfig class | Dirty, Raw, Has(), GetInt(), GetFloat(), GetBool(), GetString(), GetIds(), Set(), Set(), Set(), Set(), SetIds() |
SDModKit.Content | PartData class | Count, For(), TryGet(), Forget(), Clear(), Prune(), Export(), Import() |
SDModKit.Content | PartFinish class | InteractionOutset, SocketStandoff, Apply(), Seat(), InteractionBox(), Volume() |
SDModKit.Content | ResearchLayout class | Exclude(), Settle() |
SDModKit.Content | ScreenFrame struct | Text, Scroll, TextColor, Image |
SDModKit.Content | Screens class | Provide(), Font() |
SDModKit.Content | ScreenSurface class | – |
SDModKit.Content | SocketDef class | Id, Type, Position, Facing, Relayed |
SDModKit.Content | WorldData class | Get(), Set() |
SDModKit.Game | BuildCost class | Total(), Shortfall(), TryCharge(), Refund() |
SDModKit.Game | BuildPreview class | Current() |
SDModKit.Game | BuildTabs class | Machines, Logistics, Declare(), Add(), AddAll(), Add(), Hide() |
SDModKit.Game | CustomPartDefinition class | Id, Name, Donor, BuildMenuAfter, BuildTab, BuildRow, Configure, AutoFinish |
SDModKit.Game | CustomParts class | RowCapacity, PartsReady, BuildMenuReport, All, Holder, RestoreBuildMenu(), CloneIntoHolder(), Register(), EnsureAll(), Get() |
SDModKit.Game | DropHit struct | ObjectId, ItemId, ItemName, Quantity, Distance, IsDebris |
SDModKit.Game | GameRay class | FromCamera(), From() |
SDModKit.Game | GameRefs class | IsServer, IsClient, NetworkManagerCount, InWorld, PlayersServer, PlayersClient, Preferences, Controls, PlayerCamera, Service(), Invalidate() |
SDModKit.Game | GhostSet class | Placements, GhostSet(), Begin(), Part(), Part(), Line(), End(), Clear() |
SDModKit.Game | IconOverrides class | SetPart(), ClearParts(), SetItem(), VanillaItemIcon(), RestoreItems() |
SDModKit.Game | ItemCables class | Item, Label, Installed, NetworkCount, MapFor(), TryGetGroup(), Forget(), RetryVisualsFor(), ReloadCableVisuals() |
SDModKit.Game | ItemCatalog class | ItemAngle, All, Count, Icon(), ById(), NameOf(), Thumbnail(), DisplayName(), Invalidate() |
SDModKit.Game | ItemMove class | Accepts(), IsMachine(), Count(), Total(), Contents(), Transfer(), Move(), MaxStack() |
SDModKit.Game | LaserTuning class | BaseLength, RangeScale, SpeedScale, SpeedAvailable, SetRange(), SetSpeed() |
SDModKit.Game | LocalPlayer class | BuildToolItem, StateType, IsPiloting, IsOnFoot, GameMenuOpen, TryEquip() |
SDModKit.Game | MeshData class | IsObj(), FromText(), Load(), FromObj(), FromJson() |
SDModKit.Game | MinableHit struct | Label, ItemName, ItemId, AmountMin, AmountMax, Health, MaxHealth, Distance, Position |
SDModKit.Game | PartBox struct | Center, Rotation, Half, Overlaps() |
SDModKit.Game | PartGeometry class | TouchDistance, Sockets(), Invalidate(), SocketLocal(), Box(), Footprint(), Radius() |
SDModKit.Game | PartLookup class | ByAssetName(), ById(), Invalidate() |
SDModKit.Game | PartSocketInfo struct | Id, Type, Local |
SDModKit.Game | PlayerAccess class | LocalNetworkState, LocalInputs, LocalServerPlayer, WorldPosition, TryGetLocalSpace(), TryGive(), TryClearInventory(), CurrentCounts(), Invalidate() |
SDModKit.Game | RecycleGuard class | Recycling, EnsurePatched() |
SDModKit.Game | ServerTrade class | StateOf(), HeldItem(), Has(), TryTrade(), Split() |
SDModKit.Game | ShipAim class | TryGetAimedPart(), TryPart(), ShipTransform() |
SDModKit.Game | SocketPositions class | – |
SDModKit.Game | TabFitter class | Menu, Refit() |
SDModKit.Game | ToolRefs class | Build, Recycler, BuildActive, RecyclerActive |
SDModKit.Game | WorldQuery class | ScanMinables(), ScanDrops() |
SDModKit.Input | Keybind class | Label, File, Section, IsDefault, Key, Mouse, Display, Pressed, Released, Held, Owns(), Reset(), Clear(), SetKey(), SetMouse(), Button() |
SDModKit.Input | Keybinds class | All, Typing, AnyKeyboard, For(), Bound(), Register(), RegisterMouse(), SaveAll() |
SDModKit.Input | MouseBind enum | – |
SDModKit.Patching | ModPatches class | BrokenHandlers, Handler(), For(), ApplyAll(), Apply(), Remove() |
SDModKit.UI | Banner class | Show(), ShowTagged() |
SDModKit.UI | ContentPage class | Draw() |
SDModKit.UI | DiagnosticsPage class | Draw() |
SDModKit.UI | GameModal class | IsOpen, Open(), Close(), Section(), Line(), Note(), Settle() |
SDModKit.UI | GameStyle class | Ready, Palette, Font, Background, Line, Text, TextStrong, TextDim, TextInvalid, Bad, Forget() |
SDModKit.UI | InteractionTips class | CentreOnCrosshair, Wrapper |
SDModKit.UI | KeybindsPage class | Draw() |
SDModKit.UI | KeyTips class | Mode(), HideVanilla(), Add(), Add(), Add(), ShortName() |
SDModKit.UI | MenuHost class | IsOpen, Toggle(), Open(), Close(), MenuHost() |
SDModKit.UI | ModalChoice class | Label, Bad, OnPick |
SDModKit.UI | ModalInput class | Held, Capture(), Release(), Hold() |
SDModKit.UI | ModUi class | BeginSection(), EndSection(), Section(), Fold(), BeginRow(), EndRow(), Row(), Label(), Hosting(), Note(), Space(), Separator(), Button(), Toggle(), Text(), Int(), Slider(), Search(), Matches() |
SDModKit.UI | SlotCounts class | – |
SDModKit.UI | Theme class | WindowWidth, 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.Util | Dump class | MaxDepth, MaxChildren, Folder, LastPath, Write(), Everything(), Tags(), Layers(), SnapDetectors(), CableConfigs(), MenuPrefabs(), ContentParts(), DonorParts(), AimedPart(), Tree() |
SDModKit.Util | EmbeddedAssets class | Bytes(), Text(), Texture(), TextureFromBytes() |
SDModKit.Util | Log class | Verbose, For(), Info(), Trace(), Warn(), Error(), Error(), Guard() |
SDModKit.Util | Seal class | Open() |
SDModKit.Util | Toasts class | Show(), Error() |
Constants & defaults
| Value | Content |
|---|---|
Plugin.Guid | "stellardrive.modkit" |
ModToggles.FileName / Section | "modkit" / "Mods" |
ModVersionSkew.FileName / Section | "handshake-skew" / "Versions" |
ContentSave.FileName / Format | "sdmodkit-content.json" / 1 |
ContentLog.Capacity | 400 entries |
ContentLog.ConsoleFloor | Warn (from here into the console) |
CustomParts.RowCapacity | 8 parts per build menu row |
PartFinish.InteractionOutset / SocketStandoff | 0.02 / 0.06 m |
Dump.MaxDepth / MaxChildren | 8 / 64 |
| Failure limit mods / behaviours | 5 exceptions → muted until world change |
| Config autosave | 2 s after the last change |
| Kit keys (only with a dev mod) | Insert menu · F9 dump of the aimed part |
Theme.Accent | #4FC3E8 |
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.
| Section | Content |
|---|---|
tags, layers | Unity tags and layers. |
snapping | All snapping detectors/styles. |
cables | Cable configurations per type. |
menus | Menu prefabs. |
content parts | Your cloned parts with bounds, sockets, renderers. |
donor parts | Vanilla parts with their object tree (up to depth 8, 64 children each). |
aimed part | The 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) | Cause | Fix |
|---|---|---|
pack has no id | The id field is missing at the top of the pack. | Add a unique "id". |
not valid json | Syntax error (comma, bracket, quotes). | Check the file in a JSON validator. |
pack id … loaded twice | Two 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 skipped | id = 0 or asset missing. | Set both fields. |
part id … already used by | ID collision with vanilla or another mod. | Choose a different id. |
failed to build, donor … was not found | Wrong donor name. | Use the exact asset name from the dump. |
behaviour … is neither a registered key nor a type in any loaded assembly | Behaviour registered too late or wrong name. | Call ContentRegistry.Behaviour before loading, or use the full class name. |
does not derive from PartBehaviour | The class found does not inherit from PartBehaviour. | Check the base class. |
mesh not found | Wrong 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/donorPath | No valid mesh entry. | Specify one of the three sources. |
donorPath part … not found | The part before | is not a known part. | Check the asset name. |
material … not found and PartMetal missing | Material not loaded. | Omit material or take the name from a dump. |
texture not found | Wrong icon/image path. | Check path and capitalisation. |
snapping style … only works on hull parts | Hull snapping on a normal part. | Automatically set to PreciseOnFloor. |
unknown snapping style | Name not in the enum. | See the “Snapping table” dump on the Diagnostics page. |
socket ids must run 0..n-1 | Gap or duplicate in socket IDs. | Number IDs consecutively from 0. |
no socket template for | No template found for the socket type. | Check the type (power, signal, fluid, item). |
cost item … not found, dropped from the recipe | Unknown item ID in cost. | Check the ID via ItemCatalog.All. |
item id … is already declared | Duplicate item ID. | Choose a different ID. |
has no model, it will draw blank and cannot be dropped | Item without model. | Add model.meshes. |
position must be [x, y] | Research without a valid position. | "position": [x, y] or hidden. |
tech tree cell … already used | Cell occupied. | Choose a different position. |
required tech … not found | Unknown ID in requires. | Check the tech ID. |
unlocked part … not found | Unknown asset name in research.parts. | Match it with parts[].asset. |
unknown feature | Name not in ResearchUnlockableFeature. | Check the spelling. |
muted after 5 failures | Behaviour/mod throws exceptions repeatedly. | Read the stack trace in the log, fix the error, “Unmute behaviours”. |
TrackedShipServer.UpdateParts not found | A game update changed the method. | Wait for a new SDModKit version. |
GameSaver.SaveGame(string) not found | Game 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.