Skip to content

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.md 1.3 records what that flight did and did not reach. The one thing named at the end is not built. KSArmory.Armoury.Schema is 1.

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.4 is 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 Deg is 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:

  1. Is the mod enabled? KSA writes a newly discovered mod into manifest.toml with enabled = false and 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.
  2. 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 .xml in there is read. A pack KSArmory never mentions at all is one it never found.
  3. 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 of Popup.DrawAll and 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 Missile also matches a subpart called MissileRail. 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 Mast and RollNod.
  • 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.