Skip to content

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
    → ItemGunAsset

ItemWeaponAsset 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:

OffsetFieldTypeDescription
0-1Sight IDushortAttached sight item's legacy ID
2-3Tactical IDushortAttached tactical item's legacy ID
4-5Grip IDushortAttached grip item's legacy ID
6-7Barrel IDushortAttached barrel item's legacy ID
8-9Magazine IDushortCurrent magazine's legacy ID
10Ammo countbyteCurrent ammo in the magazine (0-255)
11FiremodebyteCurrent firemode (EFiremode value)
12Interact statebyteInteraction state (e.g., safety toggle)
13Sight qualitybyteSight attachment durability (0-100)
14Tactical qualitybyteTactical attachment durability
15Grip qualitybyteGrip attachment durability
16Barrel qualitybyteBarrel attachment durability
17Magazine qualitybyteMagazine 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 from Magazine_Calibers (count) + Magazine_Caliber_N (individual IDs).
  • attachmentCalibers: Caliber IDs that sight/tactical/grip/barrel attachments must match. Loaded from Attachment_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:

  1. Default magazine: Set via Magazine key (GUID + legacy ID pair stored in defaultMagazineGuid / defaultMagazineLegacyId).
  2. Magazine replacements: The Magazine_Replacements integer 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).
  3. Spawn table resolution: If the resolved asset is a SpawnAsset (spawn table) rather than a direct ItemMagazineAsset, SpawnTableTool.Resolve picks 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 valueRPSRPM
050.03000
125.01500
58.33500
104.54273
202.38143

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:

ActionBehaviorDelete Empty MagsDefault CasingFirerate Note
TriggerSemi-automatic, single shot per trigger pullNoCasingStandard semi-auto
PumpRequires manual rechambering after each shotYesShellShotgun pump action
BoltRequires manual bolt pull after each shotYesCasingSniper bolt action
RailRailgun — no physical casing ejectionYesNoneElectromagnetic
StringBow/crossbow — uses draw animationYesNoneShort rolloff (16m)
RocketLauncher — fires rocket projectileYesNoneLong rolloff (64m)
BreakBreak-action — opens breech to loadYesShellAll casings eject on reload
MinigunSpool-up rotary — must aim to shootNoCasingContinuous 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 to Safety, Semi, Auto keys.
  • hasBurst — Derived from bursts > 0.
  • bursts — Number of shots per burst (stored as int).

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:

FieldDefault.dat KeyDescription
baseSpreadAngleRadiansSpread_Angle_Degrees or Spread_HipBase spread cone half-angle in radians
spreadAimSpread_AimMultiplier while aiming down sights
spreadSprint1.25Spread_SprintMultiplier while sprinting
spreadCrouch0.85Spread_CrouchMultiplier while crouched
spreadProne0.7Spread_ProneMultiplier while prone
spreadSwimming1.1Spread_SwimmingMultiplier while swimming
spreadMidair1.5Spread_MidairMultiplier 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 KeyDescription
recoilMin_x / recoilMin_yRecoil_Min_X / Recoil_Min_YMinimum horizontal/vertical recoil per shot
recoilMax_x / recoilMax_yRecoil_Max_X / Recoil_Max_YMaximum 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 KeyDescription
recover_xRecover_XHorizontal recovery rate (units/sec toward zero)
recover_yRecover_YVertical recovery rate (units/sec toward zero)

Stance Multipliers

FieldDefault.dat Key
aimingRecoilMultiplier1.0Aiming_Recoil_Multiplier
recoilSprint1.25Recoil_Sprint
recoilCrouch0.85Recoil_Crouch
recoilProne0.7Recoil_Prone
recoilSwimming1.1Recoil_Swimming
recoilMidair1.0Recoil_Midair

Camera Shake

Screen shake on firing is defined as random ranges per axis:

Field.dat Key
shakeMin_x / shakeMax_xShake_Min_X / Shake_Max_X
shakeMin_y / shakeMax_yShake_Min_Y / Shake_Max_Y
shakeMin_z / shakeMax_zShake_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 KeyDescription
ballisticStepsBallistic_StepsNumber of simulation steps per shot (byte)
ballisticTravelBallistic_TravelDistance per step in meters (float)
muzzleVelocityComputedballisticTravel * PlayerInput.TOCK_PER_SECOND (32 ticks/sec)
bulletGravityMultiplierBullet_Gravity_Multiplier or Ballistic_DropGravity scale applied to bullet drop (default 4.0)
ballisticForceBallistic_ForceImpulse 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:

