# SDModKit Dokumentation – StellarDrive Mod Kit für Stellar Drive Modding > Vollständige Entwickler-Dokumentation für SDModKit 1.3.3, das StellarDrive Mod Kit für MelonLoader: eigene Parts, Items und Forschung per JSON, C#-API, Blender-Modelle, Multiplayer und Fehlersuche – mit Beispielen. URL: https://sdmodkit.mauscd.de/ SDModKit source: https://github.com/Krunk-theduck/SDModKit ## Ü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 / Disclaimer**Dies ist eine **inoffizielle Community-Dokumentation**. SDModKit gehört nicht dem Betreiber dieser Seite – alle Rechte an der Mod liegen bei ihrem Autor [Krunk](https://github.com/Krunk-theduck/SDModKit). 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. | Eigenschaft | Wert | | --- | --- | | Mod-Name | StellarDrive Mod Kit | | Version | `1.3.3` | | Autor | Krunk ([GitHub: Krunk-theduck/SDModKit](https://github.com/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 **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: | 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`. Spielordner: ```plaintext 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) ├─ .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](https://sdmodkit.mauscd.de/#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. 1. **Modell in Blender exportieren**Datei → Exportieren → Wavefront (.obj). UVs und Normalen aktivieren, Maßstab 1 m = 1 Einheit. Details unter [OBJ aus Blender](https://sdmodkit.mauscd.de/#blender-obj). 2. **Ordner anlegen**`StellarDrive/UserData/SDModKit/Content/Pflanzen/models/` und die Datei `plant.obj` dort ablegen. 3. **pack.json schreiben**Im Ordner `Pflanzen/` eine Datei `pack.json` mit folgendem Inhalt anlegen. 4. **Spiel starten**Im MelonLoader-Log sollte `Pflanzen-Pack 1.0.0 loaded: 0 items, 1 parts, 0 techs` stehen. 5. **Welt laden**Der Part erscheint im Baumenü direkt hinter dem Signal-Splitter. UserData/SDModKit/Content/Pflanzen/pack.json: ```json { "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](https://sdmodkit.mauscd.de/#projekt-setup). Main.cs: ```csharp using MelonLoader; using SDModKit; using SDModKit.Api; using SDModKit.Configuration; using SDModKit.Content; using SDModKit.Input; using SDModKit.UI; using SDModKit.Util; using UnityEngine.InputSystem; [assembly: MelonInfo(typeof(ExampleMod.Plugin), "Example Mod", "1.0.0", "YourName")] [assembly: MelonGame("CuriousOwlGames", "StellarDrive")] namespace ExampleMod { // 1) MelonLoader-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 \_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](https://sdmodkit.mauscd.de/#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 | | --- | --- | --- | --- | | `id`Pflicht | 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](https://sdmodkit.mauscd.de/#parts) | | `items` | array | [] | Neue Items/Ressourcen → [items](https://sdmodkit.mauscd.de/#items) | | `research` | array | [] | Neue Forschungsknoten → [research](https://sdmodkit.mauscd.de/#research) | | `attach` | array | [] | Sockets/Logik an Vanilla-Parts anhängen → [attach](https://sdmodkit.mauscd.de/#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 | | --- | --- | --- | --- | | `id`Pflicht | ushort | – | Eindeutige Part-ID (1–65535). `0` oder fehlend → Part wird übersprungen. | | `asset`Pflicht | 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](https://sdmodkit.mauscd.de/#dumps)). | | `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ü](https://sdmodkit.mauscd.de/#config-menue). | | `snapping` | string | Donor | Name eines `SnappingStyle`-Werts (Groß/klein egal). Siehe [Bounds & Snapping](https://sdmodkit.mauscd.de/#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](https://sdmodkit.mauscd.de/#visual). | | `sockets` | array | Donor | Eigene Anschlüsse. Ist das Feld vorhanden (auch leer), werden alle Donor-Sockets entfernt → [Sockets](https://sdmodkit.mauscd.de/#sockets). | | `cost` | object | {} | Baukosten `{ "itemId": menge }`. Unbekannte Items werden mit Warnung verworfen. | | `config` | object | null | Startwerte für die [PartConfig](https://sdmodkit.mauscd.de/#daten) 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-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 id ` und `state `. ## 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). | Beispiel: Gehäuse aus Grundformen: ```json "visual": { "clearDonor": true, "meshes": [ { "shape": "cube", "position": [0, 0.15, 0], "scale": [0.8, 0.3, 0.8], "color": [0.2, 0.25, 0.3] }, { "shape": "cylinder", "position": [0, 0.40, 0], "scale": [0.3, 0.1, 0.3], "material": "PartMetal" }, { "shape": "text", "position": [0, 0.15, -0.41], "scale": [0.6, 0.2, 1], "text": "EX-01", "textColor": [0.3, 0.8, 1] } ] } ``` 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. | Beispiel: zwei Item-Ports: ```json "sockets": [ { "id": 0, "type": "item", "position": [-0.45, 0.2, 0] }, { "id": 1, "type": "item", "position": [ 0.45, 0.2, 0] }, { "id": 2, "type": "power", "position": [ 0, 0.2, 0.45], "facing": [0, 180, 0] } ] ``` 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 | | --- | --- | --- | --- | | `id`Pflicht | uint | – | Eindeutige Item-ID. Kollisionen mit Vanilla- oder anderen Mod-Items werden übersprungen. | | `name` | string | "item " | 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](https://sdmodkit.mauscd.de/#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`). Beispiel: ```json "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 | | --- | --- | --- | --- | | `id`Pflicht | ushort | – | Eindeutige Tech-ID. | | `name` | string | "Tech " | 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). | Beispiel: versteckte Freischaltung: ```json "research": [ { "id": 4100, "name": "Hydrokultur", "position": [6, 2], "requires": [12], "parts": ["PottedPlant"], "cost": { "9001": 20 } } ] ``` Mit `"hidden": true` und `"grantedBy": ` 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. | Feld | Typ | Standard | Beschreibung | | --- | --- | --- | --- | | `id` | string | "attach" | Name für das Log. | | `socket.type` | string | "item" | Socket-Typ wie bei [sockets](https://sdmodkit.mauscd.de/#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. | Beispiel: Item-Ports an alle Kisten: ```json "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. UserData/SDModKit/Content/ExampleMonitor/pack.json: ```json { "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-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. models/wedge.json: ```json { "name": "Wedge", "vertices": [0,0,0, 1,0,0, 0,1,0, 0,0,1], "normals": [0,0,-1, 0,0,-1, 0,0,-1, 0,-1,0], "uv": [0,0, 1,0, 0,1, 1,1], "submeshes": [ [0,2,1, 0,1,3, 0,3,2, 1,2,3] ] } ``` | Feld | Inhalt | | --- | --- | | `vertices`Pflicht | 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). | | `submeshes`Pflicht | 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. ```json { "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. C#: ```csharp 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`). | 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. ExampleMod.csproj (Auszug): ```xml ``` C#: ```csharp 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 anlegen**Visual Studio → „Klassenbibliothek (.NET Framework)“ oder ein SDK-Projekt mit `net472`. Kein .NET 6/8. 2. **Referenzen setzen**Die DLLs aus dem Spielordner referenzieren (siehe csproj unten), jeweils mit `Private=false`, damit sie nicht mitkopiert werden. 3. **Bauen & kopieren**Die fertige `DeineMod.dll` nach `StellarDrive/Mods/` kopieren – neben `SDModKit.dll`. ExampleMod.csproj: ```xml net472 latest ExampleMod ExampleMod 1.0.0 C:\Program Files (x86)\Steam\steamapps\common\StellarDrive $(GameDir)\StellarDrive\_Data\Managed $(GameDir)\MelonLoader\net35\MelonLoader.dllfalse $(GameDir)\MelonLoader\net35\0Harmony.dllfalse $(GameDir)\Mods\SDModKit.dllfalse $(Managed)\Assembly-CSharp.dllfalse $(Managed)\UnityEngine.CoreModule.dllfalse $(Managed)\UnityEngine.IMGUIModule.dllfalse $(Managed)\Unity.InputSystem.dllfalse $(Managed)\Unity.TextMeshPro.dllfalse $(Managed)\Newtonsoft.Json.dllfalse $(Managed)\FishNet.Runtime.dllfalse ``` 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` | | `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 | | --- | --- | --- | | `Id`abstract | string | Eindeutige ID, z. B. `"examplemod"`. Schlüssel in `modkit.cfg` und im Spielstand. | | `Name`abstract | 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 | 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: UserData/SDModKit/examplemod.cfg: ```ini [General] # Geschwindigkeit Speed = 1.5 [Keys] Hello = H ``` C#: ```csharp ConfigEntry speed = Config.Bind("General", "Speed", 1.5f, "Geschwindigkeit"); ConfigEntry enabled = Config.Bind("General", "Enabled", true, "Funktion an/aus"); ConfigEntry 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. C#: ```csharp 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 C#: ```csharp // 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ü](https://sdmodkit.mauscd.de/#config-menue)). | | `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 PlantBehaviour.cs: ```csharp 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 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); } } } ``` pack.json (Auszug): ```json { "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](https://sdmodkit.mauscd.de/#dumps). ### 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` – einmal pro Schiffs-Update auf dem Host. | | `ContentRuntime.All`, `TrackedShips` | Alle 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`. | `menu` | Ergebnis | | --- | --- | | `auto` | Das Donor-Menü bleibt, bekommt nur deinen Titel. | | `config` | Einfaches Einstellungs-Panel neben dem Donor-Menü (Abschnitt, Label, Wert, Auswahl, Icons). | | `panel` | Eigenes Panel ersetzt das Menü komplett – mit Tabs, Banner, Notizen, Icons, Detailtexten und Farben. | ### ConfigRow – Felder | Feld | Typ | Wirkung | | --- | --- | --- | | `Section` | string | Zwischenüberschrift (Zeile nur mit `Section`). | | `Label` | string | Beschriftung links. | | `Value` | string | Wert rechts. | | `ValueColor` / `LabelColor` | Color? | Farben, z. B. `Theme.Good`, `Theme.Warn`, `Theme.Bad`. | | `Choices` | string[] | Auswahl-Buttons. | | `ChoiceEnabled` | bool[] | Einzelne Buttons deaktivieren (panel). | | `OnChoose` | Action | Wird mit dem Index aufgerufen; danach baut sich das Menü neu auf. | | `Highlight` | bool | Zeile hervorheben. | | `Icons` | List | Icon-Leiste: `Image`, `Name` (Tooltip), `State` (Farbindex), `OnClick`. | | `Icon` / `ValueIcon` | Texture | Kleines Bild vor Label bzw. Wert (panel). | | `Detail` / `DetailColor` | string / Color? | Zweite, kleinere Textzeile (panel). | | `Banner` | string | Hinweisbalken oben (panel). | | `Note` | string | Hinweistext (panel). | | `Tabs`, `ActiveTab`, `TabColors`, `Aside` | string[], int, Color[], string | Tab-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. C#: ```csharp int n = part.Config.GetInt("count", 0); float f = part.Config.GetFloat("rate", 1f); bool on = part.Config.GetBool("enabled", true); string name = part.Config.GetString("name", ""); List filter = part.Config.GetIds("filter"); // 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: C#: ```csharp 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. C#: ```csharp 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. C#: ```csharp 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 C#: ```csharp 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`. | C#: ```csharp 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()` | 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“. | C# – alle Item-IDs ins Log schreiben: ```csharp 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. ItemGiverMod.cs: ```csharp using SDModKit.Api; using SDModKit.Configuration; using SDModKit.Game; using SDModKit.Input; using SDModKit.Util; using UnityEngine.InputSystem; public sealed class ItemGiverMod : SDMod { public override string Id => "example.itemgiver"; public override string Name => "Item Giver"; private ConfigEntry \_itemId; private ConfigEntry \_amount; private Keybind \_give; public override void OnRegistered() { \_itemId = Config.Bind("Give", "ItemId", 1, "Item-ID, 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. **Registrieren**In `OnInitializeMelon`: `ModKit.Register(new ItemGiverMod(), ConfigFile.For("example.itemgiver"));` 2. **Welt als Host starten**`TryGive` funktioniert nur auf dem Host (sonst Meldung `not hosting`). 3. **Testen**`F6` 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. CounterBehaviour.cs: ```csharp 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 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); } } ``` pack.json: ```json { "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] } ] } } ] } ``` Plugin.cs (Auszug): ```csharp ContentRegistry.Behaviour("example.counter", () => new CounterBehaviour()); ContentRegistry.LoadEmbedded(typeof(Plugin).Assembly, "pack.json"); ``` 1. **State-Klasse prüfen**`StorageChestState` ist ein Platzhalter. Den echten Namen liefert der Dump (`F9` auf eine Kiste zielen). 2. **Bauen**Den orangen Würfel aus dem Baumenü platzieren. 3. **Testen**Interagieren → 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`. pack.json (Part): ```json { "id": 1301, "asset": "ExampleDisplay", "label": "Info-Display", "buildMenuAfter": "SignalSplitter", "visual": { "meshes": [ { "shape": "cube", "position": [0, 0.2, 0], "scale": [0.6, 0.4, 0.1], "color": [0.15, 0.17, 0.2] }, { "shape": "screen", "screen": "example.clock", "position": [0, 0.2, -0.051], "scale": [0.52, 0.32, 1] } ] } } ``` C# – in OnRegistered(): ```csharp Screens.Provide("example.clock", screen => { int t = (int)UnityEngine.Time.time; string role = SDModKit.Game.GameRefs.IsServer ? "HOST" : "CLIENT"; return new ScreenFrame { Text = role + "\n" + (t / 60).ToString("00") + ":" + (t % 60).ToString("00") }; }); ``` 1. **Bauen**Den Part platzieren – der Bildschirm zeigt sofort Rolle und Zeit. 2. **Lauftext testen**`Scroll = 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. SettingsExampleMod.cs: ```csharp using SDModKit.Api; using SDModKit.Configuration; using SDModKit.UI; using SDModKit.Util; public sealed class SettingsExampleMod : SDMod { public override string Id => "example.settings"; public override string Name => "Settings Example"; private ConfigEntry \_enabled; private ConfigEntry \_strength; public override void OnRegistered() { \_enabled = Config.Bind("General", "Enabled", true, "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ü öffnen**Mit Dev-Mod über `Einfg`, sonst per `ModKit.OpenMenu()` (siehe [Das Mod-Menü](https://sdmodkit.mauscd.de/#modkit-menue)). 2. **Testen**Werte ä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: Spielstart**SDModKit lädt zuerst** `MelonPriority(-1000)`. Kit-Config, Menü-Host, Content-Hooks werden installiert, dann alle JSON-Packs aus `UserData/SDModKit/Content` gelesen. Spielstart**Deine `OnInitializeMelon`** `ContentRegistry.Behaviour(…)` → `LoadEmbedded(…)` → `ModKit.Register(…)` → `SDMod.OnRegistered()`. Jeder Frame**`SDMod.OnUpdate()`** Auch im Hauptmenü. Stummgeschaltete Mods werden übersprungen. Welt geladen**Content 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`. Interaktion**Menü** `ConfigTitle` + `DescribeConfig`; Klicks → `OnChoose` bzw. `ContentActions.Send` → `OnAction` auf dem Host. Speichern**Spielstand** Nach `GameSaver.SaveGame` schreibt das Kit `sdmodkit-content.json` neben den Spielstand. Welt verlassen**Aufräumen** `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. sdmodkit-content.json: ```json { "format": 1, "game": "0.x.y", "mods": ["modkit@1.3.3", "examplemod@1.0.0"], "packs": ["example.plants@1.0.0"], "parts": { "…": "PartConfig 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/.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` | ## 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. DevMod.cs – Entwickler-Tasten aktivieren: ```csharp 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). | Button | Wirkung | | --- | --- | | Reload user packs | Packs aus `UserData/SDModKit/Content` neu einlesen. | | Rebuild world content | Content-Parts der aktuellen Welt neu aufbauen. | | Unmute behaviours | Stummgeschaltete 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. | 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. | C#: ```csharp 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.