SDModKit Docs v1.3.3
EN

StellarDrive · MelonLoader · SDModKit 1.3.3

SDModKit Entwickler-Dokumentation

Alles, was du brauchst, um mit dem StellarDrive Mod Kit (SDModKit) eigene Parts, Items, Forschungen und Logik für StellarDrive (auch „Stellar Drive“) zu bauen – von der ersten JSON-Datei bis zur eigenen C#-Mod mit MelonLoader.

Überblick

SDModKit („StellarDrive Mod Kit“) ist eine gemeinsame MelonLoader-Bibliothek von Krunk, auf der viele StellarDrive-Mods aufbauen. Sie nimmt dir die schwierigen Teile des Moddings ab: neue Bauteile ins Spiel klonen, Baumenü-Einträge anlegen, Sockets und Kabel, Konfigurationsmenüs, Speichern im Spielstand, Multiplayer-Synchronisation, Keybinds und ein eigenes Ingame-Mod-Menü.

Hinweis / DisclaimerDies ist eine inoffizielle Community-Dokumentation. SDModKit gehört nicht dem Betreiber dieser Seite – alle Rechte an der Mod liegen bei ihrem Autor Krunk. Die Inhalte wurden aus der Datei SDModKit.dll (Version 1.3.3) abgeleitet und können bei neueren Versionen abweichen. StellarDrive ist ein Spiel von Curious Owl Games; diese Seite steht in keiner Verbindung zu Krunk oder Curious Owl Games.
EigenschaftWert
Mod-NameStellarDrive Mod Kit
Version1.3.3
AutorKrunk (GitHub: Krunk-theduck/SDModKit)
SpielMelonGame("CuriousOwlGames", "StellarDrive")
LadeprioritätMelonPriority(-1000) – lädt vor allen anderen Mods
Zielframework.NET Framework 4.7.2 (Mono)
NetzwerkFishNet (Broadcasts)
AbhängigkeitenMelonLoader, 0Harmony, Newtonsoft.Json, Unity InputSystem, TextMeshPro

Wichtig vorab

SDModKit lädt keine Unity-AssetBundles

Im gesamten Quellcode gibt es keinen einzigen Aufruf von AssetBundle. Modelle werden aus OBJ-Dateien, einem einfachen JSON-Mesh-Format, aus Unity-Grundformen (Würfel, Zylinder …) oder aus Meshes vorhandener Spiel-Parts gebaut. Für reine SDModKit-Parts brauchst du also kein Unity – ein OBJ-Export aus Blender reicht.

Die wichtigsten Datentypen

Viele Fehler in Visual Studio entstehen durch falsche Typen. Die Kernfelder eines Parts sind:

FeldTypBeispielBedeutung
Idushort1200Numerische Part-ID (0–65535), muss eindeutig sein. Kein String.
Assetstring"PottedPlant"Interner Name des Parts (wird zum Namen der PartSettings). Kein GameObject.
Donorstring"SignalSplitter"Vanilla-Part, dessen Prefab geklont wird (Physik, Interaktion, Menü).

Normalerweise erzeugst du ContentPartDef nicht selbst im Code, sondern schreibst eine JSON-Datei. SDModKit liest sie ein und baut alles automatisch.

Ordnerstruktur

Alle Dateien von SDModKit liegen im UserData-Ordner von MelonLoader im Spielverzeichnis. Der Basispfad im Code ist ConfigFile.Folder.

StellarDrive/
├─ Mods/
│  ├─ SDModKit.dll                  ← das Mod Kit selbst
│  └─ ExampleMod.dll                   ← deine C#-Mod (optional)
└─ UserData/
   └─ SDModKit/                     ← ConfigFile.Folder
      ├─ modkit.cfg                 ← Mod-Schalter (an/aus) aller Mods
      ├─ stellardrive.modkit.cfg    ← Einstellungen + Keybinds des Kits
      ├─ handshake-skew.cfg         ← Versionsabgleich (ModVersionSkew)
      ├─ <deinemod>.cfg             ← ConfigFile.For("deinemod")
      ├─ Content/                   ← ContentRegistry.UserFolder
      │  └─ MeinPack/
      │     ├─ pack.json            ← jede *.json wird geladen (rekursiv)
      │     ├─ models/plant.obj
      │     └─ icons/plant.png
      └─ Dumps/                     ← Diagnose-Dumps (Dump.Folder)

Zusätzlich schreibt SDModKit beim Speichern eine Datei sdmodkit-content.json neben den Spielstand der Welt. Darin stehen die Einstellungen deiner platzierten Parts (siehe Speicherung).

Existiert UserData/SDModKit/Content noch nicht, legt SDModKit den Ordner beim ersten Start an. Packs werden nur beim Spielstart eingelesen; im Mod-Menü gibt es zusätzlich „Reload user packs“.

Zwei Wege zur Mod

Content-Pack (JSON)C#-Mod mit SDModKit
Programmieren nötigNeinJa (C#, Visual Studio)
Neue Parts, Items, ForschungJaJa (Pack in die DLL eingebettet)
Eigene Logik pro PartNur über vorhandene Behaviours anderer ModsJa, über PartBehaviour
Einstellungen, Keybinds, MenüseitenNeinJa
AblageortUserData/SDModKit/Content/Mods/DeineMod.dll

Beide Wege lassen sich kombinieren: Die C#-Mod liefert ein Behaviour, die JSON-Datei verweist per "behaviour" darauf.

Erster Part ohne Code

In fünf Schritten zu einer Topfpflanze im Baumenü – ohne Unity und ohne Visual Studio.

  1. Modell in Blender exportierenDatei → Exportieren → Wavefront (.obj). UVs und Normalen aktivieren, Maßstab 1 m = 1 Einheit. Details unter OBJ aus Blender.
  2. Ordner anlegenStellarDrive/UserData/SDModKit/Content/Pflanzen/models/ und die Datei plant.obj dort ablegen.
  3. pack.json schreibenIm Ordner Pflanzen/ eine Datei pack.json mit folgendem Inhalt anlegen.
  4. Spiel startenIm MelonLoader-Log sollte Pflanzen-Pack 1.0.0 loaded: 0 items, 1 parts, 0 techs stehen.
  5. Welt ladenDer Part erscheint im Baumenü direkt hinter dem Signal-Splitter.
{
  "id": "example.plants",
  "name": "Pflanzen-Pack",
  "version": "1.0.0",
  "parts": [
    {
      "id": 1200,
      "asset": "PottedPlant",
      "label": "Topfpflanze",
      "description": "Ein bisschen Grün für dein Schiff.",
      "donor": "SignalSplitter",
      "buildMenuAfter": "SignalSplitter",
      "mass": 5,
      "visual": {
        "meshes": [
          { "resource": "models/plant.obj", "material": "PartMetal", "color": [0.35, 0.75, 0.4] }
        ]
      }
    }
  ]
}

Kein bounds angegeben? Dann misst SDModKit die Größe automatisch aus deinem Modell und passt auch die Interaktions-Hitbox an.

Erste C#-Mod

Eine minimale Mod, die sich beim Kit registriert, eine Einstellung und einen Keybind anlegt und ein eingebettetes Content-Pack lädt. Projekt-Setup siehe Projekt einrichten.

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-Einstiegspunkt
    public sealed class Plugin : MelonMod
    {
        public override void OnInitializeMelon()
        {
            // Behaviours IMMER vor dem Laden des Packs registrieren
            ContentRegistry.Behaviour("examplemod.plant", () => new PlantBehaviour());

            // pack.json als EmbeddedResource in der DLL
            ContentRegistry.LoadEmbedded(typeof(Plugin).Assembly, "pack.json");

            // Mod beim Kit anmelden (eigene .cfg-Datei unter UserData/SDModKit/)
            ModKit.Register(new ExampleKitMod(), ConfigFile.For("examplemod"));
        }
    }

    // 2) Die eigentliche 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, "Geschwindigkeit");
            _hello = Keybinds.Register(Config, "Keys", "Hello", Key.H, "Hallo sagen");
            AddPage("Einstellungen", DrawPage);
        }

        public override void OnWorldEnter() => Log.Info("Welt geladen");

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

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

PlantBehaviour ist im Abschnitt Part-Behaviours vollständig ausgeschrieben.

Aufbau eines Packs

Ein Content-Pack ist eine JSON-Datei. Sie wird von ContentPack.Parse mit Newtonsoft.Json gelesen. Auf oberster Ebene gibt es nur sieben Felder:

