We've made some changes to how XML Data definitions are loaded and merged. This will make it easier for modders to change the behaviour of any piece of game data.
Changes to make modding easier
KSA's asset loading previously relied on combining auto-generated Part data with a hand authored PartGameData. The two pieces of Data were first deserialized into C# classes and then combined using a fixed heuristic. This was good as it allowed us to seperate and easily re-import autogenerated XML, but meant we had to have special handling for duplicate Ids and left modders very few tools for modifying existing assets.With our new system we're still taking a similar high level approach; combining auto-generated data with hand authored data by Id, but we've moved the process out of the class scope and up into the Xml scope. How the data is combined is now defined in the XML itself via a small group of reserved attributes.
What this means for modders
Modders now get to use the same tools that we use. While we only strictly need this system for our part asset pipeline it's now generically available to modify any piece of Data that has an Id attribute.For example, when we want to combine two Xml Elements one auto-generated and one hand authored, the 2nd hand authored element must declare a
Modify="Patch" attribute.Auto-generated,
CoreFuelTankAAssets.xml:
Code
<Part Id="CoreFuelTankA_Prefab_LF1W1HA">
<SubParts>
<SubPart Key="CoreFuelTankA_Subpart_Skin1W1HA" Ref="CoreFuelTankA_Subpart_Skin1W1HA" />
<SubPart Key="CoreFuelTankA_Subpart_EndcapLF1WA" Ref="CoreFuelTankA_Subpart_EndcapLF1WA">
<Transform>
<Position X="0.4468" />
</Transform>
</SubPart>
<SubPart Key="CoreFuelTankA_Subpart_EndcapLF1WA1" Ref="CoreFuelTankA_Subpart_EndcapLF1WA">
<Transform>
<Position X="-0.4468" />
<Rotation X="3.1416" Z="3.1416" />
</Transform>
</SubPart>
</SubParts>
<Connectors>
<Connector Key="_connector17">
<Transform>
<Position X="0.5" />
</Transform>
</Connector>
<Connector Key="_connector18">
<Transform>
<Position X="-0.5" />
<Rotation Z="3.1416" />
</Transform>
</Connector>
</Connectors>
</Part>
CoreFuelTankAGameData.xml:
Code
<Part Id="CoreFuelTankA_Prefab_LF1W1HA" Modify="Patch">
<EditorTags Modify="Extend">
<EditorTag Ref="Fuel Tanks" />
</EditorTags>
<Diameters Modify="Extend">
<Diameter M="1" />
</Diameters>
<Components Modify="Extend">
<Tank>
<CylindricalTank>
<Material Ref="Aluminum.2014" Phase="Solid" />
<Length M="1" />
<OuterRadius M="1" />
<WallThickness Mm="4" />
</CylindricalTank>
</Tank>
</Components>
<Connectors Modify="Extend">
<Connector Key="_connector17" Modify="Patch">
<Capabilities>BulkFluid</Capabilities>
</Connector>
<Connector Key="_connector18" Modify="Patch">
<Capabilities>BulkFluid</Capabilities>
</Connector>
</Connectors>
</Part>
SubParts and the connector transforms above, is kept from the auto-generated data. Attributes on the patch are added, or overwrite the existing value.Child elements can also declare modify attributes if they want to vary from the default patch logic (which is to replace the same named element). The most common usage will be the extend rule which adds this child's elements to the child element in the patched class of the same name.
In the patch above,
Connectors Modify="Extend" keeps the generated connectors, and each Connector Modify="Patch" is matched by its Key. The EditorTags array doesn't exist on the generated part, so Extend creates it.Ids Refs Keys and KeyRefs
If you dig into the xml you'll note the usage of the attributeId has been changed to mean exactly one thing: an Xml Element in the Assets array.Two rules are absolutely enforced:
- Any Element in the assets array must have a unique Id.
- Any Element that has an Id must be only declared in the Assets array.
Code
<PbrMaterial Id="CoreFuelTankA_Material">
<Diffuse Path="Textures/CoreFuelTankA_TextureAtlas_Diffuse.ktx2" Category="Vessel" />
<Normal Path="Textures/CoreFuelTankA_TextureAtlas_Normal.ktx2" Category="Vessel" />
<AoRoughMetal Path="Textures/CoreFuelTankA_TextureAtlas_PBR.ktx2" Category="Vessel" />
<ThinFilm Path="Textures/CoreFuelTankA_TextureAtlas_TFI_Heat.ktx2" Category="Vessel" />
</PbrMaterial>
Code
<Texture Id="CoreFuelTankA_TextureAtlas_Diffuse" Path="Textures/CoreFuelTankA_TextureAtlas_Diffuse.ktx2" Category="Vessel" />
<Texture Id="CoreFuelTankA_TextureAtlas_Normal" Path="Textures/CoreFuelTankA_TextureAtlas_Normal.ktx2" Category="Vessel" />
<Texture Id="CoreFuelTankA_TextureAtlas_PBR" Path="Textures/CoreFuelTankA_TextureAtlas_PBR.ktx2" Category="Vessel" />
<Texture Id="CoreFuelTankA_TextureAtlas_TFI_Heat" Path="Textures/CoreFuelTankA_TextureAtlas_TFI_Heat.ktx2" Category="Vessel" />
<PbrMaterial Id="CoreFuelTankA_Material">
<Diffuse Ref="CoreFuelTankA_TextureAtlas_Diffuse" />
<Normal Ref="CoreFuelTankA_TextureAtlas_Normal" />
<AoRoughMetal Ref="CoreFuelTankA_TextureAtlas_PBR" />
<ThinFilm Ref="CoreFuelTankA_TextureAtlas_TFI_Heat" />
</PbrMaterial>
PbrMaterial references them using the new Ref attribute. The data is now clear on whether an element is a reference to another piece of data or an instance, previously the term Id was used for both and could be confusing.Child data often needs an identifier for cross connections or serialization. Previously these used the attribute
Id, now they use the attribute Key, a Key should be unique inside its own scope, but does not need to be globally unique. Other pieces of child data may reference siblings via the KeyRef attribute.
Code
<SubPart Id="CorePropulsionA_Subpart_EngineALargeVacAssembly" Modify="Patch">
<Rockets Modify="Extend">
<Rocket Key="Engine">
<Core KeyRef="ThrustChamber" />
<Nozzles>
<Nozzle KeyRef="Nozzle" />
</Nozzles>
</Rocket>
</Rockets>
<RocketCores Modify="Extend">
<Combustor Key="ThrustChamber">
<FeedsFroms>
<FeedsFrom FromParent="true" />
</FeedsFroms>
<Reaction Ref="Hydrolox">
<MixtureRatio>5.5</MixtureRatio>
</Reaction>
<MaxPressure Bar="49" />
<ThermalEfficiency Value="1" />
<MinimumThrottle Value="0.1" />
</Combustor>
</RocketCores>
<RocketNozzles Modify="Extend">
<DeLavalNozzle Key="Nozzle">
<ExitDiameter M="2.5" />
<AreaRatio Value="49" />
<ReactionPlumes>
<ReactionPlume Default="true">
<VolumetricExhaust Ref="EngineALarge" />
</ReactionPlume>
</ReactionPlumes>
<SoundEvent Ref="DefaultEngineSoundBehavior" Action="On" />
</DeLavalNozzle>
</RocketNozzles>
</SubPart>
Combustor and the DeLavalNozzle are named with a Key. The Rocket then wires them together, pointing at the combustor with KeyRef="ThrustChamber" and at the nozzle with KeyRef="Nozzle". These Keys only need to be unique inside this SubPart, so every engine can call its chamber ThrustChamber. Compare that with Reaction Ref="Hydrolox" and VolumetricExhaust Ref="EngineALarge", which point at assets declared in the Assets array by their Id.XML Arrays
Previously unrelated components of a part could be declared in any order and were placed into their correct collections by the deserializer. While this helped reduce indentation, it also made collections much harder to modify. Now you'll see any elements that are part of a collection will be declared inside an Xml Array element.Before:
Code
<PartGameData Id="CoreFuelTankA_Prefab_LF1W1HA">
<EditorTag Value="Fuel Tanks" />
<Diameter M="1" />
<Collider Id="Collider1">
<Cylinder Id="CylinderCollider1">
...
</Cylinder>
</Collider>
<Tank>
...
</Tank>
<Connector Id="_connector17">
<Capabilities>BulkFluid</Capabilities>
</Connector>
</PartGameData>
Code
<Part Id="CoreFuelTankA_Prefab_LF1W1HA" Modify="Patch">
<EditorTags Modify="Extend">
<EditorTag Ref="Fuel Tanks" />
</EditorTags>
<Diameters Modify="Extend">
<Diameter M="1" />
</Diameters>
<Components Modify="Extend">
<Collider Key="Collider1">
<Colliders>
<Cylinder Key="CylinderCollider1">
...
</Cylinder>
</Colliders>
</Collider>
<Tank>
...
</Tank>
</Components>
<Connectors Modify="Extend">
<Connector Key="_connector17" Modify="Patch">
<Capabilities>BulkFluid</Capabilities>
</Connector>
</Connectors>
</Part>
And finally, a table of modify rules for modifying Id'd objects and child objects.
| Modify | On an Id'd object | On a child object |
|---|---|---|
| (none) | Declares a new asset. A duplicate Id is an error. | Replaces the child of the same name. |
Patch | Merges into the existing asset with the same Id. Anything not mentioned is kept. | Merges into the child of the same name, or the array item with the same Key. |
Extend | Not used. | Adds its items to the array of the same name, creating it if needed. Items with their own Modify are matched by Key (or Ref) and patched, replaced or removed. |
Replace | Replaces the whole asset. | Replaces the whole element. |
Remove | Removes the asset. The element must be empty. | Removes the element. The element must be empty. |
PatchIfExists, ReplaceIfExists, RemoveIfExists | Same as above, but skipped silently if the asset doesn't exist. | Not used. |
Enjoyed this update?
Support the team behind Kitten Space Agency to keep updates like this coming.