ItemGunAsset — The Weapon Definition
ItemGunAsset is the asset definition for all ranged weapons in Unturned. At 1,442 lines it is the largest item asset class in the codebase. It inherits from ItemWeaponAsset and covers fire rate, recoil, spread, ballistic projection, damage falloff, attachment compatibility, magazine management, jamming, turret support, camera shake, audio, and quest reward integration. A gun asset is deserialized from a .dat file inside an asset bundle via PopulateAsset. The class also provides BuildDescription for the inventory tooltip and BuildCargoData for wiki export.
Source code location: Unturned/Bundles/ItemGunAsset.cs
Inheritance Chain
ItemAsset
→ ItemWeaponAsset
→ ItemGunAssetItemWeaponAsset provides the base damage tables (playerDamageMultiplier, zombieDamageMultiplier, animalDamageMultiplier, barricadeDamageMultiplier, structureDamageMultiplier, vehicleDamageMultiplier, resourceDamageMultiplier, objectDamageMultiplier), range, and the BuildNonExplosiveDescription / BuildExplosiveDescription helpers that ItemGunAsset calls. The ItemWeaponAsset base also provides quality tracking, the shouldFriendlySentryTargetUser default (true for guns), and quest reward infrastructure.
Attachment System
Guns support up to four attachment slots encoded in the item state byte array (18 bytes). The slots and their byte offsets are:
| Offset | Field | Type | Description |
|---|---|---|---|
| 0-1 | Sight ID | ushort | Attached sight item's legacy ID |
| 2-3 | Tactical ID | ushort | Attached tactical item's legacy ID |
| 4-5 | Grip ID | ushort | Attached grip item's legacy ID |
| 6-7 | Barrel ID | ushort | Attached barrel item's legacy ID |
| 8-9 | Magazine ID | ushort | Current magazine's legacy ID |
| 10 | Ammo count | byte | Current ammo in the magazine (0-255) |
| 11 | Firemode | byte | Current firemode (EFiremode value) |
| 12 | Interact state | byte | Interaction state (e.g., safety toggle) |
| 13 | Sight quality | byte | Sight attachment durability (0-100) |
| 14 | Tactical quality | byte | Tactical attachment durability |
| 15 | Grip quality | byte | Grip attachment durability |
| 16 | Barrel quality | byte | Barrel attachment durability |
| 17 | Magazine quality | byte | Magazine durability |
The default getState method constructs this 18-byte array using getMagazineState(GetDefaultMagazineLegacyId()) for the magazine bytes. The ammo count defaults to ammoMax for admin/adventure origins, or random between ammoMin and ammoMax adjusted by the game mode's Gun_Bullets_Multiplier for world-spawned items. The firemode byte is set from the weapon's initial firemode field.
csharp
public override byte[] getState(EItemOrigin origin)
{
byte[] magazineState = getMagazineState(GetDefaultMagazineLegacyId());
return new byte[18]
{
sightState[0], sightState[1],
tacticalState[0], tacticalState[1],
gripState[0], gripState[1],
barrelState[0], barrelState[1],
magazineState[0], magazineState[1],
origin != EItemOrigin.WORLD || Random.value < (Provider.modeConfigData != null
? Provider.modeConfigData.Items.Gun_Bullets_Full_Chance : 0.9f)
? ammoMax
: (byte)Mathf.CeilToInt(Random.Range(ammoMin, ammoMax + 1)
* (Provider.modeConfigData != null
? Provider.modeConfigData.Items.Gun_Bullets_Multiplier : 1.0f)),
(byte)firemode,
1, // interact state
100, // sight quality
100, // tactical quality
100, // grip quality
100, // barrel quality
100 // magazine quality
};
}There is also an overload that accepts explicit IDs for all five attachment slots and an ammo count:
csharp
public byte[] getState(ushort sight, ushort tactical, ushort grip, ushort barrel,
ushort magazine, byte ammo)Attachment Hook Detection
Attachment hooks are detected by the presence of Hook_Sight, Hook_Tactical, Hook_Grip, and Hook_Barrel keys in the .dat:
csharp
hasSight = p.data.ContainsKey("Hook_Sight");
hasTactical = p.data.ContainsKey("Hook_Tactical");
hasGrip = p.data.ContainsKey("Hook_Grip");
hasBarrel = p.data.ContainsKey("Hook_Barrel");If a hook key is absent, that slot does not exist on the weapon and no attachment can be placed. The gun's built-in sightID, tacticalID, gripID, and barrelID fields define the default attachments (what the gun spawns with). These are loaded as simple ushort values from the .dat:
csharp
sightID = p.data.ParseUInt16("Sight");
tacticalID = p.data.ParseUInt16("Tactical");
gripID = p.data.ParseUInt16("Grip");
barrelID = p.data.ParseUInt16("Barrel");Each ID property has a setter that also updates the corresponding byte array cache used in getState:
csharp
public ushort sightID
{
get => _sightID;
set
{
_sightID = value;
sightState = BitConverter.GetBytes(sightID);
}
}Attachment Caliber Filtering
The gun can restrict attachments by caliber. Two arrays are populated during PopulateAsset:
magazineCalibers: Caliber IDs that magazines must match. Loaded fromMagazine_Calibers(count) +Magazine_Caliber_N(individual IDs).attachmentCalibers: Caliber IDs that sight/tactical/grip/barrel attachments must match. Loaded fromAttachment_Calibers+Attachment_Caliber_N.
If Magazine_Calibers is absent, a single Caliber value is used for both arrays. If Attachment_Calibers is absent but Magazine_Calibers is present, attachmentCalibers defaults to magazineCalibers:
csharp
int magazineCaliberCount = p.data.ParseInt32("Magazine_Calibers");
if (magazineCaliberCount > 0)
{
magazineCalibers = new ushort[magazineCaliberCount];
for (int caliberIndex = 0; caliberIndex < magazineCaliberCount; caliberIndex++)
magazineCalibers[caliberIndex] = p.data.ParseUInt16("Magazine_Caliber_" + caliberIndex);
int attachmentCaliberCount = p.data.ParseInt32("Attachment_Calibers");
if (attachmentCaliberCount > 0)
{
attachmentCalibers = new ushort[attachmentCaliberCount];
for (int caliberIndex = 0; caliberIndex < attachmentCaliberCount; caliberIndex++)
attachmentCalibers[caliberIndex] = p.data.ParseUInt16("Attachment_Caliber_" + caliberIndex);
}
else
{
attachmentCalibers = magazineCalibers;
}
}
else
{
magazineCalibers = new ushort[1];
magazineCalibers[0] = p.data.ParseUInt16("Caliber");
attachmentCalibers = magazineCalibers;
}The requiresNonZeroAttachmentCaliber flag (default false) forces attachments to specify at least one non-zero caliber. This is used to block vanilla attachments (which have caliber 0) from modded guns that require specific calibers.
BuildDescription Attachment Display
BuildDescription reads the 18-byte state and resolves each attachment slot:
csharp
ushort instanceSightId = BitConverter.ToUInt16(itemInstance.state, 0);
ItemSightAsset sightAsset = Assets.find(EAssetType.ITEM, instanceSightId) as ItemSightAsset;If the attached item has a custom itemName, that is displayed with the item's rarity color. Otherwise, the weapon's own itemName is used. Empty slots show "None." Each slot only displays if the hook key is present or the attachment differs from the default.
Magazine System
Magazines use a multi-level resolution system with per-level overrides. The SelectDefaultMagazine() method implements the full chain:
- Default magazine: Set via
Magazinekey (GUID + legacy ID pair stored indefaultMagazineGuid/defaultMagazineLegacyId). - Magazine replacements: The
Magazine_Replacementsinteger defines N replacement entries. Each has:Magazine_Replacement_N_ID: GUID + legacy ID of the replacement magazine.Magazine_Replacement_N_Map: Level name (case-sensitive).
- Spawn table resolution: If the resolved asset is a
SpawnAsset(spawn table) rather than a directItemMagazineAsset,SpawnTableTool.Resolvepicks a random entry.
csharp
public ItemMagazineAsset SelectDefaultMagazine()
{
bool replaced = false;
Asset asset = null;
if (Level.info != null && magazineReplacements != null)
{
foreach (MagazineReplacement magazineReplacement in magazineReplacements)
{
if (magazineReplacement.map == Level.info.name)
{
asset = Assets.FindByGuidOrLegacyId(magazineReplacement.guid,
EAssetType.ITEM, magazineReplacement.legacyId);
replaced = true;
break;
}
}
}
if (!replaced)
asset = Assets.FindByGuidOrLegacyId(defaultMagazineGuid,
EAssetType.ITEM, defaultMagazineLegacyId);
if (asset is SpawnAsset spawnAsset)
asset = SpawnTableTool.Resolve(spawnAsset, EAssetType.ITEM,
OnGetDefaultMagazineSpawnTableErrorContext);
return asset as ItemMagazineAsset;
}The MagazineReplacement struct:
csharp
public struct MagazineReplacement
{
public string map; // Level name to match
public ushort legacyId; // Legacy ID fallback
public Guid guid; // GUID
}Magazine State Management
The getState method includes a call to getMagazineState() which converts the magazine's legacy ID to a 2-byte array. The magazine ID is stored at bytes 8-9 of the weapon state. This allows the weapon to reference any magazine, not just its default — the magazine can be swapped by changing these bytes.
Fire Rate
The firerate field is a single byte. The conversion to rounds per second is implemented in CalculateRoundsPerSecond():
csharp
internal float CalculateRoundsPerSecond()
{
return 50.0f / Mathf.Max(1, firerate + 1);
}firerate value | RPS | RPM |
|---|---|---|
| 0 | 50.0 | 3000 |
| 1 | 25.0 | 1500 |
| 5 | 8.33 | 500 |
| 10 | 4.54 | 273 |
| 20 | 2.38 | 143 |
The RPM is displayed in the inventory description as Mathf.RoundToInt(roundsPerSecond * 60.0f).
Action Types (EAction)
The action field (type EAction) defines the weapon's mechanical behavior — it determines animations, firing loops, reload behavior, casing ejection, and empty magazine handling:
| Action | Behavior | Delete Empty Mags | Default Casing | Firerate Note |
|---|---|---|---|---|
Trigger | Semi-automatic, single shot per trigger pull | No | Casing | Standard semi-auto |
Pump | Requires manual rechambering after each shot | Yes | Shell | Shotgun pump action |
Bolt | Requires manual bolt pull after each shot | Yes | Casing | Sniper bolt action |
Rail | Railgun — no physical casing ejection | Yes | None | Electromagnetic |
String | Bow/crossbow — uses draw animation | Yes | None | Short rolloff (16m) |
Rocket | Launcher — fires rocket projectile | Yes | None | Long rolloff (64m) |
Break | Break-action — opens breech to load | Yes | Shell | All casings eject on reload |
Minigun | Spool-up rotary — must aim to shoot | No | Casing | Continuous fire |
The shouldDeleteEmptyMagazines default varies by action: true for Pump, Rail, String, Rocket, Break; false otherwise. The Should_Delete_Empty_Magazines key can override. The legacy Delete_Empty_Magazines key is also supported for backwards compatibility.
Firemode Selection
The gun defines which firemodes are available via boolean flags:
hasSafety/hasSemi/hasAuto— Corresponds toSafety,Semi,Autokeys.hasBurst— Derived frombursts > 0.bursts— Number of shots per burst (stored asint).
The initial firemode (type EFiremode) is set in priority order: Auto → Semi → Burst → Safety:
csharp
if (hasAuto) firemode = EFiremode.AUTO;
else if (hasSemi) firemode = EFiremode.SEMI;
else if (hasBurst) firemode = EFiremode.BURST;
else if (hasSafety) firemode = EFiremode.SAFETY;The EFiremode enum values: SAFETY = 0, SEMI = 1, AUTO = 2, BURST = 3. The player can toggle between available firemodes by pressing the firemode key (default F).
Ammo and Fire Delay
ammoMin / ammoMax are both byte values loaded from Ammo_Min / Ammo_Max. They define the spawn ammo range. ammoPerShot (default 1) is how many bullets each shot consumes. infiniteAmmo prevents ammo consumption entirely (used for turrets).
fireDelay is an integer count of simulation ticks (32 ticks/sec) to wait after input before the weapon fires:
csharp
fireDelay = Mathf.RoundToInt(p.data.ParseFloat("Fire_Delay_Seconds") * PlayerInput.TOCK_PER_SECOND);When the input is pressed but the delay is active, the fireDelaySound AudioClip plays. After the delay expires, the weapon fires once and resets. This is used for minigun spool-up and charge-shot weapons. MustAimToShoot (defaults to true for miniguns) requires the player to be aiming before firing.
Spread (Accuracy)
Spread is defined as a base angle in radians and modulated by stance-specific multipliers:
| Field | Default | .dat Key | Description |
|---|---|---|---|
baseSpreadAngleRadians | — | Spread_Angle_Degrees or Spread_Hip | Base spread cone half-angle in radians |
spreadAim | — | Spread_Aim | Multiplier while aiming down sights |
spreadSprint | 1.25 | Spread_Sprint | Multiplier while sprinting |
spreadCrouch | 0.85 | Spread_Crouch | Multiplier while crouched |
spreadProne | 0.7 | Spread_Prone | Multiplier while prone |
spreadSwimming | 1.1 | Spread_Swimming | Multiplier while swimming |
spreadMidair | 1.5 | Spread_Midair | Multiplier while not grounded |
The actual spread angle for a given shot is computed as baseSpreadAngleRadians * stanceMultiplier. Additional spread is added per consecutive shot (bloom) based on the weapon's configuration.
The field baseSpreadAngleRadians was added in a 2022 refactor. The legacy spreadHip field is marked [Obsolete] and represents the old tangent-based spread. During PopulateAsset:
csharp
if (p.data.ContainsKey("Spread_Angle_Degrees"))
{
baseSpreadAngleRadians = Mathf.Deg2Rad * p.data.ParseFloat("Spread_Angle_Degrees");
spreadHip = Mathf.Tan(baseSpreadAngleRadians);
}
else
{
spreadHip = p.data.ParseFloat("Spread_Hip");
baseSpreadAngleRadians = Mathf.Atan(spreadHip);
}The -LogGunSpreadConversion command line flag logs the conversion for debugging.
Recoil
Recoil is defined as a random range per axis, with recovery and stance multipliers.
Per-Shot Recoil
| Field | .dat Key | Description |
|---|---|---|
recoilMin_x / recoilMin_y | Recoil_Min_X / Recoil_Min_Y | Minimum horizontal/vertical recoil per shot |
recoilMax_x / recoilMax_y | Recoil_Max_X / Recoil_Max_Y | Maximum horizontal/vertical recoil per shot |
The actual recoil applied each shot is a random value between min and max: Random.Range(recoilMin_x, recoilMax_x). Recoil accumulates while firing (each shot adds more recoil without resetting) and recovers toward zero at rates defined by recover_x and recover_y.
Recoil Recovery
| Field | .dat Key | Description |
|---|---|---|
recover_x | Recover_X | Horizontal recovery rate (units/sec toward zero) |
recover_y | Recover_Y | Vertical recovery rate (units/sec toward zero) |
Stance Multipliers
| Field | Default | .dat Key |
|---|---|---|
aimingRecoilMultiplier | 1.0 | Aiming_Recoil_Multiplier |
recoilSprint | 1.25 | Recoil_Sprint |
recoilCrouch | 0.85 | Recoil_Crouch |
recoilProne | 0.7 | Recoil_Prone |
recoilSwimming | 1.1 | Recoil_Swimming |
recoilMidair | 1.0 | Recoil_Midair |
Camera Shake
Screen shake on firing is defined as random ranges per axis:
| Field | .dat Key |
|---|---|
shakeMin_x / shakeMax_x | Shake_Min_X / Shake_Max_X |
shakeMin_y / shakeMax_y | Shake_Min_Y / Shake_Max_Y |
shakeMin_z / shakeMax_z | Shake_Min_Z / Shake_Max_Z |
Ballistic Projection
Ballistics simulate bullet travel in discrete steps at a fixed rate of 32 ticks per second. Each ballistic step covers ballisticTravel meters, and the bullet travels through ballisticSteps steps before expiring:
| Field | .dat Key | Description |
|---|---|---|
ballisticSteps | Ballistic_Steps | Number of simulation steps per shot (byte) |
ballisticTravel | Ballistic_Travel | Distance per step in meters (float) |
muzzleVelocity | Computed | ballisticTravel * PlayerInput.TOCK_PER_SECOND (32 ticks/sec) |
bulletGravityMultiplier | Bullet_Gravity_Multiplier or Ballistic_Drop | Gravity scale applied to bullet drop (default 4.0) |
ballisticForce | Ballistic_Force | Impulse applied to physics objects hit by bullets (default 0.002) |
Range Calculation
The effective range is ballisticSteps * ballisticTravel. During PopulateAsset, this is validated against the weapon's range (from ItemWeaponAsset):
csharp
float testRange = ballisticSteps * ballisticTravel;
if (Mathf.Abs(testRange - range) > 0.1f)
Assets.ReportError(this, "range and manual ballistic range are mismatched by "
+ rangeError + "m. Recommended to only have one or the other specified!");If only one of Ballistic_Steps or Ballistic_Travel is specified, the other is derived to match the weapon's range:
csharp
if (hasBallisticSteps && hasBallisticTravel) { /* validate */ }
else if (hasBallisticSteps)
ballisticTravel = range / ballisticSteps;
else if (hasBallisticTravel)
ballisticSteps = (byte)Mathf.CeilToInt(range / ballisticTravel);
else
{
ballisticTravel = 10.0f;
ballisticSteps = (byte)Mathf.CeilToInt(range / ballisticTravel);
}Bullet Drop
The bulletGravityMultiplier field scales the standard physics gravity (-9.81 m/s²) for bullet drop. A value of 4.0 produces 4x normal bullet drop. A value of 0.0 disables bullet drop entirely.
Backwards compatibility with the legacy Ballistic_Drop property is handled by simulating the old drop algorithm and solving for the equivalent gravity multiplier:
csharp
// Old algorithm:
heightDelta += direction.y * ballisticTravel;
direction.y -= ballisticDrop;
direction.Normalize();
// Equivalent gravity:
float totalDeltaTime = ballisticSteps * UseableGun.BALLISTICS_DELTA_TIME;
float gravity = (2.0f * heightDelta) / (totalDeltaTime * totalDeltaTime);
bulletGravityMultiplier = gravity / -9.81f;The -LogBallisticDropConversion command line flag logs the conversion for debugging.
Damage Falloff
Guns can define a damage falloff curve over distance:
| Field | Default | .dat Key | Description |
|---|---|---|---|
damageFalloffRange | 1.0 | Damage_Falloff_Range | Fraction of max range where falloff begins |
damageFalloffMaxRange | 1.0 | Damage_Falloff_Max_Range | Fraction of max range where falloff finishes |
damageFalloffMultiplier | 1.0 | Damage_Falloff_Multiplier | Damage multiplier at max falloff range |
For example, damageFalloffRange=0.5, damageFalloffMaxRange=0.8, damageFalloffMultiplier=0.3 means:
- Damage is 100% out to 50% of max range.
- From 50% to 80% range, damage linearly decreases from 100% to 30%.
- Beyond 80% range, damage stays at 30%.
The description UI renders this as a range string:
csharp
string start = MeasurementTool.FormatLengthString(range * damageFalloffRange);
string end = MeasurementTool.FormatLengthString(range * damageFalloffMaxRange);
builder.Append(localization.format("ItemDescription_DamageFalloff", start, end,
$"{damageFalloffMultiplier:P}"));Projectile and Explosive Weapons
If the gun has a Projectile prefab (_projectile != null), it fires physics projectiles instead of hitscan bullets. Key projectile fields:
| Field | .dat Key | Default | Description |
|---|---|---|---|
projectileLifespan | Projectile_Lifespan | 30.0 | Seconds before projectile is destroyed |
projectilePenetrateBuildables | Projectile_Penetrate_Buildables | false | Whether projectiles pass through constructions |
muzzleGuid / muzzle | Muzzle | — | Effect spawned at the muzzle on firing |
shellGuid / shell | Shell | Action-dependent | Effect for ejected shell casings |
projectileExplosionEffectGuid / explosion | Explosion | — | Explosion effect on projectile impact |
projectileExplosionLaunchSpeed | Projectile_Explosion_Launch_Speed | playerDamage * 0.1 | Physics launch on explosion |
Shell defaults vary by action:
- Pump / Break: Shell GUID (
0dc9bf936ce0409585fe9525287c7a7d). - Rail: No shell (null).
- All others: Casing GUID (
f380a6a6f41f422c9f5b9ac13e3b13e8).
Explosive Description
When _projectile != null, BuildDescription calls BuildExplosiveDescription which shows:
- Explosive warning text.
- Blast radius.
- Damage per entity type (player, zombie, animal, barricade, structure, vehicle, resource, object).
When there is no projectile, BuildNonExplosiveDescription shows damage per limb for hitscan bullets.
Reload and Rechamber
| Field | .dat Key | Default | Description |
|---|---|---|---|
reloadTime | Reload_Time | — | Seconds for the reload animation |
hammerTime | Hammer_Time | — | Seconds for the hammer animation |
unplace | Unplace | — | Time to detach magazine |
replace | Replace | 1.0 | Multiplier for re-attach time |
RechamberAfterShotCount | RechamberAfterShotCount | 1 for Pump/Bolt | Shots before hammer plays |
RechamberAfterShotDelay | RechamberAfterShotDelay | 0.25s | Delay before hammer after shot |
EjectAfterHammerDelay | EjectAfterHammerDelay | 0.45s | Delay before casing eject |
EjectAfterReloadDelay | EjectAfterReloadDelay | 0.5s | Delay before casing eject on reload |
CasingEjectCountAfterRechamberingAfterShooting | CasingEjectCountAfterRechamberingAfterShooting | 1 | Casing particles after shot |
CasingEjectCountAfterReload | CasingEjectCountAfterReload | 0 (ammoMax for Break) | Casing particles on reload |
ShouldEjectCasingAfterShooting | EjectCasingAfterShooting | True for Trigger/Minigun | Whether to eject on shot |
Magazine Rechambering
Two ERechamberGunAfterReloadMode values control hammer animation playback after magazine insertion or removal:
| Field | Default | Description |
|---|---|---|
RechamberAfterMagazineAttached | IfAmmoWasEmpty | Hammer plays when magazine is inserted and chamber was empty |
RechamberAfterMagazineDetached | Always | Hammer plays when magazine is removed |
csharp
public enum ERechamberGunAfterReloadMode
{
IfAmmoWasEmpty, // Default. Plays "Hammer" if ammo count was zero.
Never, // Does not play "Hammer" after reloading.
Always, // Will play "Hammer" after reloading regardless.
}Jamming
| Field | .dat Key | Default | Description |
|---|---|---|---|
canEverJam | Can_Ever_Jam | false | Whether jamming is possible |
jamQualityThreshold | Jam_Quality_Threshold | 0.4 | Quality % below which jamming can start |
jamMaxChance | Jam_Max_Chance | 0.1 | Max jam probability at 0% quality |
unjamChamberAnimName | Unjam_Chamber_Anim | "UnjamChamber" | Animation to unjam |
The chance of jamming is linearly interpolated between 0% at jamQualityThreshold and jamMaxChance at 0% quality. For example, at 20% quality with default values: jamChance = 10% × (0.4 - 0.2) / 0.4 = 5%.
The _chamberJammedSound AudioClip plays when a jam occurs. The player must play the unjam animation to clear the jam before firing again.
Turret Support
Guns used as vehicle turrets have additional properties:
| Field | .dat Key | Description |
|---|---|---|
isTurret | Turret | True if this gun is used on a turret mount |
driverTurretViewmodelMode | DriverTurretViewmodelMode | Controls first-person arm visibility |
csharp
internal enum EDriverTurretViewmodelMode
{
OffscreenWhileAiming, // Default. Pushes arms off-screen while aiming.
AlwaysOffscreen, // Push arms off-screen when equipped.
AlwaysOnscreen, // Arms always visible.
}Turrets also commonly use infiniteAmmo = true since vehicle turrets don't track ammo per-passenger. The ammoPerShot field controls multi-ammo consumption for turret shotguns.
Audio
Up to seven AudioClip references on the gun asset:
| Field | .dat Fallback | Bundle Asset | Description |
|---|---|---|---|
_shoot | ShootAudioClip | "Shoot" | Primary firing sound |
_reload | ReloadAudioClip | "Reload" | Magazine change sound |
_hammer | HammerAudioClip | "Hammer" | Rechambering sound |
_aim | AimAudioClip | "Aim" | ADS transition sound |
_minigun | MinigunAudioClip | "Minigun" | Spool-up continuous sound |
_chamberJammedSound | ChamberJammedAudioClip | "ChamberJammed" | Jam notification |
fireDelaySound | FireDelayAudioClip | "FireDelay" | Fire delay warning |
Audio loading uses LoadRedirectableAsset<AudioClip> which checks the .dat key first and falls back to the bundle asset:
csharp
_shoot = LoadRedirectableAsset<AudioClip>(p.bundle, "Shoot", p.data, "ShootAudioClip");
_reload = LoadRedirectableAsset<AudioClip>(p.bundle, "Reload", p.data, "ReloadAudioClip");Gunshot Rolloff
gunshotRolloffDistance controls how far the gunshot can be heard. Defaults vary by action:
| Action | Default Rolloff |
|---|---|
String | 16m |
Rocket | 64m |
| All others | 512m |
The Gunshot_Rolloff_Distance key can override the default. Barrel attachments multiply this distance via gunshotRolloffDistanceMultiplier.
Alert Radius
alertRadius (default 48) controls the radius in which zombies are alerted to the gunshot.
Inventory Audio
GetDefaultInventoryAudio returns different sounds for small (≤ 2×2 grid) vs large guns. Small guns use SmallGunAttachment.asset; large guns use LargeGunAttachment.asset. Bows get the base item inventory audio.
AIM and Movement
| Field | .dat Key | Default | Description |
|---|---|---|---|
aimInDuration | Aim_In_Duration | 0.2s | Seconds from pressing aim to full ADS |
shouldScaleAimAnimations | Scale_Aim_Animation_Speed | true | Scale Aim_Start/Aim_Stop by aim duration |
canAimDuringSprint | Can_Aim_During_Sprint | false | Allow ADS while sprinting |
aimingMovementSpeedMultiplier | Aiming_Movement_Speed_Multiplier | 0.75 or 1.0 | Movement speed while ADS |
ShouldForceStopAimingAfterShooting | Stop_Aiming_After_Shooting | false | Stop aiming after each shot |
The aimingMovementSpeedMultiplier defaults to 1.0 if canAimDuringSprint is true, otherwise 0.75.
Quest Rewards
The gun can grant quest rewards when fired via shootQuestRewards. This is a NPCRewardsList parsed from Shoot_Quest_Rewards / Shoot_Quest_Reward_N.
csharp
public void GrantShootQuestRewards(Player player)
{
shootQuestRewards.Grant(player);
}Other Fields
| Field | .dat Key | Default | Description |
|---|---|---|---|
instakillHeadshots | Instakill_Headshots | false | Instantly kill players with headshots (only when game config enables) |
allowMagazineChange | Allow_Magazine_Change | true | Can the magazine be changed by the player |
rangeRangefinder | Range_Rangefinder | Range value | Overrides rangefinder attachment's maximum distance |
shouldFriendlySentryTargetUser | Hardcoded | true | Guns always cause sentries to target the user |
The UseableGun Layer
The UseableGun class (separate file) drives runtime weapon behavior:
- Firing loop: On input, checks firemode. For SEMI: fires once. For AUTO: fires continuously at firerate intervals. For BURST: fires
burstsshots in sequence. - Ballistic simulation: For hitscan weapons,
UseableGun.BALLISTICS_DELTA_TIME= 0.02s per step. The raycast direction includes spread and recoil offsets. Each ballistic step checks for collision. - Ammo consumption: Each shot consumes
ammoPerShotbullets. IfinfiniteAmmo, no consumption. - Reload: Triggers when ammo reaches 0 and a magazine is available. Plays reload animation, updates state bytes.
- Attachment integration: On equip, the weapon's attachment slots are parsed from state bytes. The corresponding attachment prefabs are instantiated and parented to the weapon model.
- Network synchronization: Firing, reloading, ammo count, firemode, and attachment state are all replicated.
Cargo Data Export
BuildCargoData writes to five Cargo tables:
- Gun table: All stats, flags, and configuration values.
- Gun_AttachmentCaliber child table: Per-index caliber entries.
- Gun_MagazineCaliber child table: Per-index magazine caliber entries.
- Gun_MagazineReplacement child table: Per-map magazine override entries.
- Base class tables for
WeaponandItem.
The cargo export covers over 70 fields including all ballistic, recoil, spread, damage, audio, firemode, attachment, magazine, and quality configuration values.
Common Issues
- Spread conversion — The switch from
Spread_Hip(tangent) toSpread_Angle_Degrees(angle) changes the effective spread curve. Converted values may not match visually. Enable-LogGunSpreadConversionto audit. - Ballistic range mismatch — If
ballisticSteps * ballisticTraveldiffers fromrangeby > 0.1m, a warning is logged but no fix is applied. The actual ballistic range and the intended range may differ. - Attachment quality — Quality bytes for attachments (offsets 13-17) are initialized to 100 but are not automatically decreased — the game mode config controls attachment wear. Zero quality attachments provide no stat benefits.
- Fire delay tick rounding — The fire delay uses
Mathf.RoundToIntonFire_Delay_Seconds * 32. A delay of 0.03s rounds to 1 tick (0.03125s); 0.02s rounds to 1 tick as well. Fine-tuning below 1 tick is impossible. - Caliber matching logic —
CalibersContainAnyOfIdschecks if any of the attachment's calibers match any of the gun's calibers. If both arrays are non-empty and intersection is empty, the attachment is rejected. A caliber value of 0 matches all attachments (vanilla behavior). - Magazine replacement map matching —
MagazineReplacement.mapis compared toLevel.info.namecase-sensitively. Map name mismatches silently skip replacement. - Speed_Max scaling — In the
VehicleAssetcontext (for turret-mounted guns), note that vehicle speed values are scaled by 1.25, but gun assets have no such scaling on their firerate calculations. - RechamberAfterShotCount action defaults — Only Pump and Bolt actions default to 1. All other actions default to 0, meaning no hammer animation plays after shooting unless explicitly configured.