FeldTypStandardBeschreibung
idPflichtstring–Eindeutige Pack-ID, z. B. "example.plants". Ohne ID wird das Pack verworfen.
namestring= idAnzeigename im Log und im Mod-Menü.
versionstring"0.0.0"Wird als id@version im Spielstand vermerkt.
partsarray[]Neue Bauteile → parts
itemsarray[]Neue Items/Ressourcen → items
researcharray[]Neue Forschungsknoten → research
attacharray[]Sockets/Logik an Vanilla-Parts anhängen → attach

Gemeinsame Werteformate

FormatSchreibweiseHinweis
Vector3[x, y, z]Mindestens 3 Zahlen, sonst wird der Standard genutzt. Einheiten in Metern, lokal zum Part, Y = oben.
Vector2Int[x, y]Ganzzahlen, z. B. Position im Forschungsbaum.
Farbe[r, g, b] oder [r, g, b, a]Werte von 0 bis 1. Alpha ist sonst 1.
Kosten{ "itemId": menge }Schlüssel ist die numerische Item-ID als String, z. B. { "12": 4 }.
null"feld": nullWird wie „nicht angegeben“ behandelt.

Kommt dieselbe Pack-id zweimal vor, gewinnt das zuerst geladene Pack (Log: pack id … loaded twice).

parts – Bauteile

Jeder Eintrag in parts wird zu einer ContentPartDef. SDModKit klont dafür das Prefab des Donors (eines vorhandenen Spiel-Parts), ersetzt die Optik durch deine Meshes und trägt das Ergebnis als neue PartSettings ins Spiel ein.

FeldTypStandardBeschreibung
idPflichtushort–Eindeutige Part-ID (1–65535). 0 oder fehlend → Part wird übersprungen.
assetPflichtstring–Interner Name. Muss eindeutig sein, wird zum Objektnamen der PartSettings und dient als Schlüssel in research.parts und ContentRegistry.PartByAsset.
labelstring= assetAnzeigename im Spiel (fullLabel), auch Titel des Part-Menüs.
descriptionstring""Beschreibungstext im Baumenü.
donorstring"SignalSplitter"Asset-Name des Vanilla-Parts, der geklont wird. Bestimmt Physik, Interaktion und Menütyp.
statestring"SignalSplitterState"Zustandsklasse (internalStateClass). Bei einem anderen Donor auf dessen State-Klasse setzen (steht im Dump).
behaviourstringnullRegistrierter Behaviour-Schlüssel oder voller Klassenname einer PartBehaviour-Klasse.
iconstringnullPfad zu einem PNG/JPG relativ zum Pack. Ersetzt das Baumenü-Thumbnail.
buildMenuAfterstringnullAsset-Name oder Label eines Vanilla-Parts. Der neue Part wird in dessen Zeile direkt dahinter einsortiert (bei voller Zeile in eine freie).
buildTabstringnullEigener Baumenü-Tab, z. B. "Machines" oder "Logistics". Hat Vorrang vor buildMenuAfter.
buildRowint0Zeile innerhalb von buildTab.
massfloat10Masse, wird auf 0,1–2000 begrenzt.
menustring"auto"auto: Donor-Menü, nur umbenannt · config: Einstellungs-Panel neben dem Menü · panel: Panel ersetzt das Menü. Siehe Konfigurationsmenü.
snappingstringDonorName eines SnappingStyle-Werts (Groß/klein egal). Siehe Bounds & Snapping.
boundsobjectauto{ "center": [x,y,z], "size": [x,y,z] }. Fehlt es, wird die Größe aus dem Modell gemessen.
visualobject–clearDonor, fitInteraction, meshes → Visual & Meshes.
socketsarrayDonorEigene Anschlüsse. Ist das Feld vorhanden (auch leer), werden alle Donor-Sockets entfernt → Sockets.
costobject{}Baukosten { "itemId": menge }. Unbekannte Items werden mit Warnung verworfen.
configobjectnullStartwerte für die PartConfig jedes platzierten Exemplars.

Was beim Bauen passiert

ContentBuilder.Configure arbeitet die Definition in dieser Reihenfolge ab:

  1. Label, Beschreibung, State-Klasse und Masse auf die geklonten PartSettings schreiben.
  2. Snapping setzen (nur wenn angegeben) und Baukosten eintragen.
  3. Optik des Donors entfernen (wenn clearDonor = true) und deine Meshes unter einem Kindobjekt SDModKitMeshes einhängen.
  4. Sockets neu aufbauen (wenn sockets vorhanden).
  5. Bounds aus allen Renderern messen (wenn bounds fehlt) und ShipPartBounds setzen.
  6. Interaktions-Collider auf die Bounds anpassen (wenn fitInteraction = true).
  7. Hover-Umriss verknüpfen und Icon setzen.

Bounds & Snapping

Bounds bestimmen Platzbedarf, Kollisionsprüfung beim Bauen und die Hitbox. Gibst du weder center noch size an, misst SDModKit alle Renderer des Parts (ohne Sockets). Standard bei fehlendem Modell: Mitte [0, 0.25, 0], Größe [0.9, 0.5, 0.9], Mindestgröße 0,1 je Achse.

Snapping legt fest, wie der Part beim Platzieren einrastet. Der Wert ist ein Name aus dem Spiel-Enum SnappingStyle. Die verfügbaren Namen listet der Dump-Abschnitt snapping bzw. die „Snapping table“ im Mod-Menü auf.

Die ersten sechs Snapping-Werte (Enum 0–5) funktionieren nur bei Hüllen-Parts. Bei schwebenden Parts lassen sie die Bauvorschau abstürzen – SDModKit erzwingt dann PreciseOnFloor (Enum 6) und meldet das im Log.

Donor wählen

Der Donor bestimmt alles, was du nicht selbst überschreibst: Physik-Komponenten, Netzwerk-Zustand, Interaktion und Menü. Bekannte Asset-Namen aus dem Code:

Asset-NameTypischer Einsatz
SignalSplitterStandard-Donor. Kleiner, schwebender Logik-Part.
FluidSplitter, PowerSplitterKleine Verteiler; liefern Socket-Vorlagen für Fluid/Power.
FluidTank, Battery, MathGateWeitere Socket-Vorlagen-Lieferanten.
StorageChestPart mit Inventar und Storage-Menü – nötig für menu: "config"/"panel".

Den exakten Asset-Namen und die State-Klasse eines beliebigen Vanilla-Parts findest du, indem du im Spiel auf den Part zielst und den Dump „Dump aimed part“ (F9) auslöst. Die Ausgabe enthält settings <Name> id <ID> und state <Klasse>.

visual – Meshes

Das Objekt visual steuert die Optik eines Parts. Jedes Element in meshes wird zu einem Kindobjekt (MeshDef).

FeldTypStandardBeschreibung
clearDonorbooltrueEntfernt Renderer, Mesh-Filter und Kindobjekte der Donor-Optik.
fitInteractionbooltruePasst den Interaktions-Collider an die Bounds an und deaktiviert überlappende Collider.
meshesarray[]Liste von Mesh-Definitionen (siehe unten).

Mesh-Felder

Die Mesh-Quelle wird in dieser Reihenfolge gesucht: resource → donorPath → shape. Sonderfälle sind shape: "text" und shape: "screen".

FeldTypStandardBeschreibung
resourcestringnullPfad zu einer .obj-Datei oder einer JSON-Mesh-Datei, relativ zum Pack-Ordner (oder Name einer eingebetteten Ressource).
donorPathstringnullMesh aus einem Spiel-Prefab übernehmen: "Kind/Pfad" im Donor oder "AnderesPart|Kind/Pfad".
shapestringnullcube, sphere, cylinder, capsule, quad, plane (unbekannte Werte = cube) sowie text und screen.
materialstring"PartMetal"Name eines geladenen Spiel-Materials. Nicht gefunden → PartMetal.
colorFarbenullTönung, setzt _BaseColor/_Color auf einer Materialkopie.
positionVector3[0,0,0]Lokale Position.
rotationVector3[0,0,0]Euler-Winkel in Grad.
scaleVector3[1,1,1]Skalierung. Bei text/screen: x/y = Fläche.
textstringnullFester Text für shape: "text".
textColorFarbeweiß / cyanTextfarbe (Text: sonst color oder weiß, Screen: hellcyan).
fontstringSpiel-FontName eines TextMeshPro-Fonts. Ohne Angabe wird der häufigste Spiel-Font genutzt.
screenstringnullKanalname für dynamische Inhalte (macht das Mesh automatisch zum Screen).
columnsint16Zeichenbreite des Lauftexts (4–64).
speedfloat6Laufschrift-Geschwindigkeit in Zeichen pro Sekunde (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] }
  ]
}

Grundformen ohne eigene Textur bekommen automatisch die mitgelieferte Textur ink_albedo.png, damit sie nicht flach weiß aussehen.

sockets – Anschlüsse

