# SDModKit Documentation – StellarDrive Mod Kit for Stellar Drive Modding > Complete developer documentation for SDModKit 1.3.3, the StellarDrive Mod Kit for MelonLoader: custom parts, items and research via JSON, C# API, Blender models, multiplayer and troubleshooting – with examples. URL: https://sdmodkit.mauscd.de/en/ SDModKit source: https://github.com/Krunk-theduck/SDModKit ## 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. **Disclaimer**This 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](https://github.com/Krunk-theduck/SDModKit). 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. | Property | Value | | --- | --- | | Mod name | StellarDrive Mod Kit | | Version | `1.3.3` | | Author | Krunk ([GitHub: Krunk-theduck/SDModKit](https://github.com/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 **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: | 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`. Game folder: ```plaintext 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) ├─ .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](https://sdmodkit.mauscd.de/en/#speicherung)). 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. 1. **Export the model from Blender**File → Export → Wavefront (.obj). Enable UVs and normals, scale 1 m = 1 unit. Details in [OBJ from Blender](https://sdmodkit.mauscd.de/en/#blender-obj). 2. **Create the folder**`StellarDrive/UserData/SDModKit/Content/Plants/models/` and put `plant.obj` there. 3. **Write pack.json**Create a file `pack.json` in the `Plants/` folder with the content below. 4. **Start the game**The MelonLoader log should show `Plant Pack 1.0.0 loaded: 0 items, 1 parts, 0 techs`. 5. **Load a world**The part appears in the build menu right after the signal splitter. UserData/SDModKit/Content/Plants/pack.json: ```json { "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](https://sdmodkit.mauscd.de/en/#projekt-setup). Main.cs: ```csharp 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 \_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](https://sdmodkit.mauscd.de/en/#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 | | --- | --- | --- | --- | | `id`required | 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](https://sdmodkit.mauscd.de/en/#parts) | | `items` | array | [] | New items/resources → [items](https://sdmodkit.mauscd.de/en/#items) | | `research` | array | [] | New research nodes → [research](https://sdmodkit.mauscd.de/en/#research) | | `attach` | array | [] | Attach sockets/logic to vanilla parts → [attach](https://sdmodkit.mauscd.de/en/#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 | | --- | --- | --- | --- | | `id`required | ushort | – | Unique part ID (1–65535). `0` or missing → part is skipped. | | `asset`required | 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](https://sdmodkit.mauscd.de/en/#dumps)). | | `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](https://sdmodkit.mauscd.de/en/#config-menue). | | `snapping` | string | donor | Name of a `SnappingStyle` value (case-insensitive). See [Bounds & snapping](https://sdmodkit.mauscd.de/en/#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](https://sdmodkit.mauscd.de/en/#visual). | | `sockets` | array | donor | Custom connectors. If the field is present (even empty), all donor sockets are removed → [sockets](https://sdmodkit.mauscd.de/en/#sockets). | | `cost` | object | {} | Build cost `{ "itemId": amount }`. Unknown items are dropped with a warning. | | `config` | object | null | Initial values for the [PartConfig](https://sdmodkit.mauscd.de/en/#daten) 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 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 id ` and `state `. ## 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). | Example: housing made from primitives: ```json "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. | Example: two item ports: ```json "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 | | --- | --- | --- | --- | | `id`required | uint | – | Unique item ID. Collisions with vanilla or other mod items are skipped. | | `name` | string | "item " | 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](https://sdmodkit.mauscd.de/en/#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`). Example: ```json "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 | | --- | --- | --- | --- | | `id`required | ushort | – | Unique tech ID. | | `name` | string | "Tech " | 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). | Example: ```json "research": [ { "id": 4100, "name": "Hydroponics", "position": [6, 2], "requires": [12], "parts": ["PottedPlant"], "cost": { "9001": 20 } } ] ``` With `"hidden": true` and `"grantedBy": ` 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](https://sdmodkit.mauscd.de/en/#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. | Example: item ports on all containers: ```json "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. UserData/SDModKit/Content/ExampleMonitor/pack.json: ```json { "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 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. models/wedge.json: ```json { "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 | | --- | --- | | `vertices`required | 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). | | `submeshes`required | 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. ```json { "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. C#: ```csharp 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`). | 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. ExampleMod.csproj (excerpt): ```xml ``` C#: ```csharp 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 project**Visual Studio → “Class Library (.NET Framework)” or an SDK project with `net472`. Not .NET 6/8. 2. **Add references**Reference the DLLs from the game folder (see the csproj below), each with `Private=false` so they are not copied. 3. **Build & copy**Copy the finished `YourMod.dll` to `StellarDrive/Mods/` – next to `SDModKit.dll`. ExampleMod.csproj: ```xml net472 latest ExampleMod ExampleMod 1.0.0 C:\Program Files (x86)\Steam\steamapps\common\StellarDrive $(GameDir)\StellarDrive\_Data\Managed $(GameDir)\MelonLoader\net35\MelonLoader.dllfalse $(GameDir)\MelonLoader\net35\0Harmony.dllfalse $(GameDir)\Mods\SDModKit.dllfalse $(Managed)\Assembly-CSharp.dllfalse $(Managed)\UnityEngine.CoreModule.dllfalse $(Managed)\UnityEngine.IMGUIModule.dllfalse $(Managed)\Unity.InputSystem.dllfalse $(Managed)\Unity.TextMeshPro.dllfalse $(Managed)\Newtonsoft.Json.dllfalse $(Managed)\FishNet.Runtime.dllfalse ``` 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` | | `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 | | --- | --- | --- | | `Id`abstract | string | Unique ID, e.g. `"examplemod"`. Key in `modkit.cfg` and in the world save. | | `Name`abstract | 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 | 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: UserData/SDModKit/examplemod.cfg: ```ini [General] # Speed Speed = 1.5 [Keys] Hello = H ``` C#: ```csharp ConfigEntry speed = Config.Bind("General", "Speed", 1.5f, "Speed"); ConfigEntry enabled = Config.Bind("General", "Enabled", true, "Feature on/off"); ConfigEntry 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. C#: ```csharp 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 C#: ```csharp // 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](https://sdmodkit.mauscd.de/en/#config-menue)). | | `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 PlantBehaviour.cs: ```csharp 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 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); } } } ``` pack.json (excerpt): ```json { "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](https://sdmodkit.mauscd.de/en/#dumps) 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` – once per ship update on the host. | | `ContentRuntime.All`, `TrackedShips` | All 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`. | `menu` | Result | | --- | --- | | `auto` | The donor menu stays, it only gets your title. | | `config` | Simple settings panel beside the donor menu (section, label, value, choices, icons). | | `panel` | Your own panel replaces the menu completely – with tabs, banner, notes, icons, detail text and colours. | ### ConfigRow – fields | Field | Type | Effect | | --- | --- | --- | | `Section` | string | Sub-heading (row with only `Section`). | | `Label` | string | Label on the left. | | `Value` | string | Value on the right. | | `ValueColor` / `LabelColor` | Color? | Colours, e.g. `Theme.Good`, `Theme.Warn`, `Theme.Bad`. | | `Choices` | string[] | Choice buttons. | | `ChoiceEnabled` | bool[] | Disable individual buttons (panel). | | `OnChoose` | Action | Called with the index; the menu rebuilds afterwards. | | `Highlight` | bool | Highlight the row. | | `Icons` | List | Icon strip: `Image`, `Name` (tooltip), `State` (tint index), `OnClick`. | | `Icon` / `ValueIcon` | Texture | Small image before the label or value (panel). | | `Detail` / `DetailColor` | string / Color? | Second, smaller text line (panel). | | `Banner` | string | Notice bar at the top (panel). | | `Note` | string | Note text (panel). | | `Tabs`, `ActiveTab`, `TabColors`, `Aside` | string[], int, Color[], string | Tab 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. C#: ```csharp 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 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: C#: ```csharp 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. C#: ```csharp 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. C#: ```csharp 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 C#: ```csharp 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`. | C#: ```csharp 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()` | 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. | C# – write all item IDs to the log: ```csharp 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. ItemGiverMod.cs: ```csharp 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 \_itemId; private ConfigEntry \_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. **Register**In `OnInitializeMelon`: `ModKit.Register(new ItemGiverMod(), ConfigFile.For("example.itemgiver"));` 2. **Start a world as host**`TryGive` only works on the host (otherwise the message is `not hosting`). 3. **Test**Press `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. CounterBehaviour.cs: ```csharp 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 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); } } ``` pack.json: ```json { "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] } ] } } ] } ``` Plugin.cs (excerpt): ```csharp ContentRegistry.Behaviour("example.counter", () => new CounterBehaviour()); ContentRegistry.LoadEmbedded(typeof(Plugin).Assembly, "pack.json"); ``` 1. **Check the state class**`StorageChestState` is a placeholder. The dump gives you the real name (aim at a chest and press `F9`). 2. **Build**Place the orange cube from the build menu. 3. **Test**Interact → 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`. pack.json (part): ```json { "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] } ] } } ``` C# – in OnRegistered(): ```csharp 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. **Build**Place the part – the screen immediately shows role and time. 2. **Try the ticker**Set `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. SettingsExampleMod.cs: ```csharp 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 \_enabled; private ConfigEntry \_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 menu**With a dev mod via `Insert`, otherwise via `ModKit.OpenMenu()` (see [The mod menu](https://sdmodkit.mauscd.de/en/#modkit-menue)). 2. **Test**Change 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 start**SDModKit loads first** `MelonPriority(-1000)`. Kit config, menu host and content hooks are installed, then all JSON packs from `UserData/SDModKit/Content` are read. Game start**Your `OnInitializeMelon`** `ContentRegistry.Behaviour(…)` → `LoadEmbedded(…)` → `ModKit.Register(…)` → `SDMod.OnRegistered()`. Every frame**`SDMod.OnUpdate()`** Also in the main menu. Muted mods are skipped. World loaded**Content 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. Interaction**Menu** `ConfigTitle` + `DescribeConfig`; clicks → `OnChoose` or `ContentActions.Send` → `OnAction` on the host. Saving**World save** After `GameSaver.SaveGame` the kit writes `sdmodkit-content.json` next to the world save. Leaving the world**Clean-up** `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. sdmodkit-content.json: ```json { "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/.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` | ## 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. DevMod.cs – enable developer keys: ```csharp 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). | Button | Effect | | --- | --- | | Reload user packs | Re-read packs from `UserData/SDModKit/Content`. | | Rebuild world content | Rebuild the content parts of the current world. | | Unmute behaviours | Re-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. | 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. | C#: ```csharp 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.