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ü.
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.Content-Pack
Ein Part nur mit einer JSON-Datei und einem OBJ-Modell aus Blender.
C# / .NET 4.7.2Eigene Mod
Logik, Menüs, Keybinds und Einstellungen über die SDModKit-API.
ReferenzJSON-Felder
Jedes Feld von parts, items, research und attach.
Log-Meldungen
Was die Warnungen im MelonLoader-Log bedeuten und wie du sie behebst.
| Eigenschaft | Wert |
|---|---|
| Mod-Name | StellarDrive Mod Kit |
| Version | 1.3.3 |
| Autor | Krunk (GitHub: Krunk-theduck/SDModKit) |
| Spiel | MelonGame("CuriousOwlGames", "StellarDrive") |
| Ladepriorität | MelonPriority(-1000) – lädt vor allen anderen Mods |
| Zielframework | .NET Framework 4.7.2 (Mono) |
| Netzwerk | FishNet (Broadcasts) |
| Abhängigkeiten | MelonLoader, 0Harmony, Newtonsoft.Json, Unity InputSystem, TextMeshPro |
Wichtig vorab
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:
| Feld | Typ | Beispiel | Bedeutung |
|---|---|---|---|
Id | ushort | 1200 | Numerische Part-ID (0–65535), muss eindeutig sein. Kein String. |
Asset | string | "PottedPlant" | Interner Name des Parts (wird zum Namen der PartSettings). Kein GameObject. |
Donor | string | "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ötig | Nein | Ja (C#, Visual Studio) |
| Neue Parts, Items, Forschung | Ja | Ja (Pack in die DLL eingebettet) |
| Eigene Logik pro Part | Nur über vorhandene Behaviours anderer Mods | Ja, über PartBehaviour |
| Einstellungen, Keybinds, Menüseiten | Nein | Ja |
| Ablageort | UserData/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.
- Modell in Blender exportierenDatei → Exportieren → Wavefront (.obj). UVs und Normalen aktivieren, Maßstab 1 m = 1 Einheit. Details unter OBJ aus Blender.
- Ordner anlegen
StellarDrive/UserData/SDModKit/Content/Pflanzen/models/und die Dateiplant.objdort ablegen. - pack.json schreibenIm Ordner
Pflanzen/eine Dateipack.jsonmit folgendem Inhalt anlegen. - Spiel startenIm MelonLoader-Log sollte
Pflanzen-Pack 1.0.0 loaded: 0 items, 1 parts, 0 techsstehen. - 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:
| Feld | Typ | Standard | Beschreibung |
|---|---|---|---|
idPflicht | string | – | Eindeutige Pack-ID, z. B. "example.plants". Ohne ID wird das Pack verworfen. |
name | string | = id | Anzeigename im Log und im Mod-Menü. |
version | string | "0.0.0" | Wird als id@version im Spielstand vermerkt. |
parts | array | [] | Neue Bauteile → parts |
items | array | [] | Neue Items/Ressourcen → items |
research | array | [] | Neue Forschungsknoten → research |
attach | array | [] | Sockets/Logik an Vanilla-Parts anhängen → attach |
Gemeinsame Werteformate
| Format | Schreibweise | Hinweis |
|---|---|---|
| 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": null | Wird 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.
| Feld | Typ | Standard | Beschreibung |
|---|---|---|---|
idPflicht | ushort | – | Eindeutige Part-ID (1–65535). 0 oder fehlend → Part wird übersprungen. |
assetPflicht | string | – | Interner Name. Muss eindeutig sein, wird zum Objektnamen der PartSettings und dient als Schlüssel in research.parts und ContentRegistry.PartByAsset. |
label | string | = asset | Anzeigename im Spiel (fullLabel), auch Titel des Part-Menüs. |
description | string | "" | Beschreibungstext im Baumenü. |
donor | string | "SignalSplitter" | Asset-Name des Vanilla-Parts, der geklont wird. Bestimmt Physik, Interaktion und Menütyp. |
state | string | "SignalSplitterState" | Zustandsklasse (internalStateClass). Bei einem anderen Donor auf dessen State-Klasse setzen (steht im Dump). |
behaviour | string | null | Registrierter Behaviour-Schlüssel oder voller Klassenname einer PartBehaviour-Klasse. |
icon | string | null | Pfad zu einem PNG/JPG relativ zum Pack. Ersetzt das Baumenü-Thumbnail. |
buildMenuAfter | string | null | Asset-Name oder Label eines Vanilla-Parts. Der neue Part wird in dessen Zeile direkt dahinter einsortiert (bei voller Zeile in eine freie). |
buildTab | string | null | Eigener Baumenü-Tab, z. B. "Machines" oder "Logistics". Hat Vorrang vor buildMenuAfter. |
buildRow | int | 0 | Zeile innerhalb von buildTab. |
mass | float | 10 | Masse, wird auf 0,1–2000 begrenzt. |
menu | string | "auto" | auto: Donor-Menü, nur umbenannt · config: Einstellungs-Panel neben dem Menü · panel: Panel ersetzt das Menü. Siehe Konfigurationsmenü. |
snapping | string | Donor | Name eines SnappingStyle-Werts (Groß/klein egal). Siehe Bounds & Snapping. |
bounds | object | auto | { "center": [x,y,z], "size": [x,y,z] }. Fehlt es, wird die Größe aus dem Modell gemessen. |
visual | object | – | clearDonor, fitInteraction, meshes → Visual & Meshes. |
sockets | array | Donor | Eigene Anschlüsse. Ist das Feld vorhanden (auch leer), werden alle Donor-Sockets entfernt → Sockets. |
cost | object | {} | Baukosten { "itemId": menge }. Unbekannte Items werden mit Warnung verworfen. |
config | object | null | Startwerte für die PartConfig jedes platzierten Exemplars. |
Was beim Bauen passiert
ContentBuilder.Configure arbeitet die Definition in dieser Reihenfolge ab:
- Label, Beschreibung, State-Klasse und Masse auf die geklonten
PartSettingsschreiben. - Snapping setzen (nur wenn angegeben) und Baukosten eintragen.
- Optik des Donors entfernen (wenn
clearDonor= true) und deine Meshes unter einem KindobjektSDModKitMesheseinhängen. - Sockets neu aufbauen (wenn
socketsvorhanden). - Bounds aus allen Renderern messen (wenn
boundsfehlt) undShipPartBoundssetzen. - Interaktions-Collider auf die Bounds anpassen (wenn
fitInteraction= true). - 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-Name | Typischer Einsatz |
|---|---|
SignalSplitter | Standard-Donor. Kleiner, schwebender Logik-Part. |
FluidSplitter, PowerSplitter | Kleine Verteiler; liefern Socket-Vorlagen für Fluid/Power. |
FluidTank, Battery, MathGate | Weitere Socket-Vorlagen-Lieferanten. |
StorageChest | Part 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).
| Feld | Typ | Standard | Beschreibung |
|---|---|---|---|
clearDonor | bool | true | Entfernt Renderer, Mesh-Filter und Kindobjekte der Donor-Optik. |
fitInteraction | bool | true | Passt den Interaktions-Collider an die Bounds an und deaktiviert überlappende Collider. |
meshes | array | [] | 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".
| Feld | Typ | Standard | Beschreibung |
|---|---|---|---|
resource | string | null | Pfad zu einer .obj-Datei oder einer JSON-Mesh-Datei, relativ zum Pack-Ordner (oder Name einer eingebetteten Ressource). |
donorPath | string | null | Mesh aus einem Spiel-Prefab übernehmen: "Kind/Pfad" im Donor oder "AnderesPart|Kind/Pfad". |
shape | string | null | cube, sphere, cylinder, capsule, quad, plane (unbekannte Werte = cube) sowie text und screen. |
material | string | "PartMetal" | Name eines geladenen Spiel-Materials. Nicht gefunden → PartMetal. |
color | Farbe | null | Tönung, setzt _BaseColor/_Color auf einer Materialkopie. |
position | Vector3 | [0,0,0] | Lokale Position. |
rotation | Vector3 | [0,0,0] | Euler-Winkel in Grad. |
scale | Vector3 | [1,1,1] | Skalierung. Bei text/screen: x/y = Fläche. |
text | string | null | Fester Text für shape: "text". |
textColor | Farbe | weiß / cyan | Textfarbe (Text: sonst color oder weiß, Screen: hellcyan). |
font | string | Spiel-Font | Name eines TextMeshPro-Fonts. Ohne Angabe wird der häufigste Spiel-Font genutzt. |
screen | string | null | Kanalname für dynamische Inhalte (macht das Mesh automatisch zum Screen). |
columns | int | 16 | Zeichenbreite des Lauftexts (4–64). |
speed | float | 6 | Laufschrift-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.
| Feld | Typ | Standard | Beschreibung |
|---|---|---|---|
id | byte | 0 | Socket-Nummer. Müssen lückenlos 0 … n-1 laufen. |
type | string | "item" | power, signal, fluid oder item (alles andere = item). |
position | Vector3 | [0,0,0] | Lokale Position am Part. |
facing | Vector3 | auto | Euler-Rotation. Ohne Angabe wird der Socket waagerecht relativ zum Bounds-Mittelpunkt ausgerichtet. |
relayed | bool | false | Alle Sockets mit relayed: true werden untereinander als durchgeschleift verknüpft (wie bei einem Splitter). |
Socket-Typen
| JSON | CableType | Bemerkung |
|---|---|---|
signal | 0 | Signalkabel |
fluid | 1 | Fluid-Leitung |
power | 2 | Stromkabel |
item | 3 | Neuer 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.
| Feld | Typ | Standard | Beschreibung |
|---|---|---|---|
idPflicht | uint | – | Eindeutige Item-ID. Kollisionen mit Vanilla- oder anderen Mod-Items werden übersprungen. |
name | string | "item <id>" | Anzeigename. |
icon | string | null | PNG/JPG relativ zum Pack. |
mass | float | 1 | Masse pro Einheit. |
maxStack | int | 20 | Stapelgröße, 1–200. |
isFluid | bool | false | Item ist eine Flüssigkeit. |
isGas | bool | false | Item ist ein Gas. |
fluidColor | Farbe | schwarz | Farbe in Tanks/Leitungen. |
model | object | – | { "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.
| Feld | Typ | Standard | Beschreibung |
|---|---|---|---|
idPflicht | ushort | – | Eindeutige Tech-ID. |
name | string | "Tech <id>" | Name im Forschungsbaum. |
position | [x, y] | [0, 0] | Zelle im Tech-Tree. Pflicht, außer bei hidden. Belegte Zellen werden gemeldet. |
requires | ushort[] | [] | IDs der Voraussetzungen. |
parts | string[] | [] | Asset-Namen der Parts, die freigeschaltet werden. |
features | string[] | [] | Namen aus dem Spiel-Enum ResearchUnlockableFeature. Unbekannte Namen werden ignoriert. |
cost | object | {} | Forschungskosten { "itemId": menge }, auch eigene Items. |
hidden | bool | false | Knoten wird nicht im Baum angezeigt. |
grantedBy | ushort | 0 | Wird 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
excludestehen.
Laut Log werden dabei je Part zwei Sockets ergänzt. Das Prefab wird vorher geklont, beim Verlassen der Welt wird alles zurückgesetzt.
| Feld | Typ | Standard | Beschreibung |
|---|---|---|---|
id | string | "attach" | Name für das Log. |
socket.type | string | "item" | Socket-Typ wie bei sockets. |
socket.face | string | "front" | front, back, left, right, top. Vorhandene Sockets haben Vorrang bei der Seitenwahl. |
behaviour | string | null | Behaviour für alle erweiterten Parts. |
menu | string | "config" | Menümodus für erweiterte Parts. |
config | object | null | Startwerte der PartConfig. |
exclude | string[] | [] | 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) undusemtl. - 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
usemtlwird ein eigenes Submesh..mtl-Dateien werden ignoriert.
Empfohlene Export-Einstellungen
| Blender-Option | Einstellung | Warum |
|---|---|---|
| Forward / Up | -Z / Y (Standard) | Passt zur Achsen-Umrechnung des Parsers. |
| Scale | 1.0, Maßstab vorher anwenden (Strg+A) | 1 Blender-Einheit = 1 m im Spiel. |
| UV Coordinates | an | Für Texturen/Material-Mapping. |
| Normals | an | Sonst werden harte Kanten weich gerechnet. |
| Triangulated Mesh | an (empfohlen) | Vermeidet Fehler bei nicht-konvexen Flächen. |
| Materials | möglichst ein Material | Der Renderer bekommt nur ein Material. Weitere Submeshes werden nicht sichtbar gerendert (aus Unity-Verhalten abgeleitet). |
| Ursprung | unten mittig | Der 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] ]
}
| Feld | Inhalt |
|---|---|
verticesPflicht | Flache Liste x, y, z, … (Unity-Koordinaten, keine Achsen-Umrechnung). |
normals | Gleiche Länge wie vertices, sonst automatisch berechnet. |
uv | u, v je Vertex (Länge = Vertex-Anzahl × 2). |
submeshesPflicht | Liste von Index-Listen (Dreiecke, Uhrzeigersinn = Unity-Vorderseite). |
name | Name 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-Feld | Typ | Wirkung |
|---|---|---|
Text | string | Angezeigter Text (Rich-Text von TextMeshPro möglich, außer bei Scroll). |
Scroll | bool | Lauftext: zeigt columns Zeichen und scrollt mit speed Zeichen/s in Monospace. |
TextColor | Color? | Überschreibt die Textfarbe. |
Image | Texture2D | Ersetzt 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.
| Verwendung | Feld | Wirkung |
|---|---|---|
| Baumenü-Icon eines Parts | parts[].icon | Überschreibt das gerenderte Thumbnail (IconOverrides.SetPart). |
| Item-Icon | items[].icon | Icon im Inventar. |
| Material | meshes[].material | Sucht 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:
- exakter Ressourcenname,
- ein Name, der auf
.models.plant.objendet, - ein Name, der auf
.plant.objendet (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.
- Projekt anlegenVisual Studio → „Klassenbibliothek (.NET Framework)“ oder ein SDK-Projekt mit
net472. Kein .NET 6/8. - Referenzen setzenDie DLLs aus dem Spielordner referenzieren (siehe csproj unten), jeweils mit
Private=false, damit sie nicht mitkopiert werden. - Bauen & kopierenDie fertige
DeineMod.dllnachStellarDrive/Mods/kopieren – nebenSDModKit.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
| Namespace | Inhalt |
|---|---|
SDModKit | ModKit, ModStamp, ModToggles |
SDModKit.Api | SDMod, ModPage |
SDModKit.Configuration | ConfigFile, ConfigEntry<T> |
SDModKit.Content | Packs, Behaviours, PartConfig, Screens, Menüs |
SDModKit.Input | Keybinds, Keybind, MouseBind |
SDModKit.UI | ModUi, Banner, GameModal, KeyTips, Theme |
SDModKit.Util | Log, Toasts, Dump, EmbeddedAssets |
SDModKit.Game | Spiel-Helfer: Spieler, Items, Schiffe, Baumenü |
SDModKit.Patching | ModPatches (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
| Mitglied | Typ | Beschreibung |
|---|---|---|
Idabstract | string | Eindeutige ID, z. B. "examplemod". Schlüssel in modkit.cfg und im Spielstand. |
Nameabstract | string | Anzeigename im Mod-Menü. |
Essential | bool | true = kann im Mod-Menü nicht abgeschaltet werden. |
Dev | bool | true aktiviert die Entwickler-Tasten des Kits (Einfg Menü, F9 Dump). |
HostOnly | bool | Vorhanden, wird aber in 1.3.3 nirgends ausgewertet. |
Config | ConfigFile | Die bei Register übergebene Datei. |
Log | Log | Logger mit dem Mod-Namen als Präfix. |
Pages | IReadOnlyList<ModPage> | Eigene Seiten im Mod-Menü. |
OnRegistered() | virtual | Direkt nach Register. Hier Config-Einträge, Keybinds und Seiten anlegen. |
OnWorldEnter() | virtual | Wenn eine Welt geladen ist (Host und Client). |
OnWorldExit() | virtual | Beim Verlassen der Welt. |
OnUpdate() | virtual | Jeder Frame (auch im Hauptmenü). |
AddPage(title, draw) | protected | Seite hinzufügen. Überladung AddPage(group, title, draw) für Untergruppen. |
ModKit – statische API
| Mitglied | Beschreibung |
|---|---|
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 / Disabled | Aktive 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. |
DevToolsPresent | true, wenn eine registrierte Mod Dev => true hat. |
Menu, Log | Menü-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
Convertin 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()oderConfigFile.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
}
| Mitglied | Beschreibung |
|---|---|
Pressed / Held / Released | Zustand im aktuellen Frame. |
Display | Anzeigetext der Belegung, z. B. „G“. |
Key / Mouse | Aktuelle 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
| Methode | Wann / 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. |
PanelScale | Größenfaktor des Panels im Modus panel (mind. 0,5). |
DrawConfig(part), Status(part) | Vorhanden, werden in 1.3.3 aber nicht aufgerufen. |
Trace / Warn / Error | Log-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
| Mitglied | Beschreibung |
|---|---|
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.ShipServerTick | Event Action<IShipStateWrite, float> – einmal pro Schiffs-Update auf dem Host. |
ContentRuntime.All, TrackedShips | Alle Behaviours, Anzahl beobachteter Schiffe. |
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();
}
| ModUi | Beschreibung |
|---|---|
Section(title, body), BeginSection/EndSection | Kasten mit Überschrift. |
Fold(title, body) | Aufklappbarer Bereich. |
Row(label, controls), BeginRow/EndRow | Zeile mit Label links. |
Label, Note, Space, Separator | Text, 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
| API | Beschreibung |
|---|---|
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. |
Theme | Kit-Farben: Accent, Text, TextDim, Good, Warn, Bad, Panel … |
GameStyle | Farben 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.
| Klasse | Wichtige Mitglieder | Zweck |
|---|---|---|
GameRefs | IsServer, IsClient, InWorld, PlayersServer, PlayerCamera, Service<T>() | Grundzustand und Spiel-Dienste. |
LocalPlayer | StateType, IsPiloting, IsOnFoot, GameMenuOpen, TryEquip | Lokaler Spieler. |
PlayerAccess | LocalServerPlayer, WorldPosition, TryGive(itemId, qty, out err), TryClearInventory, CurrentCounts | Inventar und Position. |
ItemCatalog | All, ById, NameOf, DisplayName, Icon, Thumbnail | Alle Items (inkl. Mod-Items). |
PartLookup | ByAssetName, ById | PartSettings finden. |
ItemMove | Accepts, Count, Total, Contents, Transfer, Move, MaxStack, IsMachine | Items zwischen Inventaren bewegen (Host). |
ServerTrade | Has, TryTrade, Split, HeldItem | Handel/Item in der Hand. |
WorldQuery | ScanMinables, ScanDrops | Abbaubare Objekte und Drops in der Nähe. |
ShipAim | TryGetAimedPart, TryPart, ShipTransform | Part im Fadenkreuz. |
LaserTuning | SetRange(owner, factor), SetSpeed(owner, factor) | Abbaulaser anpassen. |
BuildTabs | Declare(tab, order, icon), Add, AddAll, Hide | Eigene Baumenü-Tabs („Machines“ = 10, „Logistics“ = 20). |
CustomParts | Register(CustomPartDefinition), Get, All, PartsReady | Parts per Code statt JSON (Id, Name, Donor, BuildMenuAfter, BuildTab, BuildRow, Configure, AutoFinish). |
IconOverrides | SetPart, SetItem | Icons ersetzen. |
BuildCost | Total, Shortfall, TryCharge, Refund | Baukosten berechnen und abbuchen. |
GhostSet, BuildPreview, PartGeometry, GameRay, ToolRefs | – | Bauvorschau, Geometrie, Raycasts, Werkzeuge. |
ItemCables | CableType 3 | Der 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"
}
}
- RegistrierenIn
OnInitializeMelon:ModKit.Register(new ItemGiverMod(), ConfigFile.For("example.itemgiver")); - Welt als Host starten
TryGivefunktioniert nur auf dem Host (sonst Meldungnot hosting). - 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");
- State-Klasse prüfen
StorageChestStateist ein Platzhalter. Den echten Namen liefert der Dump (F9 auf eine Kiste zielen). - BauenDen orangen Würfel aus dem Baumenü platzieren.
- 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")
};
});
- BauenDen Part platzieren – der Bildschirm zeigt sofort Rolle und Zeit.
- Lauftext testen
Scroll = trueimScreenFramesetzen und im JSON"columns": 12ergä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");
}}
});
}
}
}
- Mod-Menü öffnenMit Dev-Mod über Einfg, sonst per
ModKit.OpenMenu()(siehe Das Mod-Menü). - TestenWerte ändern, Spiel neu starten – die Werte stehen in
UserData/SDModKit/example.settings.cfgund bleiben erhalten.
Lebenszyklus
Die Reihenfolge, in der SDModKit und deine Mod aufgerufen werden:
MelonPriority(-1000). Kit-Config, Menü-Host, Content-Hooks werden installiert, dann alle JSON-Packs aus UserData/SDModKit/Content gelesen.
OnInitializeMelonContentRegistry.Behaviour(…) → LoadEmbedded(…) → ModKit.Register(…) → SDMod.OnRegistered().
SDMod.OnUpdate()Auch im Hauptmenü. Stummgeschaltete Mods werden übersprungen.
Items eingefügt, Parts geklont und konfiguriert, attach angewendet, Forschung eingefügt, Spielstand-Daten (sdmodkit-content.json) geladen. Danach SDMod.OnWorldEnter() und PartBehaviour.OnWorldEnter().
OnPartAdded / OnPartRemoved, dann ServerTick je Exemplar und das Event ShipServerTick.
ConfigTitle + DescribeConfig; Klicks → OnChoose bzw. ContentActions.Send → OnAction auf dem Host.
Nach GameSaver.SaveGame schreibt das Kit sdmodkit-content.json neben den Spielstand.
OnWorldExit(), Configs speichern, attach-Änderungen und Forschung zurücksetzen, Stummschaltungen aufheben.
Multiplayer
StellarDrive nutzt FishNet. SDModKit trennt sauber zwischen Host und Client:
| Was | Host | Client |
|---|---|---|
ServerTick, OnPartAdded, OnPartRemoved, ShipServerTick | ja | nein |
OnAction | ja (vom Client ausgelöst) | nein |
DescribeConfig | ja | ja, mit synchronisierter Config |
ContentPart.Ship | gesetzt | null |
| PartConfig ändern | direkt | per OnChoose → ganze Config an Host |
SDMod.OnUpdate, Keybinds, UI | ja | ja |
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" }
}
| Daten | Wo gespeichert |
|---|---|
| PartConfig | sdmodkit-content.json → parts |
| WorldData | sdmodkit-content.json → world |
| Mod-Einstellungen, Keybinds | UserData/SDModKit/<name>.cfg (global, nicht pro Welt) |
| Mod an/aus | UserData/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.
| Namespace | Typ | Öffentliche Mitglieder |
|---|---|---|
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() |
Konstanten & Standardwerte
| Wert | Inhalt |
|---|---|
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 Einträge |
ContentLog.ConsoleFloor | Warn (ab hier in die Konsole) |
CustomParts.RowCapacity | 8 Parts pro Baumenü-Zeile |
PartFinish.InteractionOutset / SocketStandoff | 0.02 / 0.06 m |
Dump.MaxDepth / MaxChildren | 8 / 64 |
| Fehlergrenze Mods / Behaviours | 5 Ausnahmen → stumm bis Weltwechsel |
| Config-Autospeichern | 2 s nach letzter Änderung |
| Kit-Tasten (nur mit Dev-Mod) | Einfg Menü · F9 Dump des anvisierten Parts |
Theme.Accent | #4FC3E8 |
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.
| Abschnitt | Inhalt |
|---|---|
tags, layers | Unity-Tags und Layer. |
snapping | Alle Snapping-Detektoren/Stile. |
cables | Kabel-Konfigurationen je Typ. |
menus | Menü-Prefabs. |
content parts | Deine geklonten Parts mit Bounds, Sockets, Renderern. |
donor parts | Vanilla-Parts mit Objektbaum (bis Tiefe 8, je 64 Kinder). |
aimed part | Der 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) | Ursache | Lösung |
|---|---|---|
pack has no id | Feld id fehlt oben im Pack. | Eindeutige "id" ergänzen. |
not valid json | Syntaxfehler (Komma, Klammer, Anführungszeichen). | Datei in einem JSON-Validator prüfen. |
pack id … loaded twice | Zwei Packs mit gleicher ID (z. B. Kopie im Content-Ordner und eingebettet). | Doppelte Datei entfernen. |
part with no id or asset name skipped | id = 0 oder asset fehlt. | Beide Felder setzen. |
part id … already used by | ID-Kollision mit Vanilla oder anderer Mod. | Andere id wählen. |
failed to build, donor … was not found | Falscher Donor-Name. | Exakten Asset-Namen aus dem Dump nehmen. |
behaviour … is neither a registered key nor a type in any loaded assembly | Behaviour zu spät registriert oder Name falsch. | ContentRegistry.Behaviour vor dem Laden aufrufen oder vollen Klassennamen nutzen. |
does not derive from PartBehaviour | Die gefundene Klasse erbt nicht von PartBehaviour. | Basisklasse prüfen. |
mesh not found | Pfad in resource falsch oder Datei nicht eingebettet. | Pfad relativ zum Pack prüfen, bei DLL EmbeddedResource setzen. |
mesh skipped, nothing resolved from shape/resource/donorPath | Kein gültiger Mesh-Eintrag. | Eine der drei Quellen angeben. |
donorPath part … not found | Teil vor | ist kein bekannter Part. | Asset-Namen prüfen. |
material … not found and PartMetal missing | Material nicht geladen. | material weglassen oder Namen aus Dump nehmen. |
texture not found | Icon-/Bildpfad falsch. | Pfad und Groß/Kleinschreibung prüfen. |
snapping style … only works on hull parts | Hüllen-Snapping bei normalem Part. | Wird automatisch auf PreciseOnFloor gesetzt. |
unknown snapping style | Name nicht im Enum. | Dump „Snapping table“ auf der Diagnostics-Seite ansehen. |
socket ids must run 0..n-1 | Lücke oder Doppelung in Socket-IDs. | IDs fortlaufend ab 0 vergeben. |
no socket template for | Keine Vorlage für den Socket-Typ gefunden. | Typ prüfen (power, signal, fluid, item). |
cost item … not found, dropped from the recipe | Unbekannte Item-ID in cost. | ID über ItemCatalog.All prüfen. |
item id … is already declared | Item-ID doppelt. | Andere ID wählen. |
has no model, it will draw blank and cannot be dropped | Item 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 used | Zelle belegt. | Andere Position wählen. |
required tech … not found | Unbekannte ID in requires. | Tech-ID prüfen. |
unlocked part … not found | Asset-Name in research.parts unbekannt. | Mit parts[].asset abgleichen. |
unknown feature | Name nicht in ResearchUnlockableFeature. | Schreibweise prüfen. |
muted after 5 failures | Behaviour/Mod wirft wiederholt Ausnahmen. | Stacktrace im Log lesen, Fehler beheben, „Unmute behaviours“. |
TrackedShipServer.UpdateParts not found | Spiel-Update hat Methode geändert. | Auf neue SDModKit-Version warten. |
GameSaver.SaveGame(string) not found | Spiel-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.