Sockets sind die Steckplätze für Kabel und Leitungen. SDModKit kopiert dafür echte Socket-Objekte aus Vanilla-Parts als Vorlage.

FeldTypStandardBeschreibung
idbyte0Socket-Nummer. Müssen lückenlos 0 … n-1 laufen.
typestring"item"power, signal, fluid oder item (alles andere = item).
positionVector3[0,0,0]Lokale Position am Part.
facingVector3autoEuler-Rotation. Ohne Angabe wird der Socket waagerecht relativ zum Bounds-Mittelpunkt ausgerichtet.
relayedboolfalseAlle Sockets mit relayed: true werden untereinander als durchgeschleift verknüpft (wie bei einem Splitter).

Socket-Typen

JSONCableTypeBemerkung
signal0Signalkabel
fluid1Fluid-Leitung
power2Stromkabel
item3Neuer Kabeltyp „Item Line“ von SDModKit (ItemCables). Orange eingefärbt, nutzt die Fluid-Socket-Optik als Vorlage.
"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] }
]

Schon ein leeres "sockets": [] entfernt alle Sockets des Donors. Lass das Feld weg, wenn du die Donor-Sockets behalten willst.

items – Items

Neue Items (Ressourcen, Flüssigkeiten, Gase) werden in die Item-Liste des Spiels eingefügt (ContentItems) und können in Baukosten und Forschung verwendet werden.

FeldTypStandardBeschreibung
idPflichtuint–Eindeutige Item-ID. Kollisionen mit Vanilla- oder anderen Mod-Items werden übersprungen.
namestring"item <id>"Anzeigename.
iconstringnullPNG/JPG relativ zum Pack.
massfloat1Masse pro Einheit.
maxStackint20Stapelgröße, 1–200.
isFluidboolfalseItem ist eine Flüssigkeit.
isGasboolfalseItem ist ein Gas.
fluidColorFarbeschwarzFarbe in Tanks/Leitungen.
modelobject–{ "meshes": [ … ] } – gleiche Felder wie bei visual.

Ein Item ohne model wird unsichtbar gezeichnet und kann nicht fallen gelassen werden (Log: has no model, it will draw blank and cannot be dropped).