FieldDefault.dat KeyDescription
damageFalloffRange1.0Damage_Falloff_RangeFraction of max range where falloff begins
damageFalloffMaxRange1.0Damage_Falloff_Max_RangeFraction of max range where falloff finishes
damageFalloffMultiplier1.0Damage_Falloff_MultiplierDamage 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 KeyDefaultDescription
projectileLifespanProjectile_Lifespan30.0Seconds before projectile is destroyed
projectilePenetrateBuildablesProjectile_Penetrate_BuildablesfalseWhether projectiles pass through constructions
muzzleGuid / muzzleMuzzleEffect spawned at the muzzle on firing
shellGuid / shellShellAction-dependentEffect for ejected shell casings
projectileExplosionEffectGuid / explosionExplosionExplosion effect on projectile impact
projectileExplosionLaunchSpeedProjectile_Explosion_Launch_SpeedplayerDamage * 0.1Physics 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 KeyDefaultDescription
reloadTimeReload_TimeSeconds for the reload animation
hammerTimeHammer_TimeSeconds for the hammer animation
unplaceUnplaceTime to detach magazine
replaceReplace1.0Multiplier for re-attach time
RechamberAfterShotCountRechamberAfterShotCount1 for Pump/BoltShots before hammer plays
RechamberAfterShotDelayRechamberAfterShotDelay0.25sDelay before hammer after shot
EjectAfterHammerDelayEjectAfterHammerDelay0.45sDelay before casing eject
EjectAfterReloadDelayEjectAfterReloadDelay0.5sDelay before casing eject on reload
CasingEjectCountAfterRechamberingAfterShootingCasingEjectCountAfterRechamberingAfterShooting1Casing particles after shot
CasingEjectCountAfterReloadCasingEjectCountAfterReload0 (ammoMax for Break)Casing particles on reload
ShouldEjectCasingAfterShootingEjectCasingAfterShootingTrue for Trigger/MinigunWhether to eject on shot

Magazine Rechambering

Two ERechamberGunAfterReloadMode values control hammer animation playback after magazine insertion or removal:

FieldDefaultDescription
RechamberAfterMagazineAttachedIfAmmoWasEmptyHammer plays when magazine is inserted and chamber was empty
RechamberAfterMagazineDetachedAlwaysHammer 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 KeyDefaultDescription
canEverJamCan_Ever_JamfalseWhether jamming is possible
jamQualityThresholdJam_Quality_Threshold0.4Quality % below which jamming can start
jamMaxChanceJam_Max_Chance0.1Max jam probability at 0% quality
unjamChamberAnimNameUnjam_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 KeyDescription
isTurretTurretTrue if this gun is used on a turret mount
driverTurretViewmodelModeDriverTurretViewmodelModeControls 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 FallbackBundle AssetDescription
_shootShootAudioClip"Shoot"Primary firing sound
_reloadReloadAudioClip"Reload"Magazine change sound
_hammerHammerAudioClip"Hammer"Rechambering sound
_aimAimAudioClip"Aim"ADS transition sound
_minigunMinigunAudioClip"Minigun"Spool-up continuous sound
_chamberJammedSoundChamberJammedAudioClip"ChamberJammed"Jam notification
fireDelaySoundFireDelayAudioClip"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:

ActionDefault Rolloff
String16m
Rocket64m
All others512m

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 KeyDefaultDescription
aimInDurationAim_In_Duration0.2sSeconds from pressing aim to full ADS
shouldScaleAimAnimationsScale_Aim_Animation_SpeedtrueScale Aim_Start/Aim_Stop by aim duration
canAimDuringSprintCan_Aim_During_SprintfalseAllow ADS while sprinting
aimingMovementSpeedMultiplierAiming_Movement_Speed_Multiplier0.75 or 1.0Movement speed while ADS
ShouldForceStopAimingAfterShootingStop_Aiming_After_ShootingfalseStop 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 KeyDefaultDescription
instakillHeadshotsInstakill_HeadshotsfalseInstantly kill players with headshots (only when game config enables)
allowMagazineChangeAllow_Magazine_ChangetrueCan the magazine be changed by the player
rangeRangefinderRange_RangefinderRange valueOverrides rangefinder attachment's maximum distance
shouldFriendlySentryTargetUserHardcodedtrueGuns always cause sentries to target the user

The UseableGun Layer

The UseableGun class (separate file) drives runtime weapon behavior:

  1. Firing loop: On input, checks firemode. For SEMI: fires once. For AUTO: fires continuously at firerate intervals. For BURST: fires bursts shots in sequence.
  2. 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.
  3. Ammo consumption: Each shot consumes ammoPerShot bullets. If infiniteAmmo, no consumption.
  4. Reload: Triggers when ammo reaches 0 and a magazine is available. Plays reload animation, updates state bytes.
  5. 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.
  6. 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 Weapon and Item.

The cargo export covers over 70 fields including all ballistic, recoil, spread, damage, audio, firemode, attachment, magazine, and quality configuration values.

Common Issues

  1. Spread conversion — The switch from Spread_Hip (tangent) to Spread_Angle_Degrees (angle) changes the effective spread curve. Converted values may not match visually. Enable -LogGunSpreadConversion to audit.
  2. Ballistic range mismatch — If ballisticSteps * ballisticTravel differs from range by > 0.1m, a warning is logged but no fix is applied. The actual ballistic range and the intended range may differ.
  3. 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.
  4. Fire delay tick rounding — The fire delay uses Mathf.RoundToInt on Fire_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.
  5. Caliber matching logicCalibersContainAnyOfIds checks 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).
  6. Magazine replacement map matchingMagazineReplacement.map is compared to Level.info.name case-sensitively. Map name mismatches silently skip replacement.
  7. Speed_Max scaling — In the VehicleAsset context (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.
  8. 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.