一键导入
ksa-add-part
details about the ksa game and how to add parts
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
details about the ksa game and how to add parts
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Validate the unscience mod suite against a new upstream Kitten Space Agency (KSA) game build — the break-check playbook run when the game / its decompiled sources are bumped and you must decide what unscience needs fixed. Covers how to diff the CURRENT (new) vs PREVIOUS (old) game-assemblies trees, build-as-alarm (and the KSAFolder trap that fakes a catastrophic break), the string-reflection watchlist that fails silently at runtime, GLSL/byte-offset/render coupling the compiler cannot see, semantic drift with no symbol change, the known-broken baseline you must NOT re-report as new, and which scope/ + plans/ docs to update in lockstep. Use when asked to "check unscience against the new KSA build", "upgrade KSA", run the version-diff / break-check, or review a game update's impact. REQUIRES two KSA game-assemblies trees: CURRENT and PREVIOUS.
how to use fd cli tool
how to use genhttp effectively for http server
Execute git commit with conventional commit message analysis, intelligent staging, and message generation. Use when user asks to commit changes, create a git commit, or mentions "/commit". Supports: (1) Auto-detecting type and scope from changes, (2) Generating conventional commit messages from diff, (3) Interactive commit with optional type/scope/description overrides, (4) Intelligent file staging for logical grouping
preferred imgui layout patterns
details about how to implement a mod
基于 SOC 职业分类
| name | ksa-add-part |
| description | details about the ksa game and how to add parts |
Assembled from: strategic analysis of
Content/Core/(the built-in Core mod) and decompiled game source (decomp/ksa/).
KSA parts are data-driven — no C# code is needed to add a part. A new part consists of:
| File | Purpose |
|---|---|
*.glb | 3D mesh(es) — binary glTF, one per category "atlas" |
*_TextureAtlas_Diffuse.ktx2 | Albedo/colour texture atlas |
*_TextureAtlas_Normal.ktx2 | Normal map texture atlas |
*_TextureAtlas_PBR.ktx2 | AO / Roughness / Metallic packed texture atlas |
*Assets.xml | Visual definitions: meshes, materials, SubPart templates, Part prefabs |
*GameData.xml | Simulation definitions: mass, physics, propulsion, connector flags, editor tags |
mod.toml | Mod manifest that lists which XML files to load |
The game's internal Core mod (Content/Core/) is the canonical reference for all of the above.
The game discovers mods by scanning:
Content/ — built-in mods (Core, etc.)[Documents]/KSA/mods/ — user-installed modsEach mod directory must contain a mod.toml. The assets key in mod.toml lists the XML asset bundle files to load:
# mod.toml
id = "my-mod"
name = "My Mod"
version = "1.0.0"
author = "You"
assets = [
"MyPartsAssets.xml",
"MyPartsGameData.xml",
]
The
assetsarray lists paths relative to the mod's own directory.
The game loads mods in manifest order and merges them all into a single ModLibrary registry. Parts from different mods can be mixed freely.
.glb file with at least one named mesh node.ktx2)*Assets.xml file registered in mod.toml*GameData.xml file registered in mod.tomlNote: You can share textures and GLBs with existing categories to avoid creating your own atlases, but only if your UV layout fits within that atlas. In practice, custom parts need their own atlas unless explicitly test-fitting into an existing one.
.glb (binary glTF 2.0) exclusively. This is the only format the game loads for parts..gltf + .bin (text-based glTF) is technically possible but not used for parts.All parts in a category share a single GLB file called a "mesh atlas." The GLB contains multiple named mesh nodes — one per SubPart visual variant. The XML references them by node name.
| Category | Atlas File |
|---|---|
| Command Module | CoreCommandA_MeshAtlas.glb |
| Propulsion A | CorePropulsionA_MeshAtlas.glb |
| Propulsion B (RCS etc.) | CorePropulsionB_MeshAtlas.glb |
| Structural | CoreStructuralA_MeshAtlas.glb |
| Fuel Tanks | CoreFuelTankA_MeshAtlas.glb |
| Service Module | CoreServiceModuleA_MeshAtlas.glb |
| Fairing | CoreFairingA_MeshAtlas.glb |
| Passage/Hatches | CorePassageA_MeshAtlas.glb |
Convention: {ModId}_{CategoryName}_MeshAtlas.glb
_VM) MeshesEach SubPart should have two mesh nodes in the GLB:
_VM suffix) — used in the editor part browser thumbnail (can be lower detail or identical)Example node names inside a GLB:
CoreStructuralA_Subpart_BracketA
CoreStructuralA_Subpart_BracketA_VM
These exact node names become the Id values used in the XML.
| Format | Usage |
|---|---|
.ktx2 | Primary GPU texture format — required for Diffuse, Normal, AoRoughMetal, Emissive |
.dds | Alternative — used for ThinFilm (heat shield) and some Emissive textures |
.png | Uncompressed — usable during development only; GPU efficiency is lower |
The game natively loads
.ktx2(Khronos GPU texture format). For mod development, start with.pngduring iteration and convert to.ktx2for release using thetoktxorbasisutool.
Like meshes, all parts in a category share one texture atlas per channel. Part surfaces are differentiated by their UV layout within the atlas.
Convention: {ModId}_{CategoryName}_TextureAtlas_{Channel}.ktx2
| Channel | File suffix | Content |
|---|---|---|
| Diffuse | _Diffuse.ktx2 | Base colour (albedo) — RGBA |
| Normal | _Normal.ktx2 | Tangent-space normal map |
| AoRoughMetal | _PBR.ktx2 | R=AO, G=Roughness, B=Metallic |
| Emissive | _Emissive.ktx2 | Self-illumination (optional) |
| ThinFilm | _TFI.dds | Heat-shield interference (optional) |
Example for a mod called "MyMod" with category "WidgetA":
MyMod_WidgetA_TextureAtlas_Diffuse.ktx2
MyMod_WidgetA_TextureAtlas_Normal.ktx2
MyMod_WidgetA_TextureAtlas_PBR.ktx2
All vessel textures use Category="Vessel" in the XML. This is the only value observed for part textures.
The *Assets.xml file defines visual appearance. Its root element is <Assets>.
<?xml version="1.0" encoding="utf-8"?>
<Assets>
<!-- 1. Declare the mesh atlas GLB file -->
<MeshAtlas Path="Meshes/MyMod_WidgetA_MeshAtlas.glb"/>
<!-- 2. Declare the PBR material (textures) -->
<PbrMaterial Id="MyMod_WidgetA_Material">
<Diffuse Path="Textures/MyMod_WidgetA_TextureAtlas_Diffuse.ktx2" Category="Vessel"/>
<Normal Path="Textures/MyMod_WidgetA_TextureAtlas_Normal.ktx2" Category="Vessel"/>
<AoRoughMetal Path="Textures/MyMod_WidgetA_TextureAtlas_PBR.ktx2" Category="Vessel"/>
<!-- Optional: emissive, thin film -->
<Emissive Path="Textures/MyMod_WidgetA_TextureAtlas_Emissive.ktx2" Category="Vessel"/>
<ThinFilm Path="Textures/MyMod_WidgetA_TextureAtlas_TFI.dds" Category="Vessel"/>
</PbrMaterial>
<!-- 3. Define SubPart templates (reusable visual building blocks) -->
<SubPart Id="MyMod_WidgetA_Subpart_MainBody">
<PartModel Id="MyMod_WidgetA_Subpart_MainBody_Model">
<Mesh Id="MyMod_WidgetA_Subpart_MainBody"/> <!-- Name of GLB mesh node -->
<Material Id="MyMod_WidgetA_Material"/>
</PartModel>
<MeshView>
<Mesh Id="MyMod_WidgetA_Subpart_MainBody_VM"/> <!-- _VM GLB node for editor thumb -->
</MeshView>
</SubPart>
<!-- 4. Assemble Parts from SubPart instances and Connectors -->
<Part Id="MyMod_WidgetA_Prefab_SmallA">
<SubPart Id="MyMod_WidgetA_Subpart_MainBody1"
InstanceOf="MyMod_WidgetA_Subpart_MainBody">
<Transform>
<Position X="0" Y="0" Z="0"/>
</Transform>
</SubPart>
<!-- Attachment node (top) -->
<Connector Id="_connector_top">
<Transform>
<Position Y="0.5"/>
<Scale X="1.0" Y="1.0" Z="1.0"/>
</Transform>
</Connector>
<!-- Attachment node (bottom) -->
<Connector Id="_connector_bottom">
<Transform>
<Position Y="-0.5"/>
<Rotation Z="3.14159"/>
<Scale X="1.0" Y="1.0" Z="1.0"/>
</Transform>
</Connector>
</Part>
</Assets>
<MeshAtlas> Element| Attribute | Required | Description |
|---|---|---|
Path | Yes | Relative path to the .glb file from the mod's content root |
Id | Optional | If set, also registers the atlas as a named MeshFile asset |
<PbrMaterial> Element| Attribute | Required | Description |
|---|---|---|
Id | Yes | Unique identifier referenced by <Material Id="..."/> in SubParts |
Child elements: <Diffuse>, <Normal>, <AoRoughMetal> are required. <Emissive> and <ThinFilm> are optional.
Each texture element has:
| Attribute | Required | Description |
|---|---|---|
Path | Yes | Relative path to the texture file |
Category | Yes | Always "Vessel" for part textures |
<SubPart> Template Element| Attribute | Required | Description |
|---|---|---|
Id | Yes | Globally unique identifier for this SubPart template |
Child elements:
<PartModel Id="..."> — static rigid mesh renderer
<Mesh Id="..."/> — references a named node in the MeshAtlas GLB<Material Id="..."/> — references a <PbrMaterial> by Id<ShadowCaster>false</ShadowCaster> — optional, disables shadow casting (use for transparencies like windows)<PartModelDynamic Id="..."> — same as PartModel but for deformable/procedural geometry (e.g., SRB segments that stretch)<MeshView> — editor thumbnail mesh; contains <Mesh Id="..."/><Light> — attaches a light source (see Light Reference below)<Part> Prefab Element| Attribute | Required | Description |
|---|---|---|
Id | Yes | Globally unique part identifier — must match the <PartGameData Id="..."> entry |
Child elements:
<SubPart Id="instanceId" InstanceOf="templateId"> — places a SubPart template
<Transform> — local transform override (Position/Rotation/Scale in metres/radians)<Gimbal> — TVC pivot point (for gimbaling engines only)<Connector Id="..."> — attachment node (further configured in GameData)
<Transform> — position/rotation of the connector; Scale X encodes the port diameter in metres<EditorTag Value="..."/> — part browser category (can also be set in GameData)<Light>
<Type>Point</Type> <!-- or Spot -->
<Transform>
<Position X="0" Y="0.1" Z="0"/>
</Transform>
<Range Value="5"/> <!-- metres -->
<Intensity Value="10"/>
<Color R="1" G="0.9" B="0.8"/>
</Light>
<!-- Spot light additional fields: -->
<InnerAngle Value="0.3927"/> <!-- radians (~22.5°) -->
<OuterAngle Value="0.7854"/> <!-- radians (~45°) -->
The *GameData.xml file defines physical and simulation properties. Its root element is also <Assets>.
<?xml version="1.0" encoding="utf-8"?>
<Assets>
<!-- SubPart physics/behavior (matched by Id to SubPart template in Assets.xml) -->
<SubPartGameData Id="MyMod_WidgetA_Subpart_MainBody">
<SolidSphereMass>
<Mass Kg="50"/>
<Radius M="0.5"/>
</SolidSphereMass>
</SubPartGameData>
<!-- Part physics/behavior (matched by Id to Part prefab in Assets.xml) -->
<PartGameData Id="MyMod_WidgetA_Prefab_SmallA">
<EditorTag Value="Structural"/>
<Connector Id="_connector_top">
<Flags>FromSurface</Flags>
</Connector>
<Connector Id="_connector_bottom">
<Flags>FromSurface</Flags>
</Connector>
</PartGameData>
</Assets>
The two XML files are linked only by matching Id attributes at runtime — there is no XML cross-file reference mechanism.
| Assets.xml element | GameData.xml element | Join key |
|---|---|---|
<SubPart Id="X"> | <SubPartGameData Id="X"> | X |
<Part Id="Y"> | <PartGameData Id="Y"> | Y |
<Connector Id="Z"> in Part | <Connector Id="Z"> in PartGameData | Z |
Rules:
<SubPart> without a matching <SubPartGameData> is purely visual (no mass, no physics).<Part> without a matching <PartGameData> will still appear but will have no mass or editor tag.<PartGameData> merges its data into the base part (connectors, mass, tags, etc.) — it does not replace it.PartGameData, a second occurrence merges (like the split *Assets.xml / *GameData.xml files in Core do).<SolidSphereMass>
<Mass Kg="100"/>
<Radius M="0.5"/> <!-- used to compute inertia tensor -->
</SolidSphereMass>
<CustomMass>
<LocationBody Z="-0.117"/> <!-- centre of mass offset from part origin, metres -->
<Mass Kg="50"/>
<MassSpecificInertia Ixx="0.0256" Iyy="0.0231" Izz="0.0099"/>
</CustomMass>
MassSpecificInertia values are the principal moments of inertia divided by total mass (I/m, units: m²).
<Tank>
<SphericalTank>
<Material Id="Aluminum.2014(s)"/>
<OuterRadius M="0.276"/>
<WallThickness Mm="4"/>
</SphericalTank>
</Tank>
The Material Id references a substance defined in Substances.xml (built-in). Known material Ids include Aluminum.2014(s).
Propulsion is built from a hierarchy: RocketThrusterController → Rocket → Combustor + DeLavalNozzle.
<SubPartGameData Id="MyMod_Engine_SubPart">
<CustomMass>
<Mass Kg="200"/>
<LocationBody Z="-0.2"/>
<MassSpecificInertia Ixx="0.04" Iyy="0.04" Izz="0.01"/>
</CustomMass>
<RocketEngineController Id="EngineController">
<RocketReference Id="MainEngine"/>
</RocketEngineController>
<Rocket Id="MainEngine">
<Core Id="Chamber"/>
<Nozzle Id="Nozzle"/>
</Rocket>
<Combustor Id="Chamber">
<Combustion Id="LOX_RP1_2.56"/> <!-- propellant combination from Combustion.xml -->
<MaxPressure Bar="70"/>
<ThermalEfficiency Value="0.97"/>
<MinimumPulseTime Seconds="0.5"/>
</Combustor>
<DeLavalNozzle Id="Nozzle">
<ExitDiameter M="1.2"/>
<AreaRatio Value="25"/>
<FlowEfficiency Value="1"/>
<ExpansionEfficiency Value="0.98"/>
<ExhaustLocation X="0" Y="-1.5" Z="0"/> <!-- physics exhaust origin (metres) -->
<ExhaustDirection X="0" Y="-1" Z="0"/> <!-- exhaust unit vector -->
<FxExhaustLocation X="0" Y="-1.4" Z="0"/> <!-- visual effect origin -->
<FxExhaustDirection X="0" Y="-1" Z="0"/>
<VolumetricExhaust Id="ApolloRCS"/> <!-- exhaust plume template from ExhaustAssets.xml -->
<SoundEvent Action="On" SoundId="DefaultRcsThruster"/>
</DeLavalNozzle>
</SubPartGameData>
<SubPartGameData Id="MyMod_RCSThrust_Subpart">
<SolidSphereMass>
<Mass Kg="5"/>
<Radius M="0.1"/>
</SolidSphereMass>
<RocketThrusterController Id="RD-4">
<ControlMap CSV="PitchDown,RollLeft,YawRight"/> <!-- control axes this thruster assists -->
<RocketReference Id="Thruster"/>
</RocketThrusterController>
<Rocket Id="Thruster">
<Core Id="Chamber"/>
<Nozzle Id="Nozzle"/>
</Rocket>
<Combustor Id="Chamber">
<Combustion Id="MMH_NTO_1.6"/>
<MaxPressure Bar="7"/>
<ThermalEfficiency Value="0.95"/>
<MinimumPulseTime Seconds="0.00545"/>
</Combustor>
<DeLavalNozzle Id="Nozzle">
<ExitDiameter M="1.1"/>
<AreaRatio Value="164"/>
<FlowEfficiency Value="1"/>
<ExpansionEfficiency Value="0.70"/>
<ExhaustLocation X="-0.15" Y="0" Z="0"/>
<ExhaustDirection X="-1" Y="0" Z="0"/>
<VolumetricExhaust Id="ApolloRCS"/>
<SoundEvent Action="On" SoundId="DefaultRcsThruster"/>
</DeLavalNozzle>
</SubPartGameData>
Known Combustion Ids (from Combustion.xml):
MMH_NTO_1.6 — storable bipropellant (RCS)LOX_RP1_2.56 — kerolox (main engines)Combustion.xml in the Core modFor TVC (thrust vector control), add a <Gimbal> to the SubPart instance in the Part prefab:
<Part Id="MyMod_Engine_Prefab">
<SubPart Id="MyMod_Engine_Subpart1"
InstanceOf="MyMod_Engine_SubPart">
<Transform>
<Position Y="-0.2"/>
</Transform>
<Gimbal>
<Transform>
<Position Y="1.1"/> <!-- pivot point relative to subpart local space -->
</Transform>
</Gimbal>
</SubPart>
<Connector Id="_connector_top">
<Transform>
<Position Y="0.5"/>
<Scale X="2.0" Y="2.0" Z="2.0"/> <!-- 2-metre port -->
</Transform>
</Connector>
</Part>
Connectors are attachment points. They are declared in the <Part> in *Assets.xml (position/rotation/scale) and augmented in <PartGameData> (flags).
The <Scale> of a connector encodes the port diameter in metres. Matching ports must have the same scale for a valid attachment.
| Scale | Diameter |
|---|---|
X="0.5" | 0.5 m (small RCS) |
X="1.0" | 1 m |
X="2.0" | 2 m |
X="2.5" | 2.5 m |
X="3.0" | 3 m |
Set in <PartGameData>:
| Flag | Meaning |
|---|---|
FromSurface | Other parts' ToSurface connectors can attach here (receive surface attachments) |
ToSurface | This connector attaches radially to another part's surface |
Both flags can appear on the same connector for bidirectional surface mounting.
Standard axial (stack) connectors have no flags — they connect opposing standard connectors of matching scale.
<PartGameData Id="MyMod_Decoupler_Prefab">
<EditorTag Value="Decouplers"/>
<Decoupler ConnectorId="_connector_bottom" Force="500"/>
<SolidSphereMass>
<Mass Kg="50"/>
<Radius M="0.5"/>
</SolidSphereMass>
</PartGameData>
ConnectorId references the connector that separates on staging. Force is the separation impulse.
A simple static structural ring with no propulsion.
mod.tomlid = "my-structural-mod"
name = "My Structural Parts"
version = "1.0.0"
author = "You"
assets = [
"MyStructuralAssets.xml",
"MyStructuralGameData.xml",
]
MyStructuralAssets.xml<?xml version="1.0" encoding="utf-8"?>
<Assets>
<MeshAtlas Path="Meshes/MyStructural_MeshAtlas.glb"/>
<PbrMaterial Id="MyStructural_Material">
<Diffuse Path="Textures/MyStructural_TextureAtlas_Diffuse.ktx2" Category="Vessel"/>
<Normal Path="Textures/MyStructural_TextureAtlas_Normal.ktx2" Category="Vessel"/>
<AoRoughMetal Path="Textures/MyStructural_TextureAtlas_PBR.ktx2" Category="Vessel"/>
</PbrMaterial>
<SubPart Id="MyStructural_Subpart_Ring1m">
<PartModel Id="MyStructural_Subpart_Ring1m_Model">
<Mesh Id="MyStructural_Subpart_Ring1m"/>
<Material Id="MyStructural_Material"/>
</PartModel>
<MeshView>
<Mesh Id="MyStructural_Subpart_Ring1m_VM"/>
</MeshView>
</SubPart>
<Part Id="MyStructural_Prefab_Ring1mA">
<SubPart Id="MyStructural_Subpart_Ring1m1"
InstanceOf="MyStructural_Subpart_Ring1m">
<Transform/>
</SubPart>
<Connector Id="_top">
<Transform>
<Position Y="0.25"/>
<Scale X="1.0" Y="1.0" Z="1.0"/>
</Transform>
</Connector>
<Connector Id="_bottom">
<Transform>
<Position Y="-0.25"/>
<Rotation Z="3.14159"/>
<Scale X="1.0" Y="1.0" Z="1.0"/>
</Transform>
</Connector>
</Part>
</Assets>
MyStructuralGameData.xml<?xml version="1.0" encoding="utf-8"?>
<Assets>
<SubPartGameData Id="MyStructural_Subpart_Ring1m">
<SolidSphereMass>
<Mass Kg="25"/>
<Radius M="0.5"/>
</SolidSphereMass>
</SubPartGameData>
<PartGameData Id="MyStructural_Prefab_Ring1mA">
<EditorTag Value="Structural"/>
<Connector Id="_top">
<Flags>FromSurface</Flags>
</Connector>
<Connector Id="_bottom">
<Flags>FromSurface</Flags>
</Connector>
</PartGameData>
</Assets>
An RCS thruster assembled from a base body and a nozzle SubPart, each with their own physics data.
MyRCSAssets.xml<?xml version="1.0" encoding="utf-8"?>
<Assets>
<MeshAtlas Path="Meshes/MyRCS_MeshAtlas.glb"/>
<PbrMaterial Id="MyRCS_Material">
<Diffuse Path="Textures/MyRCS_TextureAtlas_Diffuse.ktx2" Category="Vessel"/>
<Normal Path="Textures/MyRCS_TextureAtlas_Normal.ktx2" Category="Vessel"/>
<AoRoughMetal Path="Textures/MyRCS_TextureAtlas_PBR.ktx2" Category="Vessel"/>
</PbrMaterial>
<!-- Thruster block (visual body) -->
<SubPart Id="MyRCS_Subpart_Base">
<PartModel Id="MyRCS_Subpart_Base_Model">
<Mesh Id="MyRCS_Subpart_Base"/>
<Material Id="MyRCS_Material"/>
</PartModel>
<MeshView>
<Mesh Id="MyRCS_Subpart_Base_VM"/>
</MeshView>
</SubPart>
<!-- Thruster nozzle (has propulsion data) -->
<SubPart Id="MyRCS_Subpart_Thruster">
<PartModel Id="MyRCS_Subpart_Thruster_Model">
<Mesh Id="MyRCS_Subpart_Thruster"/>
<Material Id="MyRCS_Material"/>
</PartModel>
<MeshView>
<Mesh Id="MyRCS_Subpart_Thruster_VM"/>
</MeshView>
</SubPart>
<!-- Assembled part: one base + one thruster, surface-mount connector -->
<Part Id="MyRCS_Prefab_ThrusterA">
<SubPart Id="MyRCS_Subpart_Base1"
InstanceOf="MyRCS_Subpart_Base">
<Transform/>
</SubPart>
<SubPart Id="MyRCS_Subpart_Thruster1"
InstanceOf="MyRCS_Subpart_Thruster">
<Transform>
<Position X="0.1"/>
</Transform>
</SubPart>
<Connector Id="_mount">
<Transform>
<Position Z="0.1"/>
<Rotation Y="-1.5708"/>
<Scale X="0.5" Y="0.5" Z="0.5"/>
</Transform>
</Connector>
</Part>
</Assets>
MyRCSGameData.xml<?xml version="1.0" encoding="utf-8"?>
<Assets>
<SubPartGameData Id="MyRCS_Subpart_Base">
<SolidSphereMass>
<Mass Kg="3"/>
<Radius M="0.1"/>
</SolidSphereMass>
</SubPartGameData>
<SubPartGameData Id="MyRCS_Subpart_Thruster">
<SolidSphereMass>
<Mass Kg="2"/>
<Radius M="0.08"/>
</SolidSphereMass>
<RocketThrusterController Id="ThrCtrl">
<ControlMap CSV="PitchUp,YawLeft"/>
<RocketReference Id="Thruster"/>
</RocketThrusterController>
<Rocket Id="Thruster">
<Core Id="Chamber"/>
<Nozzle Id="Nozzle"/>
</Rocket>
<Combustor Id="Chamber">
<Combustion Id="MMH_NTO_1.6"/>
<MaxPressure Bar="7"/>
<ThermalEfficiency Value="0.95"/>
<MinimumPulseTime Seconds="0.00545"/>
</Combustor>
<DeLavalNozzle Id="Nozzle">
<ExitDiameter M="0.5"/>
<AreaRatio Value="100"/>
<FlowEfficiency Value="1"/>
<ExpansionEfficiency Value="0.72"/>
<ExhaustLocation X="0.2" Y="0" Z="0"/>
<ExhaustDirection X="1" Y="0" Z="0"/>
<VolumetricExhaust Id="ApolloRCS"/>
<SoundEvent Action="On" SoundId="DefaultRcsThruster"/>
</DeLavalNozzle>
</SubPartGameData>
<PartGameData Id="MyRCS_Prefab_ThrusterA">
<EditorTag Value="RCS"/>
<Connector Id="_mount">
<Flags>ToSurface</Flags>
</Connector>
</PartGameData>
</Assets>
See Worked Example: Minimal New Part — that is already a structural part.
For a structural part with surface-attachment receive (so other parts can radial-mount to it), set FromSurface on its connectors. For radial parts that attach to surfaces, set ToSurface.
<EditorTag Value="..."/> controls which category the part appears under in the part browser. Any string value is accepted; the game auto-creates categories for new values. Known values from Core:
| Value | Category |
|---|---|
Propulsion | Engines |
RCS | RCS Thrusters |
FuelTank | Fuel Tanks |
Structural | Structural parts |
Fairing | Fairings |
Interstage | Interstage adapters |
Command | Command modules |
Passage | Hatches/passages |
ServiceModule | Service modules |
Decouplers | Decouplers |
Hidden | Hidden from the part browser (internal use) |
You can define your own new category string and it will appear in the browser.
SubParts from one XML file can be instanced in a Part defined in a different XML file, provided both asset bundles are loaded. Order of loading within the same mod is determined by the order in the assets array in mod.toml. If a referenced Id is not yet registered at parse time (i.e., the referenced file loads later), the game will still resolve it during the binding phase after all XML is parsed.
Example: Core's capsule parts in CoreCommandAAssets.xml instance hatch SubParts defined in CorePassageAAssets.xml.
The Core mod encodes dimensions in part names. Adopt these for discoverability:
| Suffix | Meaning |
|---|---|
1W, 2W, 3W | 1 / 2 / 3 metre diameter class |
HalfW | Half-width |
1H, 2H, 3H, 6H | Height multiples (1× / 2× / 3× / 6×) |
HalfH | Half-height |
SizeA, SizeB | Different absolute diameters (e.g., SizeA ≈ 1 m, SizeB ≈ 1.5 m) |
VariantA, VariantB | Visual variant (same dimensions, different appearance) |
Example: MyMod_FuelTank_Prefab_2W3H = 2-metre diameter, 3× height.
Understanding the internal pipeline explains why things work (or don't):
Game startup
│
├─ ModLibrary.PrepareManifest()
│ Scans Content/ and Documents/KSA/mods/ for dirs with mod.toml
│
├─ ModLibrary.LoadAll() [in manifest order]
│ For each enabled mod:
│ ├─ mod.LoadAssetBundles()
│ │ For each path in mod.toml assets[]:
│ │ └─ XmlLoader.Load<AssetBundle>(filePath, mod)
│ │ └─ For each XML element in <Assets>:
│ │ └─ asset.OnDataLoad(mod)
│ │ ├─ PartTemplate.OnDataLoad() → ModLibrary.Register(part)
│ │ ├─ SubPartTemplate.OnDataLoad() → registered by parent part
│ │ ├─ PartGameDataReference.OnDataLoad()
│ │ │ → if part exists: existingPart.ApplyGameData(this)
│ │ │ → else: register as PartGameDataReference for later merge
│ │ ├─ MeshFileReference.OnDataLoad() → GltfLoader reads .glb
│ │ │ → ModLibrary.RegisterBinder(mesh) [queues GPU upload]
│ │ └─ TextureReference.OnDataLoad() → queues GPU upload
│
└─ Parallel GPU upload phase
Parallel.ForEachAsync over all IBinder registrations
→ uploads meshes and textures to Vulkan device
Key implications:
PartGameData for an Id that has no matching Part is held and merged if/when the Part is loaded later (order-independent within a single load pass).OnDataLoad but GPU-uploaded asynchronously after all XML parsing is complete.| Pitfall | Detail |
|---|---|
| Duplicate Id | If two elements share the same Id, the second is silently ignored. Always use globally unique Ids (prefix with your mod Id). |
| Mesh node name mismatch | The Id in <Mesh Id="..."/> must exactly match the mesh node name inside the GLB. Case-sensitive. |
Missing _VM mesh | If <MeshView> references a node not in the GLB the part may fail to render in the editor thumbnail. Always include a _VM node. |
No mod.toml | A mod directory without mod.toml is ignored entirely. |
| No Mass defined | A part without any mass definition behaves as massless. Always define at least <SolidSphereMass> somewhere. |
| Wrong connector scale | Connectors only mate with matching scale values. If parts won't attach, check that connector scales match on both sides. |
| Wrong texture format | The game expects .ktx2 for GPU textures. PNG works but is inefficient. DDS works for some channels. |
| Texture Category missing | All texture references need Category="Vessel" on vessel parts — omitting it may cause a load error or incorrect rendering. |
| Exhaust Id not found | <VolumetricExhaust Id="..."/> must reference an Id registered in ExhaustAssets.xml (Core). Use ApolloRCS as a safe default. |
| Combustion Id not found | <Combustion Id="..."/> must reference an Id from Combustion.xml (Core). Use MMH_NTO_1.6 for RCS or LOX_RP1_2.56 for liquid engines. |
| Sound Id not found | <SoundEvent SoundId="..."/> must reference a registered sound. Use DefaultRcsThruster as a safe default. |