"items": [
  {
    "id": 9001,
    "name": "Kühlflüssigkeit",
    "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 – Forschung

Forschungsknoten werden beim Betreten der Welt in den Tech-Tree eingefügt (ContentResearch.Apply). Sie schalten Parts und Spielfunktionen frei.

FeldTypStandardBeschreibung
idPflichtushort–Eindeutige Tech-ID.
namestring"Tech <id>"Name im Forschungsbaum.
position[x, y][0, 0]Zelle im Tech-Tree. Pflicht, außer bei hidden. Belegte Zellen werden gemeldet.
requiresushort[][]IDs der Voraussetzungen.
partsstring[][]Asset-Namen der Parts, die freigeschaltet werden.
featuresstring[][]Namen aus dem Spiel-Enum ResearchUnlockableFeature. Unbekannte Namen werden ignoriert.
costobject{}Forschungskosten { "itemId": menge }, auch eigene Items.
hiddenboolfalseKnoten wird nicht im Baum angezeigt.
grantedByushort0Wird automatisch erforscht, sobald diese andere Tech erforscht ist (Host prüft jede Sekunde).
"research": [
  {
    "id": 4100,
    "name": "Hydrokultur",
    "position": [6, 2],
    "requires": [12],
    "parts": ["PottedPlant"],
    "cost": { "9001": 20 }
  }
]

Mit "hidden": true und "grantedBy": <VanillaTechId> kannst du deinen Part still an eine bestehende Forschung koppeln, ohne den Baum zu verändern.

attach – Vanilla erweitern

attach hängt einen zusätzlichen Socket-Typ (und optional ein Behaviour) an bestehende Spiel-Parts. SDModKit geht dabei alle Vanilla-Parts durch, die

  • keine Hüllen-Parts sind,
  • ein Inventar besitzen,
  • noch keinen Socket dieses Typs haben,
  • und nicht in exclude stehen.

Laut Log werden dabei je Part zwei Sockets ergänzt. Das Prefab wird vorher geklont, beim Verlassen der Welt wird alles zurückgesetzt.

FeldTypStandardBeschreibung
idstring"attach"Name für das Log.
socket.typestring"item"Socket-Typ wie bei sockets.
socket.facestring"front"front, back, left, right, top. Vorhandene Sockets haben Vorrang bei der Seitenwahl.
behaviourstringnullBehaviour für alle erweiterten Parts.
menustring"config"Menümodus für erweiterte Parts.
configobjectnullStartwerte der PartConfig.
excludestring[][]Asset-Namen oder Labels, die nicht verändert werden.
"attach": [
  {
    "id": "item-ports",
    "socket": { "type": "item", "face": "front" },
    "behaviour": "examplemod.itemport",
    "menu": "config",
    "config": { "mode": "push" },
    "exclude": ["StorageChest"]
  }
]

Vollständiges Beispiel

Ein Pack mit eigenem Item, einem Part mit Screen, Sockets, Kosten und Forschung.

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

  "items": [
    { "id": 9101, "name": "Schaltkreis", "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": "Zeigt Live-Daten auf einem 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 aus Blender

SDModKit hat einen eigenen OBJ-Parser (MeshData.FromObj). Er wird automatisch genutzt, wenn der Dateiname auf .obj endet.

Was der Parser kann

  • Liest v (Vertices), vt (UVs), vn (Normalen), f (Flächen) und usemtl.
  • Spiegelt die X-Achse und dreht die Dreiecks-Reihenfolge – das ist die übliche Umrechnung von Blender (rechtshändig) nach Unity (linkshändig). Du musst nichts von Hand drehen.
  • Vielecke werden als Fächer trianguliert. Nicht-konvexe N-Gons daher besser vorher in Blender triangulieren.
  • Fehlen Normalen an einer Fläche, werden alle Normalen neu berechnet.
  • Über 65.000 Vertices schaltet er automatisch auf 32-Bit-Indizes.
  • Jedes usemtl wird ein eigenes Submesh. .mtl-Dateien werden ignoriert.

Empfohlene Export-Einstellungen

Blender-OptionEinstellungWarum
Forward / Up-Z / Y (Standard)Passt zur Achsen-Umrechnung des Parsers.
Scale1.0, Maßstab vorher anwenden (Strg+A)1 Blender-Einheit = 1 m im Spiel.
UV CoordinatesanFür Texturen/Material-Mapping.
NormalsanSonst werden harte Kanten weich gerechnet.
Triangulated Meshan (empfohlen)Vermeidet Fehler bei nicht-konvexen Flächen.
Materialsmöglichst ein MaterialDer Renderer bekommt nur ein Material. Weitere Submeshes werden nicht sichtbar gerendert (aus Unity-Verhalten abgeleitet).
Ursprungunten mittigDer Part-Ursprung liegt auf dem Boden; Y = 0 ist die Auflagefläche.

Brauchst du mehrere Farben, teile das Modell in mehrere OBJ-Dateien auf und lege für jede einen eigenen Eintrag in meshes mit eigenem material/color an.

JSON-Mesh-Format

Jede Mesh-Ressource, die nicht auf .obj endet, wird als JSON-Mesh gelesen (MeshData.FromJson). Das Format ist ein direkter Abzug eines Unity-Meshes – nützlich, wenn du Meshes per Skript erzeugst.

{
  "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] ]
}
FeldInhalt
verticesPflichtFlache Liste x, y, z, … (Unity-Koordinaten, keine Achsen-Umrechnung).
normalsGleiche Länge wie vertices, sonst automatisch berechnet.
uvu, v je Vertex (Länge = Vertex-Anzahl × 2).
submeshesPflichtListe von Index-Listen (Dreiecke, Uhrzeigersinn = Unity-Vorderseite).
nameName des Meshes.

Texte & Screens

Fester Text

"shape": "text" erzeugt ein TextMeshPro-Objekt mit automatischer Schriftgröße. scale.x und scale.y sind die Breite und Höhe des Textfelds.

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

Dynamischer Screen

"shape": "screen" (oder einfach ein gesetztes "screen"-Feld) erzeugt ein Display-Panel mit Textfeld. Den Inhalt liefert deine Mod über einen Kanal. Der Inhalt wird alle 0,1 Sekunden abgefragt.

using SDModKit.Content;
using UnityEngine;

// einmalig, z. B. in OnRegistered()
Screens.Provide("example.status", screenObject => new ScreenFrame
{
    Text      = "Leistung: " + Mathf.RoundToInt(Time.time % 100) + " kW",
    Scroll    = true,                      // Lauftext mit "columns" Zeichen
    TextColor = new Color(0.4f, 1f, 0.6f), // optional
    Image     = null                       // Texture2D statt Text anzeigen
});
ScreenFrame-FeldTypWirkung
TextstringAngezeigter Text (Rich-Text von TextMeshPro möglich, außer bei Scroll).
ScrollboolLauftext: zeigt columns Zeichen und scrollt mit speed Zeichen/s in Monospace.
TextColorColor?Überschreibt die Textfarbe.
ImageTexture2DErsetzt den Text durch ein Bild auf dem Panel (_BaseMap).

Der Callback bekommt das Screen-GameObject übergeben. So kannst du über screenObject.transform herausfinden, zu welchem Schiff/Part es gehört. Fehler im Callback werden einmalig geloggt und der Screen bleibt leer.

Texturen, Icons & Materialien

Bilder werden über ContentAssets.Texture(pack, pfad) geladen, Pfade sind immer relativ zum Pack-Ordner (Schrägstrich / funktioniert auch unter Windows). Texturen und Meshes werden pro Pack zwischengespeichert.

VerwendungFeldWirkung
Baumenü-Icon eines Partsparts[].iconÜberschreibt das gerenderte Thumbnail (IconOverrides.SetPart).
Item-Iconitems[].iconIcon im Inventar.
Materialmeshes[].materialSucht ein bereits geladenes Material mit genau diesem Namen (Resources.FindObjectsOfTypeAll<Material>).

Der Standard PartMetal ist das normale Metall-Material der Spiel-Parts. Weitere Materialnamen kannst du über einen Dump der Donor-Prefabs herausfinden. Mit color wird eine eingefärbte Kopie des Materials angelegt – das Original bleibt unverändert.

Packs in der DLL

Für C#-Mods kannst du pack.json, Modelle und Icons als EmbeddedResource in deine DLL packen. Dann gibt es nur eine Datei im Mods-Ordner.

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

Wie Ressourcen gefunden werden

.NET benennt eingebettete Dateien als Namespace.ordner.datei.ext. SDModKit wandelt models/plant.obj in models.plant.obj um und sucht in dieser Reihenfolge:

  1. exakter Ressourcenname,
  2. ein Name, der auf .models.plant.obj endet,
  3. ein Name, der auf .plant.obj endet (nur der Dateiname).

Bei eingebetteten Packs ist pack.Folder leer – alle Pfade in der JSON-Datei werden dann ausschließlich in den Ressourcen der DLL gesucht, nicht auf der Festplatte. Gleiche Dateinamen in verschiedenen Ordnern vermeiden.

Projekt einrichten

Eine SDModKit-Mod ist eine normale MelonLoader-Mod (Klassenbibliothek für .NET Framework 4.7.2), die zusätzlich SDModKit.dll referenziert.

  1. Projekt anlegenVisual Studio → „Klassenbibliothek (.NET Framework)“ oder ein SDK-Projekt mit net472. Kein .NET 6/8.
  2. Referenzen setzenDie DLLs aus dem Spielordner referenzieren (siehe csproj unten), jeweils mit Private=false, damit sie nicht mitkopiert werden.
  3. Bauen & kopierenDie fertige DeineMod.dll nach StellarDrive/Mods/ kopieren – neben SDModKit.dll.
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net472</TargetFramework>
    <LangVersion>latest</LangVersion>
    <AssemblyName>ExampleMod</AssemblyName>
    <RootNamespace>ExampleMod</RootNamespace>
    <!-- Wird von SDModKit als "examplemod@1.0.0" im Spielstand vermerkt -->
    <Version>1.0.0</Version>
    <!-- Pfad zu deinem Spielordner anpassen -->
    <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: nach dem Build automatisch in den Mods-Ordner kopieren -->
  <Target Name="CopyToMods" AfterTargets="Build">
    <Copy SourceFiles="$(TargetPath)" DestinationFolder="$(GameDir)\Mods" />
  </Target>
</Project>

Je nach MelonLoader-Version liegen MelonLoader.dll und 0Harmony.dll direkt in MelonLoader/ oder in MelonLoader/net35/. Prüfe den Pfad in deinem Spielordner. Nur die DLLs referenzieren, die du wirklich brauchst.

Benötigte usings

NamespaceInhalt
SDModKitModKit, ModStamp, ModToggles
SDModKit.ApiSDMod, ModPage
SDModKit.ConfigurationConfigFile, ConfigEntry<T>
SDModKit.ContentPacks, Behaviours, PartConfig, Screens, Menüs
SDModKit.InputKeybinds, Keybind, MouseBind
SDModKit.UIModUi, Banner, GameModal, KeyTips, Theme
SDModKit.UtilLog, Toasts, Dump, EmbeddedAssets
SDModKit.GameSpiel-Helfer: Spieler, Items, Schiffe, Baumenü
SDModKit.PatchingModPatches (Harmony)

SDMod & ModKit

Jede Mod leitet von SDModKit.Api.SDMod ab und wird einmal mit ModKit.Register angemeldet – typischerweise in OnInitializeMelon deiner MelonMod.

SDMod – Mitglieder

MitgliedTypBeschreibung
IdabstractstringEindeutige ID, z. B. "examplemod". Schlüssel in modkit.cfg und im Spielstand.
NameabstractstringAnzeigename im Mod-Menü.
Essentialbooltrue = kann im Mod-Menü nicht abgeschaltet werden.
Devbooltrue aktiviert die Entwickler-Tasten des Kits (Einfg Menü, F9 Dump).
HostOnlyboolVorhanden, wird aber in 1.3.3 nirgends ausgewertet.
ConfigConfigFileDie bei Register übergebene Datei.
LogLogLogger mit dem Mod-Namen als Präfix.
PagesIReadOnlyList<ModPage>Eigene Seiten im Mod-Menü.
OnRegistered()virtualDirekt nach Register. Hier Config-Einträge, Keybinds und Seiten anlegen.
OnWorldEnter()virtualWenn eine Welt geladen ist (Host und Client).
OnWorldExit()virtualBeim Verlassen der Welt.
OnUpdate()virtualJeder Frame (auch im Hauptmenü).
AddPage(title, draw)protectedSeite hinzufügen. Überladung AddPage(group, title, draw) für Untergruppen.

ModKit – statische API

MitgliedBeschreibung
Register(mod, config)Meldet die Mod an. Doppelte IDs werden gewarnt. Ist die Mod in modkit.cfg deaktiviert (und nicht Essential), landet sie in Disabled und bekommt keine Aufrufe.
Mods / DisabledAktive bzw. abgeschaltete Mods.
IsMuted(mod)Nach 5 Ausnahmen in Callbacks wird eine Mod stummgeschaltet – bis zum nächsten Weltwechsel.
Unmute()Alle stummgeschalteten Mods wieder aktivieren.
SaveConfigs()Alle geänderten Config-Dateien sofort schreiben.
OpenMenu()Öffnet das Mod-Menü per Code.
DevToolsPresenttrue, wenn eine registrierte Mod Dev => true hat.
Menu, LogMenü-Host und Kit-Logger.

Mods an- und abschalten passiert über UserData/SDModKit/modkit.cfg, Abschnitt [Mods], z. B. examplemod = false. Änderungen wirken erst nach einem Neustart.

Einstellungen (ConfigFile)

ConfigFile.For("name") öffnet UserData/SDModKit/name.cfg (pro Name nur einmal). Das Format ist ein einfaches INI:

[General]
# Geschwindigkeit
Speed = 1.5

[Keys]
Hello = H
ConfigEntry<float> speed   = Config.Bind("General", "Speed", 1.5f, "Geschwindigkeit");
ConfigEntry<bool>  enabled = Config.Bind("General", "Enabled", true, "Funktion an/aus");
ConfigEntry<Mode>  mode    = Config.Bind("General", "Mode", Mode.Auto, "Betriebsart"); // Enums gehen auch

float v = speed.Value;   // lesen
speed.Value = 2f;        // schreiben → wird automatisch gespeichert
float d = speed.DefaultValue;
  • Unterstützt alle Typen, die sich per Convert in Text umwandeln lassen (int, float, bool, string …) sowie Enums. Zahlen werden kulturunabhängig (Punkt als Dezimaltrenner) geschrieben.
  • Kommentare beginnen mit # oder ;.
  • Geänderte Werte werden 2 Sekunden nach der letzten Änderung, beim Weltwechsel und beim Beenden gespeichert. Sofort: file.Save() oder ConfigFile.SaveAllDirty().

Keybinds

Keybinds werden in einer Config-Datei gespeichert und erscheinen automatisch auf der Keybinds-Seite des Mod-Menüs, wo Spieler sie neu belegen können.

using SDModKit.Input;
using UnityEngine.InputSystem;

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

public override void OnUpdate()
{
    if (Keybinds.Typing) return;          // Spieler tippt gerade in ein Textfeld
    if (toggle.Pressed)  DoToggle();      // in diesem Frame gedrückt
    if (grab.Held)       DragObject();    // gehalten
    if (grab.Released)   Drop();          // losgelassen
}
MitgliedBeschreibung
Pressed / Held / ReleasedZustand im aktuellen Frame.
DisplayAnzeigetext der Belegung, z. B. „G“.
Key / MouseAktuelle Taste bzw. Maustaste.
SetKey(key), SetMouse(btn), Clear(), Reset()Belegung ändern, löschen, auf Standard zurücksetzen.
Keybinds.All, Keybinds.For(file), Keybinds.SaveAll()Alle Binds, Binds einer Datei, alles speichern.

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

Part-Behaviours

Ein PartBehaviour ist die Logik hinter einem Content-Part. Es gibt eine Instanz pro Part-Typ, nicht pro platziertem Exemplar – die Daten eines Exemplars stehen in part.Config.

Registrieren

// VOR dem Laden des Packs – die Verknüpfung passiert beim Laden
ContentRegistry.Behaviour("examplemod.plant", () => new PlantBehaviour());
ContentRegistry.LoadEmbedded(typeof(Plugin).Assembly, "pack.json");

Im Pack steht dann "behaviour": "examplemod.plant". Alternativ kannst du den vollen Klassennamen angeben ("ExampleMod.PlantBehaviour"); SDModKit sucht ihn in der Pack-Assembly, per Type.GetType und in allen geladenen Assemblys (Groß/klein egal). Die Klasse braucht dann einen parameterlosen Konstruktor.

Callbacks

MethodeWann / wo
OnRegistered()Einmal nach dem Verknüpfen mit der Definition. Def ist gesetzt.
OnWorldEnter() / OnWorldExit()Beim Betreten/Verlassen einer Welt.
OnPartAdded(part)Host: ein Exemplar ist auf einem Schiff neu aufgetaucht.
OnPartRemoved(shipId, partId)Host: ein Exemplar ist verschwunden. Die Config wird danach gelöscht.
ServerTick(part, dt)Nur Host, jeden Schiffs-Update für jedes Exemplar. Nach 5 Fehlern stummgeschaltet.
DescribeConfig(part, rows)Host und Client: Zeilen für das Part-Menü beschreiben (siehe Konfigurationsmenü).
ConfigTitle(part)Titel des Menüs (Standard: Label).
OnAction(part, player, action)Host: Aktion eines Spielers, ausgelöst über ContentActions.Send.
PanelScaleGrößenfaktor des Panels im Modus panel (mind. 0,5).
DrawConfig(part), Status(part)Vorhanden, werden in 1.3.3 aber nicht aufgerufen.
Trace / Warn / ErrorLog-Hilfen mit dem Scope des Behaviours.

ContentPart

Alle Callbacks bekommen ein ContentPart (struct) mit: Ship (IShipStateWrite, auf Clients null), ShipId, PartId, Part (StatefulPart), Def, Config, Valid, Label und LocalPosition.

Vollständiges Beispiel

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

namespace ExampleMod
{
    public sealed class PlantBehaviour : PartBehaviour
    {
        // Host: Wasser sinkt langsam
        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) => "Topfpflanze";

        // Host + Client: Menüzeilen
        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 = "Pflanze" });
            rows.Add(new ConfigRow
            {
                Label      = "Wasser",
                Value      = Mathf.RoundToInt(water * 100f) + " %",
                ValueColor = water < 0.2f ? Theme.Bad : Theme.Good
            });
            rows.Add(new ConfigRow
            {
                Label    = "Modus",
                Value    = mode == 0 ? "Normal" : "Sparsam",
                Choices  = new[] { "Normal", "Sparsam" },
                OnChoose = i => part.Config.Set("mode", i)      // Menü aktualisiert sich danach selbst
            });
            rows.Add(new ConfigRow
            {
                Label    = "Gießen",
                Choices  = new[] { "Jetzt gießen" },
                OnChoose = _ => ContentActions.Send(part, "water") // läuft auf dem Host
            });
        }

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

Für menu: "config" oder "panel" muss der Donor beim Interagieren ein Storage-Menü öffnen (z. B. StorageChest). Der Standard-Donor SignalSplitter öffnet keins. Die State-Klasse oben ist ein Beispiel – den echten Namen liefert der Dump.

Weitere Laufzeit-Helfer

MitgliedBeschreibung
ContentRuntime.PartsOn(ship)Alle Content-Parts auf einem Schiff.
ContentRuntime.TryResolve(ship, partId, out part)Einen einzelnen Content-Part auflösen.
ContentRuntime.Of(partId)Behaviour zu einer Part-ID.
ContentRuntime.ShipServerTickEvent Action<IShipStateWrite, float> – einmal pro Schiffs-Update auf dem Host.
ContentRuntime.All, TrackedShipsAlle Behaviours, Anzahl beobachteter Schiffe.

Konfigurationsmenü

Wenn ein Spieler mit einem Content-Part interagiert, fängt SDModKit das Öffnen des Storage-Menüs ab (StorageMenuActivator.ShowMenu) und baut dein Menü aus den Zeilen von DescribeConfig.

menuErgebnis
autoDas Donor-Menü bleibt, bekommt nur deinen Titel.
configEinfaches Einstellungs-Panel neben dem Donor-Menü (Abschnitt, Label, Wert, Auswahl, Icons).
panelEigenes Panel ersetzt das Menü komplett – mit Tabs, Banner, Notizen, Icons, Detailtexten und Farben.

ConfigRow – Felder

FeldTypWirkung
SectionstringZwischenüberschrift (Zeile nur mit Section).
LabelstringBeschriftung links.
ValuestringWert rechts.
ValueColor / LabelColorColor?Farben, z. B. Theme.Good, Theme.Warn, Theme.Bad.
Choicesstring[]Auswahl-Buttons.
ChoiceEnabledbool[]Einzelne Buttons deaktivieren (panel).
OnChooseAction<int>Wird mit dem Index aufgerufen; danach baut sich das Menü neu auf.
HighlightboolZeile hervorheben.
IconsList<ConfigIcon>Icon-Leiste: Image, Name (Tooltip), State (Farbindex), OnClick.
Icon / ValueIconTextureKleines Bild vor Label bzw. Wert (panel).
Detail / DetailColorstring / Color?Zweite, kleinere Textzeile (panel).
BannerstringHinweisbalken oben (panel).
NotestringHinweistext (panel).
Tabs, ActiveTab, TabColors, Asidestring[], int, Color[], stringTab-Leiste; OnChoose bekommt den Tab-Index (panel).

Verhalten im Multiplayer

  • Auf dem Client zeigt das Menü zuerst „Waiting for the host's settings“, bis die Config vom Host da ist.
  • Ändert der Client per OnChoose die Config, wird die ganze Config an den Host geschickt.
  • Liefert DescribeConfig keine Zeilen, steht „Nothing to configure“ im Menü.
  • Von außen neu zeichnen: ContentConfigMenu.Refresh(). Änderungen abonnieren: ContentConfigMenu.Changed.

Aktionen

ContentActions.Send(part, "name") schickt eine Aktion an den Host, der dann OnAction aufruft. Der Host führt sie nur aus, wenn der Spieler gerade mit genau diesem Part interagiert – ideal für Buttons wie „Start“, „Leeren“ oder „Gießen“.

Daten speichern

PartConfig – pro platziertem Part

Jedes platzierte Exemplar hat eine eigene PartConfig (intern ein JObject). Startwerte kommen aus "config" im Pack. Sie wird im Spielstand gespeichert und an Clients synchronisiert.

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");   // z. B. Item-IDs

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

Direkter Zugriff über PartData: PartData.For(shipId, partId, def), TryGet, Forget, Count.

WorldData – pro Welt

Einfache Schlüssel/Wert-Paare (Strings), die mit der Welt gespeichert werden:

WorldData.Set("examplemod.lastVisit", System.DateTime.Now.ToString("o"));
string last = WorldData.Get("examplemod.lastVisit");   // null, wenn nicht gesetzt

Schlüssel mit deiner Mod-ID beginnen lassen, damit sie sich nicht mit anderen Mods überschneiden.

Seiten im Mod-Menü

Das Mod-Menü ist IMGUI-basiert. Deine Draw-Funktion wird jeden Frame aufgerufen, solange die Seite offen ist. ModUi liefert passende Bausteine im Kit-Stil.

private string _filter = "";

public override void OnRegistered()
{
    AddPage("Einstellungen", DrawSettings);
    ModPage dev = AddPage("Debug", "Werkzeuge", DrawTools);
    dev.Available = () => SDModKit.Game.GameRefs.InWorld;   // nur in einer Welt anzeigen
}

private void DrawSettings()
{
    ModUi.Section("Allgemein", () =>
    {
        ModUi.Row("Aktiv",  () => _enabled.Value = ModUi.Toggle(_enabled.Value, "an"));
        ModUi.Row("Tempo",  () => _speed.Value   = ModUi.Slider(_speed.Value, 0f, 5f));
        ModUi.Row("Anzahl", () => _count.Value   = ModUi.Int(_count.Value, 1, 64));
        ModUi.Note("Änderungen werden automatisch gespeichert.");
    });

    ModUi.Fold("Erweitert", () =>
    {
        _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("Dump schreiben")) SDModKit.Util.Dump.Everything();
}
ModUiBeschreibung
Section(title, body), BeginSection/EndSectionKasten mit Überschrift.
Fold(title, body)Aufklappbarer Bereich.
Row(label, controls), BeginRow/EndRowZeile mit Label links.
Label, Note, Space, SeparatorText, Hinweis, Abstand, Trennlinie.
Hosting()Zeile „Hosting“: zeigt, ob die Mod hier als Host läuft oder der Host sie ebenfalls braucht.
Button(text, width)true beim Klick.
Toggle(value, text)Checkbox.
Text(value, width)Textfeld.
Int(value, min, max, width)Zahlen-Textfeld, Ergebnis auf min–max begrenzt.
Slider(value, min, max, width)Schieberegler.
Search(value, placeholder), Matches(text, filter)Suchfeld und passender Filter.

Harmony-Patches

SDModKit nutzt HarmonyDontPatchAll und patcht gezielt. Für eigene Patches gibt es ModPatches, das fehlgeschlagene Patches sammelt statt die Mod abstürzen zu lassen.

using HarmonyLib;
using SDModKit.Patching;

// alle [HarmonyPatch]-Klassen der eigenen Assembly anwenden
bool ok = ModPatches.ApplyAll("examplemod", typeof(Plugin).Assembly, Log);

// oder eine einzelne Patch-Klasse
ModPatches.Apply("examplemod", typeof(MyPatch), Log);

// alle Patches dieser ID entfernen
ModPatches.Remove("examplemod");

ModPatches.For(id) liefert die Harmony-Instanz, Handler(owner, method) eine HarmonyMethod, BrokenHandlers die Liste fehlgeschlagener Patches (im Mod-Menü unter „Patches that did not bind“).

Wenn deine Mod als MelonMod geladen wird, patcht MelonLoader ihre [HarmonyPatch]-Klassen standardmäßig automatisch. Willst du das über ModPatches steuern, setze in deiner Assembly ebenfalls [assembly: HarmonyDontPatchAll].

Logging

Log.Info("geladen");                 // SDMod.Log – Präfix = Mod-Name
Log.Trace("nur bei VerboseLog");     // erscheint nur, wenn Log.Verbose = true
Log.Warn("Achtung");
Log.Error("kaputt", ex);

// Code absichern: Ausnahme wird geloggt, Rückgabe false
Log.Guard("Inventar lesen", () => ReadInventory());

var other = SDModKit.Util.Log.For("ExampleMod.Netz");   // eigener Logger

Log.Verbose wird über die Kit-Einstellung Diagnostics.VerboseLog geschaltet. Content-Meldungen (Packs, Behaviours) laufen über ContentLog: die letzten 400 Einträge stehen im Mod-Menü, ab Warn zusätzlich in der MelonLoader-Konsole.

UI-Helfer im Spiel

APIBeschreibung
Toasts.Show(text)Kurze Einblendung (Namespace SDModKit.Util).
Banner.Show(text, color, seconds)Großer Banner-Text im Spielstil.
GameModal.Open(title, subtitle, choices)Dialog im Spieldesign mit ModalChoice { Label, Bad, OnPick }. Danach Section, Line(text, warn), Note für Inhalt; Close(), IsOpen.
KeyTips.Mode(text), KeyTips.Add(bind, label), KeyTips.HideVanilla(tip)Tastenhinweise unten rechts. Jeden Frame neu aufrufen, solange sie sichtbar sein sollen.
ModalInput.Capture() / Release() / Hold()Spielsteuerung sperren, solange eigenes UI offen ist.
ThemeKit-Farben: Accent, Text, TextDim, Good, Warn, Bad, Panel …
GameStyleFarben und Font des Spiels (Font, Text, Background, Bad …), sobald Ready.
EmbeddedAssets.Texture(asm, name)PNG aus eingebetteter Ressource laden; auch Bytes, Text.
GameModal.Open("Schiff zurücksetzen?", "Alle Einstellungen gehen verloren.", new[]
{
    new ModalChoice { Label = "Abbrechen" },
    new ModalChoice { Label = "Zurücksetzen", Bad = true, OnPick = () => ResetAll() }
});

Spiel-Helfer

Der Namespace SDModKit.Game kapselt häufige Zugriffe auf das Spiel, damit du nicht selbst in Assembly-CSharp suchen musst.

KlasseWichtige MitgliederZweck
GameRefsIsServer, IsClient, InWorld, PlayersServer, PlayerCamera, Service<T>()Grundzustand und Spiel-Dienste.
LocalPlayerStateType, IsPiloting, IsOnFoot, GameMenuOpen, TryEquipLokaler Spieler.
PlayerAccessLocalServerPlayer, WorldPosition, TryGive(itemId, qty, out err), TryClearInventory, CurrentCountsInventar und Position.
ItemCatalogAll, ById, NameOf, DisplayName, Icon, ThumbnailAlle Items (inkl. Mod-Items).
PartLookupByAssetName, ByIdPartSettings finden.
ItemMoveAccepts, Count, Total, Contents, Transfer, Move, MaxStack, IsMachineItems zwischen Inventaren bewegen (Host).
ServerTradeHas, TryTrade, Split, HeldItemHandel/Item in der Hand.
WorldQueryScanMinables, ScanDropsAbbaubare Objekte und Drops in der Nähe.
ShipAimTryGetAimedPart, TryPart, ShipTransformPart im Fadenkreuz.
LaserTuningSetRange(owner, factor), SetSpeed(owner, factor)Abbaulaser anpassen.
BuildTabsDeclare(tab, order, icon), Add, AddAll, HideEigene Baumenü-Tabs („Machines“ = 10, „Logistics“ = 20).
CustomPartsRegister(CustomPartDefinition), Get, All, PartsReadyParts per Code statt JSON (Id, Name, Donor, BuildMenuAfter, BuildTab, BuildRow, Configure, AutoFinish).
IconOverridesSetPart, SetItemIcons ersetzen.
BuildCostTotal, Shortfall, TryCharge, RefundBaukosten berechnen und abbuchen.
GhostSet, BuildPreview, PartGeometry, GameRay, ToolRefs–Bauvorschau, Geometrie, Raycasts, Werkzeuge.
ItemCablesCableType 3Der Kabeltyp „Item Line“.
foreach (var it in SDModKit.Game.ItemCatalog.All)
    Log.Info(it.id + " = " + SDModKit.Game.ItemCatalog.DisplayName(it));

Items per Taste geben

Ein kleines Test-Werkzeug: Ein Druck auf F6 gibt dem Host-Spieler 10 Einheiten eines Items. Nützlich, um Baukosten und Rezepte schnell auszuprobieren.

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, die vergeben wird");
        _amount = Config.Bind("Give", "Amount", 10, "Menge pro Tastendruck");
        _give   = Keybinds.Register(Config, "Keys", "Give", Key.F6, "Item geben");
    }

    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("Fehlgeschlagen: " + error);   // z. B. "not hosting"
    }
}
  1. RegistrierenIn OnInitializeMelon: ModKit.Register(new ItemGiverMod(), ConfigFile.For("example.itemgiver"));
  2. Welt als Host startenTryGive funktioniert nur auf dem Host (sonst Meldung not hosting).
  3. TestenF6 drücken – das Item landet im Inventar. Item-ID und Menge in UserData/SDModKit/example.itemgiver.cfg ändern.

