Making a KSArmory weapon pack¶
A pack is an ordinary KSA mod that ships parts and art like any other, plus a file of weapon definitions and no code at all. KSArmory holds no list of packs and never learns yours by name. It looks in the same place inside every installed mod and reads what is there, which is why installing one is nothing but installing a mod.
Status. Everything described here is implemented and covered by tests, and a pack has been registered in game —
CHECKLIST.md1.3 records what that flight did and did not reach. The one thing named at the end is not built.KSArmory.Armoury.Schemais1.
There is a complete worked example: KSArmory-example-mod is this whole document as a folder
you can copy — a bomb rack, its art and its definitions, with no code in it at all. Everything
below is what it does and why.
If you have modded KSP, read FROM-KSP-MODDING.md first: there is no PartModule here, and that
is why the shape below is what it is.
PACK-API-SURFACE.md is this same contract generated from the code, and KSArmory's build fails if
it moves without being acknowledged — so a version of this page that disagrees with the reader is a
bug rather than something you have to work around.
The five-minute version¶
mods/MyWeaponPack/
mod.toml
KSArmory/Weapons.xml <- the weapons. KSArmory reads this; KSA never sees it
MyWeaponPackAssets.xml <- your parts, loaded by KSA and nothing to do with KSArmory
MyWeaponPackGameData.xml
Meshes/ Textures/
mod.toml, in full:
name = "My Weapon Pack"
assets = [ "MyWeaponPackAssets.xml", "MyWeaponPackGameData.xml" ]
That is a plain KSA content mod. Nothing in it mentions KSArmory, there is no [StarMap]
section, no assembly, and nothing to compile.
A mod is a directory, so another mod's KSArmory content sits in a folder named after KSArmory
inside it. That is the same reasoning that puts system settings at
saves/<save>/KSArmory/systems.json, and it buys the same things: several mods can do this without
agreeing on filenames, and uninstalling the pack takes its weapons with it.
KSArmory looks in that folder inside every installed mod and reads the .xml files it finds.
It holds no list of packs and never learns yours by name — the convention is the whole mechanism,
the same relationship KSA has with its own mods folder.
Split a pack across as many files as you like, named as you like. A mod's files are read as one unit: every name each declares is known to the others, so a launcher may name a round or a sensor in another file whichever order they sort in. KSArmory's own files are read the same way.
KSArmory/Weapons.xml must not be in mod.toml's assets array. KSA hands every entry there
to its own asset-bundle loader, so a weapon file named there is parsed by the wrong reader.
A mod shipping only the two *Assets.xml files would still put your part in the editor. It just
would not be a weapon.
If you do want code¶
A pack that needs to build definitions at runtime can ship an assembly instead, declare
ModDependencies = [ { ModId = "KSArmory" } ] under [StarMap], and call
KSArmory.Armoury.Register(xml, "MyWeaponPack") from [StarMapBeforeMain]. StarMap holds it back
until KSArmory is up and shares KSArmory's assembly out of its load context, both by default, and
KSArmory registers its own weapons from its own [StarMapBeforeMain], so KSArmory:20MM already
resolves when yours runs.
Register several texts in one call, Armoury.Register([("rounds", a), ("launchers", b)], "MyWeaponPack"),
when one names what another declares: they are read as one unit, as a folder is, and separate calls
are not.
Register by code or by folder, not both. Use your mod's Id as the source: a mod that has registered
from code under its own Id has its KSArmory/ folder skipped, with a line in the log saying so, because
read a second time every definition would be refused as already claimed.
Register takes text, not profiles, and that is worth preserving: constructing a
LauncherProfile in C# needs double3 from Brutal.Numerics, which is RocketWerkz's and not
redistributable, so a pack that built one would need a KSA install to compile. Passing a string
needs KSArmory.dll and StarMap.API.dll and nothing else.
Most packs will never need any of this.
Weapons.xml¶
A pack's root is <WeaponPack Schema="1">. Family is KSArmory's own, for switching a family of its
built-ins off, and is refused on a pack's: a pack is switched off by uninstalling it.
A pack may replace one of KSArmory's own weapons on KSArmory's own part. While its family is on,
the part is taken and the refusal names the family; once a player switches that family off under
KSArmory > Built-in weapons, the pack's definition is the part's, and the part stays in the editor.
The family's rounds and sensors stay registered either way, so the replacement can still name
KSArmory:20MM.
<WeaponPack Schema="1">
<Munition Name="AIM-9X" DisplayName="AIM-9X Sidewinder II" BodyMarker="Aim9x"
Guidance="Seeker" NavConstant="4" SeekerFovDeg="90"
LaunchSpeed="25" BoostSeconds="2.0" BoostAccel="480" MaxFlightSeconds="60"
DragK="2.4e-5" MinRange="300" MaxRange="18000"
ChargeKg="9.4" FuseRadius="12" FuseArmSeconds="0.5" />
<Sensor Name="Seeker9X" DisplayName="AIM-9X seeker" Range="18000" ConeDeg="90"
BoresightSource="PartForward" LockSeconds="1.0" />
<Launcher PartId="MyWeaponPack_Prefab_Lau7x" DisplayName="LAU-7 rail (AIM-9X)"
Munition="AIM-9X" Sensor="Seeker9X"
EjectAwayFromMount="1.5" ReloadSeconds="0" LaunchLoft="0">
<Tube Position="0, 0, 0.9" Direction="1, 0, 0" />
</Launcher>
</WeaponPack>
Four rules that between them explain most refusals:
- An attribute you leave out takes the profile's own default, not zero. State what differs and nothing else. It is also why a field added to KSArmory next year cannot change how your pack behaves — your file does not mention it.
- An attribute this build does not know refuses the whole definition.
NavConstnat="6"is rejected by name rather than ignored, because an ignored typo is a number you can see in your file that nothing reads. - Numbers are read in the invariant culture.
2.4is two and two fifths on every machine. Exponents (3.0e-5) are fine. Vectors are three numbers with commas:"0, 0, 0.9". - Angles are written in degrees. Every attribute ending
Degis degrees, and nothing here takes radians.
<Munition> — how a round flies and what it does on arrival¶
Name and DisplayName are required.
| Default | ||
|---|---|---|
BodyMarker |
none | subpart holding the round's mesh; without one a gun round is drawn as a streak, glowing if TracerEvery makes it a tracer |
FinMarker |
none | subpart holding one fin blade |
Guidance |
CommandLink |
Seeker, AntiRadiation, CommandLink, Inertial, None |
NavConstant |
4 |
proportional-navigation gain; 0 flies straight |
MaxLateralG |
35 |
how hard it may turn |
SeekerFovDeg |
55 |
how far off its nose a seeker still sees the target |
SeekerBand |
Radar |
Radar, Infrared or None: which decoy can take a Seeker round. Chaff fools radar, flares fool infrared, nothing fools None |
CountermeasureResistance |
0 |
0 to 1: how much of a decoy's chance of taking the seeker it refuses |
ReacquiresAfterDecoy |
true |
whether a seeker a decoy took looks for its target again once the decoy is spent |
DopplerGateMps |
0 |
m/s; a radar seeker only sees chaff within this of the target's closing speed, which is only while the target beams. Zero is no gate |
LaunchSpeed |
45 |
m/s imparted at release. A rail imparts almost nothing |
BoostSeconds |
2.4 |
motor burn |
BoostAccel |
520 |
m/s² while burning |
MaxFlightSeconds |
30 |
after which it self-destructs. For a round the ground stops, only time on a path that is not certain to land counts, so a long fall is never cut short |
MinRange, MaxRange |
0, 20000 |
engagement envelope, m |
MassKg, CalibreMm, DragCoefficient |
none | what the round's drag is computed from: mass in flight (kg), frontal diameter (mm), and a coefficient for its shape — about 0.3 for a supersonic shell. All three or none; a round given only some is refused |
DragK |
3.0e-5 |
drag by hand, k in a = k·v² in Earth's sea-level air, for a round no constant coefficient describes — a bomb through the speed of sound, a missile shedding its booster. Ignored when the three above are given |
GravityCompensation |
1 |
how much of gravity guidance cancels; a tail kit (Inertial) steers the fall it predicts and ignores it |
NeutralDensityRatio |
0 |
density it floats at. Near 840 and it swims — that is a torpedo |
SeparationSeconds |
0 |
coast before the motor lights |
FuseRadius |
15 |
proximity burst, m |
TimedFuse |
false |
flak: also burst at the flight time the lead solution asked for. The proximity fuse still runs |
FuseArmSeconds |
0.6 |
how long the proximity fuse is dead after release. A shell still strikes what it touches |
MaxChargeKg |
0 |
the top of the panel's yield slider; 0 is the slider's own ceiling |
ChargeKg |
20 |
the warhead, as one number. Lethal radius, blast radius and fireball are all derived from it by the cube-root law, so doubling it multiplies reach by 1.26 |
HitsTerrain |
false |
stops at the ground. Costs a terrain sample per round per frame |
BurstHeightMetres |
0 |
an air-burst fuse: fires this far over the ground on the way down. 0 bursts on contact. Needs HitsTerrain |
ChuteSinkMetresPerSecond |
0 |
a parachute, described by how fast it brings the store down at sea level once open. 0 is none. Needs HitsTerrain |
ChuteOpensSeconds |
1.5 |
when after release the chute opens; it fills over a second |
BodyLength |
3.10 |
m, for drawing |
BodyModel |
none | a gun round's model: the Id of a <SubPart> template, drawn once per shell in the air with no subpart on the launcher, so there is no limit on how many are seen. Without one a shell is a streak |
BurstEffect |
chosen by the charge | an <Explosion> your asset XML declares, which the warhead goes off as. Only the look: the blast, its reach and a nuclear charge's cloud stay the charge's |
PlumeEffect |
KSArmory's | a <ParticleEmitter> your asset XML declares, held at the nozzle while the motor burns |
TracerEvery |
0 |
a gun round: one in this many burns a tracer, as a belt is loaded. The rest are drawn as a faint grey streak, as bright as the player's Rounds between tracers setting in daylight and fading with the sun, so at night only the tracers are seen. 0 is none |
TracerBurnSeconds |
3 |
how long a tracer burns before it goes out; the round flies on unseen |
FinDeploySeconds |
0.18 |
|
FinDeflectionDeg |
0 |
fins that visibly steer |
FinHingeStation |
0 |
where along the body the fins hinge |
FinsPerRound |
0 |
blades; 0 means the older single-set scheme |
FinStowedScale |
0.06 |
|
MaxFaithfulStepSeconds |
0.32 |
beyond this a step is too coarse to integrate honestly |
PreferredStepSeconds |
0 |
the frame the world is held to while this round flies; 0 is MaxFaithfulStepSeconds |
SubStepSeconds |
0 |
the round's own integration step; 0 is 5 ms |
A staged motor adds stages after the first. BoostSeconds and BoostAccel are stage one, and
each <Stage> follows it in order:
<!-- boosts at 520 m/s² for 2.4 s, then sustains at 90 m/s² for 8 s, then coasts -->
<Munition Name="TwoStage" DisplayName="Two-stage" BoostSeconds="2.4" BoostAccel="520">
<Stage Seconds="8.0" Accel="90" />
</Munition>
<Sensor> — what a launcher can see¶
Name and DisplayName are required.
| Default | ||
|---|---|---|
Range |
36000 |
m |
ConeDeg |
90 |
half-angle of the search cone about the boresight, so 90 is a hemisphere |
BoresightSource |
LocalUp |
LocalUp, PartForward, TurretAxis, MountNormal. A rail wants PartForward; a hemispheric search set wants LocalUp |
ThreatRadius |
8000 |
closest approach inside which a contact is a threat |
ThreatHorizonSeconds |
40 |
how far ahead closest approach is predicted |
LockSeconds |
1.5 |
dwell before a track matures |
MinTargetSpeed |
15 |
below which a contact is ignored |
Emits |
false |
whether it transmits — the only thing anti-radiation rounds home on |
Scope |
None |
what picture it puts in front of the operator: None, Search, or Emitters. None shows no scope tab at all, which is right for a seeker head or a designation set; Emitters is an anti-radiation seeker's list of who is radiating |
ReferenceCrossSectionM2 |
0 (off) |
contact size scaling. Range goes as the fourth root, so a target a hundredth the size is seen at a third the range |
NotchSpeed |
0 (off) |
Doppler notch. Rejects clutter and loses a target crossing exactly abeam |
ChaffNotchMps |
0 (off) |
a target closing slower than this, with a bigger chaff return in its resolution cell, breaks the track |
ChaffReacquireSeconds |
3 |
how long a track chaff broke takes to be found again |
OpticalBackup |
false |
once found again, an optical channel holds the target while it stays in the notch, so chaff breaks it once per pass |
ClutterFloorMetres |
0 (off) |
height below which contacts are lost in ground return |
HorizonMasking |
true |
whether the planet's bulk blocks line of sight |
TerrainMarginMetres |
0 |
inflates the masking sphere |
TerrainSamples |
0 (off) |
height-map lookups one contact may cost. Real terrain masking, at a real per-frame price |
TerrainClearanceMetres |
30 |
how far terrain must stand above the line of sight before it blocks. Without it a site on any slope is blind along its own ground |
CuedOnly |
false |
it finds nothing by itself and only tracks what it is handed. Every director's sensor is; without it a director searches the sky on its own |
Infrared |
false |
passive: it sees a contact by its heat rather than by transmitting, and Range is then only a ceiling |
IrThresholdWPerM2 |
6e-7 |
the faintest heat an Infrared set detects, in W/m² |
LookDownPenalty |
1 |
how much higher that threshold is for a contact seen against the ground |
<Launcher> — the part, and what it does with the round¶
PartId, DisplayName, Munition and Sensor are required. PartId must be a part you declared
in your own Assets.xml.
A launcher must be able to shoot with something — at least one <Tube> or a GunMunition —
or it is refused. A launcher that can shoot with nothing is a part fire control adopts and then
holds fire on for ever, with no gate reporting why.
| Default | ||
|---|---|---|
<Tube Position="x, y, z" Direction="x, y, z" /> |
— | one per tube. Direction is optional; without it the tube points along the pod axis. Splayed tubes, a VLS and an MLRS are all just tube lists |
MagazineDepth |
0 (= tube count) |
rounds carried |
SalvoSpacing |
0.45 |
s between launches |
ReloadSeconds |
0 |
seconds after emptying before the launcher refills itself; 0 is never, and Reload in the panel is the only refill |
LaunchAlongTube |
true |
false throws the round off-axis toward a high off-boresight target |
LaunchLoft |
0.35 |
bias toward the launcher's boresight, and read only when LaunchAlongTube is false |
EjectAwayFromMount |
0 |
m/s pushing the round clear. What a rail and a rack use |
MuzzleOffset |
8 |
m ahead of the tube the round appears |
MuzzleForwardOffset |
0 |
m from the part origin to the tube muzzles, along the boresight |
TubeRingRadius |
0 |
m; the ring the launch positions fall back to when the subpart carrying the tubes will not resolve |
TubeArmamentLabel |
Missiles |
what the panel calls it. A rack says "Bombs" |
FireControlLabel |
DisplayName fire control |
what the panel's fire-control row for this part is called |
MintRows |
true |
false lists the panel's rows for this part exactly as its <Provides> declare them, with no sensor, gun or fire-control row added |
FiresBeam, BeamArmamentLabel |
false, Laser |
fires the beam an <Optic> on the same part carries, and needs no Munition. Refused if no <Optic> in the pack on its part has a <Beam> |
Trainable launchers name the subparts that move. Leave them all out and the launcher is fixed:
Trains is false, the drives are skipped, and fire control never waits for it to settle.
| Default | ||
|---|---|---|
TurretMarker |
none | the assembly that traverses |
PodsMarker, GunsMarker, RadarMarker, OpticBaseMarker |
none | assemblies that ride it |
TurretPivot |
0,0,0 |
where the traverse turns, in part space |
PodPivotFromTurret, GunPivotFromTurret, RadarPivotFromTurret, OpticBaseFromTurret |
0,0,0 |
trunnions, measured from the turret pivot |
PodReferenceElevationDeg, GunReferenceElevationDeg |
0 |
the elevation the mesh was modelled at |
SlewRateDeg, ElevationRateDeg |
70, 45 |
°/s |
SettleSeconds |
0.35 |
how long both axes must be on target before firing |
MinElevationDeg, MaxElevationDeg |
0, 82 |
|
ForwardMinElevationDeg, ForwardArcDeg, ForwardPlateauDeg |
15, 80, 62 |
a raised floor over an arc, so the gear does not swing through its own bodywork |
RestElevationDeg |
modelled pose | where it stows |
SearchRadarRpm, SearchRadarFaces |
20, 1 |
a search array turns off the clock, never off the track |
TraverseLimitDeg |
180 |
° either side of the modelled bearing; under 180 the traverse turns the long way round rather than through what is behind it |
Declaring elevating gear with no trunnion offset is refused: an assembly pivoting about the turret's own centre reads as a pod orbiting the vehicle.
A cannon needs GunMunition and at least one <Muzzle At="x, y, z" />. A launcher may have
tubes, a cannon, or both; the Phalanx has no tubes at all.
| Default | ||
|---|---|---|
GunAmmo |
480 |
belt |
TrimAccelerationMetresPerSecond2, TrimWorstAxisExhaustVelocity |
none | a post-boost bus's attitude nozzles: the acceleration they give the loaded bus, and the exhaust velocity of its worst axis |
HopEngineThrustNewtons, HopEngineExhaustVelocity, HopEngineAccelerationMetresPerSecond2 |
none | a bus's main engine, lit to hop between targets. Without them it never is |
StoreMassKg |
0 |
what one tube round weighs as carried (kg). The part's declared mass should include its full load: each round fired takes this much off it, and a reload puts it back. 0 sheds nothing |
GunRoundMassKg |
0 |
the same for one complete round in the belt, case and charge included |
GunRoundsPerMinute |
2500 |
|
GunBurstRounds, GunBurstGapSeconds |
12, 0.55 |
|
GunReloadSeconds |
0 |
the same for the belt |
GunArmamentLabel |
Cannon |
|
GunSoundId |
the default cannon loop | a <CompoundSound> your asset XML declares, played while the gun fires |
GunshotSoundId |
none | a <Sound> played once per round, for a gun slow enough to be heard shot by shot; alone, it replaces the loop |
MuzzleFlashEffect |
KSArmory's | a <ParticleEmitter> your asset XML declares, at the muzzles while the guns fire |
Gun gear that is drawn only rides the elevating gun and never changes where a round goes. Each marker needs its pin or pivot, measured from the turret pivot, or it is refused:
| Default | ||
|---|---|---|
GunBarrelMarker, GunRecoilMetres, GunRecoilSeconds, GunReturnSeconds |
none, 0, 0.08, 0.6 |
a barrel that runs back as the gun fires and returns |
GunCylinderMarker + GunCylinderPinFromTurret, GunRodMarker + GunRodPinFromTurret |
none | an elevation actuator: a cylinder on the traverse and a rod on the gun, posed from the elevation |
GunFeedMarker + GunFeedPivotFromTurret, GunFeedRatio |
none, 0 |
an ammunition feed turning by that fraction of the elevation |
GunRotorMarker + GunRotorPivotFromTurret, GunRotorSpinUpSeconds, GunRotorSpinDownSeconds |
none, 0.2, 1.0 |
a rotary barrel cluster, spun up to a barrel per round while firing |
A round can be spun as it leaves, drawn only, with a <ReleaseSpin RateDegPerSecond SpinUpSeconds
IgnitionSeconds> holding one <Nozzle Mouth="x, y, z" Exhaust="x, y, z" /> per spin motor, in the
round's own frame: the Heavy MIRV Bus's reentry vehicles light two base motors two seconds out and turn at
half a turn a second.
A launcher declares gear it carries inside itself, with a <Provides> row per assembly that is
a subpart rather than a part of its own. The survey walks parts, so a director on a turret roof is
otherwise reported as not being there at all. The sensor row, the fire-control row and the gun row
are minted from the launcher and must not be written.
| Default | ||
|---|---|---|
Role |
Sensor |
FireControl, Launcher, Sensor, Camera, Gun, Guidance |
DisplayName |
required | what the panel calls that row |
<Provides Role="Camera" DisplayName="1TPP1 director" />
Guidance is the one that flies the craft rather than anything it shoots: a craft carrying the part
is given a ballistic computer, and nothing else is. The built-in MIRV bus declares it.
<Optic> — a sighting head¶
Its own part, needing no weapon on the craft at all: a hull with one director on it is an
observation post. PartId, DisplayName, Sensor, BaseMarker, HeadMarker are required. Give
it a CuedOnly sensor.
An <Optic> on a part of its own gets a camera row in the panel automatically. One on a launcher's
part gets none: the launcher declares it with <Provides Role="Camera">, as the Pantsir does.
| Default | ||
|---|---|---|
Gimbal |
Mast |
Mast elevates over its mounting face; RollNod rolls its whole nose about the pod centreline and nods within it |
HeadPivot |
0,0,0 |
where the head turns, in part space |
RollMarker |
none | required for RollNod, meaningless on a Mast |
EyeForward |
0.30 |
m from the pivot to the aperture |
SlewRateDeg |
90 |
°/s |
MinElevationDeg, MaxElevationDeg |
-20, 85 |
mast heads |
MaxOffBoresightDeg |
135 |
roll-nod heads: the nod stop, which is the only travel limit a rolling head has |
KeyholeDeg |
4 |
cone about the roll axis the aim is held out of. Dead along it there is no roll angle, and a target crossing the nose asks for unbounded roll rate |
MaxMagnification |
16 |
how far the optics wind in, against KSA's 50° view; the zoom steps x1, x2, x4… up to it and ends on it. 53 is a 1° field; the camera takes down to 0.05°, and the most any optic may be fitted with is 1024 |
HasLaser |
false |
a laser rangefinder and designator on the line of sight, fired from the camera window's LASE button; its spot is what a laser-guided weapon homes on, by its code |
LaserRangeMetres |
20000 |
how far the laser returns from; past it the beam lases at nothing |
A <Beam> inside an <Optic> makes its head a high-energy laser, fired down its own line of sight by
a <Launcher FiresBeam="true"> on the same part. An empty <Beam /> is the YAL-1's.
| Default | ||
|---|---|---|
PowerWatts |
1e6 |
|
WavelengthMetres, ApertureMetres |
1.315e-6, 1.5 |
with the aperture, what sets how small the spot can be focused |
JitterRad |
2e-7 |
how far the pointing wanders, smearing the spot |
BurnThroughJoulesPerM2 |
6e7 |
energy per area that burns through one part's whole health |
ExtinctionPerMetre |
2e-5 |
what the air takes out per metre at sea-level density |
ChemicalSeconds, ShotSeconds |
100, 5 |
seconds of lasing in a full load, and how long one press holds it on |
<SearchSet> — a part that searches and does nothing else¶
An IRST bolted to a hull. What it finds goes into its craft's picture, for the directors and a beam
weapon to take. PartId, DisplayName and Sensor are required; it gets a sensor row in the panel.
| Default | ||
|---|---|---|
PanelName |
DisplayName | what the panel's row for the part is called |
HeadMarker |
none | the head that turns behind the window, drawn only: the set sees its whole cone wherever the head points |
HeadPivot |
0,0,0 |
where the head turns, in part space |
HeadTravelDeg |
60 |
how far the head turns off its rest direction |
HeadSlewDegPerSecond |
90 |
|
ScanHalfWidthDeg, ScanPeriodSeconds, ScanNodDeg |
45, 6, 8 |
the figure of eight the head sweeps with nothing to look at |
<Decoy> — what a dispenser throws¶
A flare or a chaff cloud: a point with a signature, falling through the air. Name, DisplayName
and Kind (Flare or Chaff) are required. A flare fools infrared seekers, chaff radar ones.
| Default | ||
|---|---|---|
PeakSignature |
0 |
a flare's in kW/sr of heat; chaff's in m² of radar return, on the scale where a craft of 11 m mean radius reads 400 |
RiseSeconds |
0 |
how long it takes to reach its peak |
LifeSeconds |
4 |
when it has faded to nothing |
MassKg |
0 |
what one weighs, taken off the dispenser's part as it is thrown |
DragPerMetre |
0 |
how quickly the air stops it |
<Dispenser> — a part that throws decoys¶
PartId, DisplayName and Decoy are required. It gets a countermeasures row in the panel. Its
kind is its decoy's; Kind may be stated too, and is refused if the two disagree. Every dispenser of
one kind on a craft throws on each press.
| Default | ||
|---|---|---|
Count |
30 |
decoys loaded |
SalvoSize, SalvoIntervalSeconds |
2, 0.25 |
how many one press throws, and how far apart |
EjectDirection |
1,0,0 |
which way they leave, in part space |
EjectSpeed |
30 |
m/s |
Effects of your own¶
A round's burst and plume and a gun's muzzle flash are KSA assets, so a pack brings its own look the way
it brings a sound: declare an <Explosion> or a <ParticleEmitter> in an asset file listed in
mod.toml, under an Id of your own, and name it with BurstEffect, PlumeEffect or
MuzzleFlashEffect. KSArmory's own, in its KSArmoryParticles.xml, are the examples to start from.
An Id no mod declares is reported in KSArmory.log when the world loads, and the weapon keeps
KSArmory's effect rather than losing it.
Bodies.xml -- describing a planet¶
A solar-system mod, or any mod, can say what a nuclear burst needs to know about a body that KSA does
not declare. Put a file with a <Bodies> root in the same KSArmory/ folder; it is told from a weapon
pack by its root element, and read in every enabled mod.
<Bodies>
<Body Id="MyMoon" FieldTesla="5e-6" FieldTiltDeg="10" Airglow="CarbonDioxide" Gamma="1.3"
Condensation="false" Surface="true" XRayOpacity="1" />
</Bodies>
| Attribute | Meaning | When left out |
|---|---|---|
Id |
the body's Id, as KSA's system declares it | required |
FieldTesla |
dipole strength at the surface equator (T), for a body KSA declares no field on; KSA's own wins where it has one | no field: no aurora, no debris held along it |
FieldTiltDeg, FieldAzimuthDeg |
the dipole's tilt from the spin axis, and toward which longitude | 0 |
Airglow |
OxygenNitrogen, CarbonDioxide, Hydrogen or Neutral |
Neutral: a dim, uncoloured glow |
Condensation |
whether a burst raises white condensation | follows whether the body has an ocean |
Gamma |
the air's ratio of specific heats, for the speed of sound | 1.4 |
Surface |
whether there is ground at all | true |
XRayOpacity |
X-ray stopping per kilogram, against nitrogen and oxygen's | 1 |
Everything about the air itself -- pressure, density, scale height, where it ends -- comes from the body's own atmosphere in KSA and is never restated here. A later mod's entry for a body replaces an earlier one's, with a line in the log saying so; once the world has loaded, one line lists every entry the solar system has no body for, which is where a misspelt or renamed Id shows up.
Names, and using somebody else's round¶
Every name your pack declares is filed under your pack. Name="AIM-9X" in MyWeaponPack
registers as MyWeaponPack:AIM-9X, so a pack that ships an AIM-9X and a pack that ships another
are two rounds rather than one silently beating the other.
Inside your own file, bare means yours:
<Launcher … Munition="AIM-9X" Sensor="Seeker9X">
To use something else, qualify it. KSArmory: is the built-ins:
<Launcher PartId="MyWeaponPack_Prefab_Gun" DisplayName="My mount"
Munition="KSArmory:20MM" Sensor="Seeker9X" GunMunition="KSArmory:20MM">
<Muzzle At="0, 0.1, 1.2" />
</Launcher>
That is a whole launcher and no munition at all. The built-in keys are 57E6, 30MM, AIM9J,
AIM120C, AGM88, 20MM, B61, MK21, and the sensors 1RS1, AIM9SEEK,
AIM120SEEK, AGM88SEEK, VPS2, BOMBSIGHT, MIRVBUS, EO, Litening.
A name that resolves to nothing is refused when the file is read, not at the first shot. So is a name something else already claims — including your own round, if you register the same pack twice. That case matters more than it looks: the name is then in the catalogue carrying a different profile, so a launcher naming it would load, fly, and throw a weapon you never shipped.
When something does not appear¶
Before launching the game, run KSArmory's validator over your folder:
./tools/validate-parts.py --mod-root ../MyWeaponPack # add --offline without a KSA install
It checks what the game would otherwise fail on silently: mod.toml listing a file that is not
there or a weapon file it should not, a texture or mesh path that differs only in case, a PartId
no <Part> declares, a drive marker matching no subpart or more than one, and a round's body or fin
marker matching nothing on the launcher that carries it. It does not judge attributes; KSArmory's
reader does that in game, and the log below is where it says so.
Everything refused is written to <KSA user dir>/Logs/KSArmory.log, one line per fault, naming
your pack, the definition and what was wrong. Check in this order:
- Is the mod enabled? KSA writes a newly discovered mod into
manifest.tomlwithenabled = falseand says nothing about it, so dropping the folder in is not enough. KSArmory warns about a pack it can see whose mod is switched off, so this one at least announces itself. - Is the file in a
KSArmory/folder inside your mod folder? That folder is how it is found; the file's own name is yours, and every.xmlin there is read. A pack KSArmory never mentions at all is one it never found. - Is your part in the editor? If not, the problem is your
Assets.xml, not KSArmory.
Once the world has loaded, KSArmory checks each registered part against what actually exists and
warns about two things the log will name: a PartId nothing declared, and a marker matching no
subpart or more than one. The second is worth reading twice — resolution is a case-insensitive
substring taking the first hit, so PodsMarker="Pods" against subparts Rail_Pods and
Rail_PodsCover drives whichever your XML happens to list first, and reordering it changes which
assembly moves.
Traps that are not yours, and will still bite you¶
- Never remove a
<SubPart>from a shipped part. KSA pairs a saved part with its current definition positionally, so removing one throws out ofPopup.DrawAlland terminates the game on every save holding that part. Adding and renaming are free. Leave a stub to hold the count. - Markers match by case-insensitive substring, first hit wins. A marker
Missilealso matches a subpart calledMissileRail. Name subparts so no marker is a prefix of another. - Do not name your part instances. Recognition matches
Part.Id, which is the instance name when there is one and the template Id only when there is not. - Case matters on Linux. A texture path that differs only in case loads on Windows and fails elsewhere.
What a pack cannot do¶
Data covers a great deal: every flight, warhead, sensor and optic number, tube positions and directions, magazine and belt, articulation markers, and which round and sensor a launcher pairs with. What it cannot do is add a kind:
- a new weapon kind — a hitscan, anything without a discrete round other than a beam.
- a new guidance law, as opposed to new numbers for one of the five.
- a new gimbal, beyond
MastandRollNod. - articulation beyond traverse-then-elevate. A drum, a translating rail, per-tube motion or a radar that trains independently of the turret has no expression here. Gear drawn on an elevating gun -- a recoiling barrel, an actuator, a feed, a barrel cluster -- does, above.
Those are C# changes to KSArmory itself. If you want one, MODULARITY.md is the argument about
what each would cost and EXTENSIBILITY.md is where a registration seam for them would go.
Porting from a KSP weapon mod meets most of these, and PACK-PORTING.md is the plan for the ones
this side can close.
Armour and penetration are not expressible either. Parts take damage one by one and keep their health between hits, but every part takes a hit alike: a pack can set how big a charge is, not what a part is made of.
One more limit is KSA's rather than ours: there is no part module, so a part cannot say what it is — KSArmory recognises it by part Id, which is why the Ids in your two files have to agree.
Not built yet¶
Named so you do not go looking:
- No in-game list. Refusals are in the log; there is no panel window showing them.