Zähler-Part mit Menü

Ein Part, der beim Interagieren ein Menü mit Zählerstand und zwei Buttons zeigt. Zeigt das Zusammenspiel von DescribeConfig, ContentActions.Send und OnAction – funktioniert auch im Multiplayer.

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

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

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

        rows.Add(new ConfigRow { Section = "Zählerstand" });
        rows.Add(new ConfigRow
        {
            Label      = "Wert",
            Value      = count.ToString(),
            ValueColor = count > 0 ? Theme.Good : Theme.TextDim
        });
        rows.Add(new ConfigRow
        {
            Label    = "Aktion",
            Choices  = new[] { "+1", "Zurücksetzen" },
            OnChoose = i => ContentActions.Send(part, i == 0 ? "inc" : "reset")
        });
    }

    // läuft nur auf dem Host
    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": "Zähler",
      "donor": "StorageChest",
      "state": "StorageChestState",
      "behaviour": "example.counter",
      "menu": "config",
      "buildMenuAfter": "StorageChest",
      "config": { "count": 0 },
      "visual": { "meshes": [ { "shape": "cube", "position": [0, 0.25, 0], "scale": [0.5, 0.5, 0.5], "color": [0.9, 0.6, 0.2] } ] }
    }
  ]
}
ContentRegistry.Behaviour("example.counter", () => new CounterBehaviour());
ContentRegistry.LoadEmbedded(typeof(Plugin).Assembly, "pack.json");
  1. State-Klasse prüfenStorageChestState ist ein Platzhalter. Den echten Namen liefert der Dump (F9 auf eine Kiste zielen).
  2. BauenDen orangen Würfel aus dem Baumenü platzieren.
  3. TestenInteragieren → Menü „Zähler“ erscheint. „+1“ erhöht den Wert, „Zurücksetzen“ setzt ihn auf 0. Nach Speichern und Neuladen bleibt der Wert erhalten.

Live-Display

Ein Part mit Bildschirm, der laufend die Spielzeit und die Rolle (Host/Client) anzeigt. Zeigt shape: "screen" und Screens.Provide.

{
  "id": 1301,
  "asset": "ExampleDisplay",
  "label": "Info-Display",
  "buildMenuAfter": "SignalSplitter",
  "visual": {
    "meshes": [
      { "shape": "cube",   "position": [0, 0.2, 0],      "scale": [0.6, 0.4, 0.1], "color": [0.15, 0.17, 0.2] },
      { "shape": "screen", "screen": "example.clock",    "position": [0, 0.2, -0.051], "scale": [0.52, 0.32, 1] }
    ]
  }
}
Screens.Provide("example.clock", screen =>
{
    int t = (int)UnityEngine.Time.time;
    string role = SDModKit.Game.GameRefs.IsServer ? "HOST" : "CLIENT";
    return new ScreenFrame
    {
        Text = role + "\n" + (t / 60).ToString("00") + ":" + (t % 60).ToString("00")
    };
});
  1. BauenDen Part platzieren – der Bildschirm zeigt sofort Rolle und Zeit.
  2. Lauftext testenScroll = true im ScreenFrame setzen und im JSON "columns": 12 ergänzen.

Einstellungsseite mit Reset

Eine Seite im Mod-Menü mit Schalter, Regler und einem „Zurücksetzen“-Button, der vorher einen Bestätigungsdialog im Spieldesign zeigt.

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, "Funktion an/aus");
        _strength = Config.Bind("General", "Strength", 0.5f, "Stärke 0–1");
        AddPage("Einstellungen", Draw);
    }

    private void Draw()
    {
        ModUi.Section("Allgemein", () =>
        {
            ModUi.Row("Aktiv",  () => _enabled.Value  = ModUi.Toggle(_enabled.Value, "an"));
            ModUi.Row("Stärke", () => _strength.Value = ModUi.Slider(_strength.Value, 0f, 1f));
            ModUi.Note("Aktueller Wert: " + _strength.Value.ToString("0.00"));
        });

        if (ModUi.Button("Auf Standard zurücksetzen"))
        {
            GameModal.Open("Zurücksetzen?", "Alle Werte dieser Mod gehen verloren.", new[]
            {
                new ModalChoice { Label = "Abbrechen" },
                new ModalChoice { Label = "Zurücksetzen", Bad = true, OnPick = () =>
                {
                    _enabled.Value  = _enabled.DefaultValue;
                    _strength.Value = _strength.DefaultValue;
                    Toasts.Show("Einstellungen zurückgesetzt");
                }}
            });
        }
    }
}
  1. Mod-Menü öffnenMit Dev-Mod über Einfg, sonst per ModKit.OpenMenu() (siehe Das Mod-Menü).
  2. TestenWerte ändern, Spiel neu starten – die Werte stehen in UserData/SDModKit/example.settings.cfg und bleiben erhalten.

Lebenszyklus

Die Reihenfolge, in der SDModKit und deine Mod aufgerufen werden:

SpielstartSDModKit lädt zuerst

MelonPriority(-1000). Kit-Config, Menü-Host, Content-Hooks werden installiert, dann alle JSON-Packs aus UserData/SDModKit/Content gelesen.

SpielstartDeine OnInitializeMelon

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

Jeder FrameSDMod.OnUpdate()

Auch im Hauptmenü. Stummgeschaltete Mods werden übersprungen.

Welt geladenContent wird gebaut

Items eingefügt, Parts geklont und konfiguriert, attach angewendet, Forschung eingefügt, Spielstand-Daten (sdmodkit-content.json) geladen. Danach SDMod.OnWorldEnter() und PartBehaviour.OnWorldEnter().

In der Welt (Host)Schiffs-Updates

OnPartAdded / OnPartRemoved, dann ServerTick je Exemplar und das Event ShipServerTick.

InteraktionMenü

ConfigTitle + DescribeConfig; Klicks → OnChoose bzw. ContentActions.Send → OnAction auf dem Host.

SpeichernSpielstand

Nach GameSaver.SaveGame schreibt das Kit sdmodkit-content.json neben den Spielstand.

Welt verlassenAufräumen

OnWorldExit(), Configs speichern, attach-Änderungen und Forschung zurücksetzen, Stummschaltungen aufheben.

Multiplayer

StellarDrive nutzt FishNet. SDModKit trennt sauber zwischen Host und Client:

WasHostClient
ServerTick, OnPartAdded, OnPartRemoved, ShipServerTickjanein
OnActionja (vom Client ausgelöst)nein
DescribeConfigjaja, mit synchronisierter Config
ContentPart.Shipgesetztnull
PartConfig änderndirektper OnChoose → ganze Config an Host
SDMod.OnUpdate, Keybinds, UIjaja

Alle Spieler brauchen dieselben Mods und Packs. Content-Parts, Item-IDs und Tech-IDs müssen auf allen Rechnern identisch sein, sonst passen die Schiffsdaten nicht zusammen. ModStamp erzeugt dafür Stempel im Format id@version (Version = Assembly-Version mit 3 Stellen).

Nützlich für Prüfungen: GameRefs.IsServer, GameRefs.IsClient, ModUi.Hosting().

Speicherung

Beim Speichern der Welt schreibt ContentSave die Datei sdmodkit-content.json in den Ordner des Spielstands. Beim Laden (GameLoader.LoadState) wird sie wieder eingelesen.

{
  "format": 1,
  "game": "0.x.y",
  "mods":  ["modkit@1.3.3", "examplemod@1.0.0"],
  "packs": ["example.plants@1.0.0"],
  "parts": { "…": "PartConfig je Schiff und Part" },
  "world": { "examplemod.lastVisit": "2026-10-13T12:00:00" }
}
DatenWo gespeichert
PartConfigsdmodkit-content.json → parts
WorldDatasdmodkit-content.json → world
Mod-Einstellungen, KeybindsUserData/SDModKit/<name>.cfg (global, nicht pro Welt)
Mod an/ausUserData/SDModKit/modkit.cfg

Configs von Parts, die nicht mehr existieren, werden beim Speichern aussortiert (PartData.Prune). Den Status von Lesen/Schreiben siehst du in ContentSave.LastWritten, LastRead und LastError.

API-Index

Alle öffentlichen Klassen von SDModKit 1.3.3 mit ihren öffentlichen Mitgliedern – automatisch aus dem Quellcode erzeugt. Tippe in das Feld, um zu filtern.

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

Konstanten & Standardwerte

WertInhalt
Plugin.Guid"stellardrive.modkit"
ModToggles.FileName / Section"modkit" / "Mods"
ModVersionSkew.FileName / Section"handshake-skew" / "Versions"
ContentSave.FileName / Format"sdmodkit-content.json" / 1
ContentLog.Capacity400 Einträge
ContentLog.ConsoleFloorWarn (ab hier in die Konsole)
CustomParts.RowCapacity8 Parts pro Baumenü-Zeile
PartFinish.InteractionOutset / SocketStandoff0.02 / 0.06 m
Dump.MaxDepth / MaxChildren8 / 64
Fehlergrenze Mods / Behaviours5 Ausnahmen → stumm bis Weltwechsel
Config-Autospeichern2 s nach letzter Änderung
Kit-Tasten (nur mit Dev-Mod)Einfg Menü · F9 Dump des anvisierten Parts
Theme.Accent#4FC3E8

Das Mod-Menü

SDModKit bringt ein eigenes Ingame-Menü mit. Jede Mod bekommt dort ihre Seiten, dazu kommen die Kit-Seiten Keybinds, Content und Diagnostics.

Die Taste Einfg funktioniert nicht?

Die Kit-Tasten (Einfg für das Menü, F9 für den Part-Dump) werden nur registriert, wenn mindestens eine Mod Dev => true meldet. Ohne Dev-Mod öffnest du das Menü per Code mit ModKit.OpenMenu(), z. B. über einen eigenen Keybind.

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

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

Seite Content

Übersicht über alle geladenen Packs, Parts und Items mit ihren Problemen sowie die letzten Meldungen aus ContentLog (Button „Clear“ leert das Log).

ButtonWirkung
Reload user packsPacks aus UserData/SDModKit/Content neu einlesen.
Rebuild world contentContent-Parts der aktuellen Welt neu aufbauen.
Unmute behavioursStummgeschaltete Behaviours reaktivieren.

Seite Diagnostics

Zeigt den Part im Fadenkreuz („Aiming at“), die Liste Patches that did not bind (fehlgeschlagene Harmony-Patches, z. B. nach Spiel-Updates) und den Bereich Write a dump mit einzelnen Dump-Buttons: Everything, Aimed part, Menu prefabs, Snapping table, Cables, Content parts und Vanilla part.

Kit-Einstellungen in stellardrive.modkit.cfg: Diagnostics.VerboseLog (Trace-Meldungen) und Diagnostics.DumpOnWorldEnter (Dump bei jedem Weltstart).

Dumps

Dumps sind Textdateien unter UserData/SDModKit/Dumps/. Sie sind das wichtigste Werkzeug, um Asset-Namen, State-Klassen, Kind-Pfade (für donorPath) und Materialien herauszufinden.

AbschnittInhalt
tags, layersUnity-Tags und Layer.
snappingAlle Snapping-Detektoren/Stile.
cablesKabel-Konfigurationen je Typ.
menusMenü-Prefabs.
content partsDeine geklonten Parts mit Bounds, Sockets, Renderern.
donor partsVanilla-Parts mit Objektbaum (bis Tiefe 8, je 64 Kinder).
aimed partDer Part im Fadenkreuz: Settings-Name, ID, State-Klasse, Objektbaum.
string path = SDModKit.Util.Dump.Everything();   // alles
SDModKit.Util.Dump.Write("meinDump", sb => sb.AppendLine("eigener Inhalt"));
// letzter Pfad: SDModKit.Util.Dump.LastPath

Log-Meldungen

Die Meldungen erscheinen im MelonLoader-Log (MelonLoader/Latest.log) und auf der Content-Seite des Mod-Menüs. Filtere nach einem Stück des Textes:

Meldung (Auszug)UrsacheLösung
pack has no idFeld id fehlt oben im Pack.Eindeutige "id" ergänzen.
not valid jsonSyntaxfehler (Komma, Klammer, Anführungszeichen).Datei in einem JSON-Validator prüfen.
pack id … loaded twiceZwei Packs mit gleicher ID (z. B. Kopie im Content-Ordner und eingebettet).Doppelte Datei entfernen.
part with no id or asset name skippedid = 0 oder asset fehlt.Beide Felder setzen.
part id … already used byID-Kollision mit Vanilla oder anderer Mod.Andere id wählen.
failed to build, donor … was not foundFalscher Donor-Name.Exakten Asset-Namen aus dem Dump nehmen.
behaviour … is neither a registered key nor a type in any loaded assemblyBehaviour zu spät registriert oder Name falsch.ContentRegistry.Behaviour vor dem Laden aufrufen oder vollen Klassennamen nutzen.
does not derive from PartBehaviourDie gefundene Klasse erbt nicht von PartBehaviour.Basisklasse prüfen.
mesh not foundPfad in resource falsch oder Datei nicht eingebettet.Pfad relativ zum Pack prüfen, bei DLL EmbeddedResource setzen.
mesh skipped, nothing resolved from shape/resource/donorPathKein gültiger Mesh-Eintrag.Eine der drei Quellen angeben.
donorPath part … not foundTeil vor | ist kein bekannter Part.Asset-Namen prüfen.
material … not found and PartMetal missingMaterial nicht geladen.material weglassen oder Namen aus Dump nehmen.
texture not foundIcon-/Bildpfad falsch.Pfad und Groß/Kleinschreibung prüfen.
snapping style … only works on hull partsHüllen-Snapping bei normalem Part.Wird automatisch auf PreciseOnFloor gesetzt.
unknown snapping styleName nicht im Enum.Dump „Snapping table“ auf der Diagnostics-Seite ansehen.
socket ids must run 0..n-1Lücke oder Doppelung in Socket-IDs.IDs fortlaufend ab 0 vergeben.
no socket template forKeine Vorlage für den Socket-Typ gefunden.Typ prüfen (power, signal, fluid, item).
cost item … not found, dropped from the recipeUnbekannte Item-ID in cost.ID über ItemCatalog.All prüfen.
item id … is already declaredItem-ID doppelt.Andere ID wählen.
has no model, it will draw blank and cannot be droppedItem ohne model.model.meshes ergänzen.
position must be [x, y]Forschung ohne gültige Position."position": [x, y] oder hidden.
tech tree cell … already usedZelle belegt.Andere Position wählen.
required tech … not foundUnbekannte ID in requires.Tech-ID prüfen.
unlocked part … not foundAsset-Name in research.parts unbekannt.Mit parts[].asset abgleichen.
unknown featureName nicht in ResearchUnlockableFeature.Schreibweise prüfen.
muted after 5 failuresBehaviour/Mod wirft wiederholt Ausnahmen.Stacktrace im Log lesen, Fehler beheben, „Unmute behaviours“.
TrackedShipServer.UpdateParts not foundSpiel-Update hat Methode geändert.Auf neue SDModKit-Version warten.
GameSaver.SaveGame(string) not foundSpiel-Update; Speichern der Part-Daten fällt aus.Auf neue SDModKit-Version warten.
Nothing to configure (im Menü)DescribeConfig liefert keine Zeilen.Zeilen hinzufügen oder menu: "auto".
Waiting for the host's settings (im Menü)Client wartet auf Config vom Host.Normal; bleibt es, fehlt die Mod beim Host.

Häufige Fragen

Brauche ich Unity für eigene Parts?

Nein. SDModKit lädt keine AssetBundles. Modelle kommen als OBJ (z. B. aus Blender), als JSON-Mesh, als Grundform oder aus vorhandenen Spiel-Parts. Unity brauchst du nur, wenn du außerhalb von SDModKit mit Bundles arbeiten willst.

Warum ist Id = "meinpart" rot unterstrichen?

Id ist ein ushort (Zahl 0–65535). Der Name gehört in Asset. In JSON: "id": 1200, "asset": "MeinPart".

Welche Part-IDs soll ich nehmen?

Eine eindeutige Zahl, die weder Vanilla noch andere Mods nutzen – z. B. ein eigener Block ab 1200 pro Mod. Kollisionen meldet das Log mit part id … already used by.

Wie finde ich Donor-Namen und State-Klassen?

Dev-Mod aktivieren (Dev => true), im Spiel auf den Part zielen und F9 drücken. Der Dump unter UserData/SDModKit/Dumps enthält Settings-Name, ID und State-Klasse.

Wie finde ich Item-IDs für Baukosten?

Mit foreach (var it in ItemCatalog.All) Log.Info(it.id + " = " + ItemCatalog.DisplayName(it)); alle IDs ins Log schreiben.

Mein Part erscheint nicht im Baumenü.

Prüfe im Log, ob das Pack geladen wurde und der Part gebaut werden konnte. buildMenuAfter muss einem vorhandenen Asset-Namen oder Label entsprechen; alternativ buildTab setzen. Ist der Part in einer research eingetragen, muss diese Forschung erst freigeschaltet sein.

Mein Menü (config/panel) erscheint nicht.

Der Donor muss ein Storage-Menü öffnen (z. B. StorageChest) und ein Behaviour muss verknüpft sein. Ohne Behaviour gibt es keine Zeilen.

Ich habe die pack.json geändert, aber nichts passiert.

Packs werden beim Spielstart gelesen. Spiel neu starten oder im Mod-Menü „Reload user packs“ und danach „Rebuild world content“ wählen. Eingebettete Packs ändern sich erst mit einer neu gebauten DLL.

Inoffizielle Dokumentation zu SDModKit 1.3.3 von Krunk, abgeleitet aus der SDModKit.dll. Die Mod gehört nicht dem Betreiber dieser Seite, alle Rechte liegen bei Krunk. StellarDrive ist ein Spiel von Curious Owl Games. Keine Verbindung zu Krunk oder Curious Owl Games.