Kerim Čamdžić
WALL 128.128

Modular Weapon Attachment System

A data-driven modular weapon attachment framework built in C++ for Source SDK 2013, designed to support deterministic stat aggregation, network-safe replication, and runtime extensibility without subclass explosion.

This system restructures the traditional static weapon architecture into a composable, entity-based modifier framework.


Problem & Constraints

The default Source SDK weapon model tightly couples behavior and configuration to concrete weapon classes. Customization typically requires subclassing or hardcoding values, leading to:

  • Class proliferation (e.g., Weapon_AK47_Silenced_Scoped_ExtMag)
  • Poor runtime flexibility
  • Increased maintenance complexity
  • Fragile multiplayer prediction behavior

The goal was to design a system that:

  • Supports multiple simultaneous attachments
  • Allows runtime stat modification
  • Maintains deterministic client/server parity
  • Avoids behavioral override spaghetti
  • Remains compatible with Source’s networking model

Core Architecture

Instead of subclassing weapons per configuration, the system uses composition.

Each weapon instance owns a map of attachment entities stored in a UtlMap keyed by attachment type:

DEFINE_UTLMAP(m_Attachments, FIELD_INTEGER, FIELD_CLASSPTR)

This structure provides:

  • Fast lookup by attachment type
  • Type-based uniqueness
  • Clean aggregation iteration
  • Scalable attachment categories

Attachments are full entity classes derived from CBaseWeaponAttachment, not simple data structs.


Constructor-Based Configuration Model

Attachments configure weapon behavior through constructor initialization and networked modifier fields rather than virtual method overrides.

Example (Silencer):

SetAttachmentType(ATTACHMENT_SILENCER);
SetDamageModifier(0.9f);
SetSpreadModifier(0.8f);
SetFireSound("Weapon_Silenced.Fire");

Modifiers are stored in network-replicated members:

CNetworkVar(float, m_flDamageModifier);
CNetworkVar(float, m_flFireRateModifier);
CNetworkVar(float, m_flSpreadModifier);
CNetworkString(m_szFireSound, MAX_PATH);

This design ensures:

  • Deterministic behavior across client and server
  • No behavioral branching inside attachment subclasses
  • Predictable aggregation model
  • Clean separation of concerns

Attachments remain passive configuration modules. The weapon class remains responsible for applying final resolved behavior.


Data-Driven Attachment Scripts

Attachments load configuration dynamically from KeyValues scripts:

AtchData.Damage = kv->GetFloat("damage_modifier", 1.0f);
AtchData.FireRate = kv->GetFloat("firerate_modifier", 1.0f);
AtchData.Spread = kv->GetFloat("spread_modifier", 1.0f);

AtchData.VM_offset_forward = kv->GetFloat("ads_offset_forward", 0.0f);
AtchData.VM_offset_right   = kv->GetFloat("ads_offset_right", 0.0f);
AtchData.VM_offset_up      = kv->GetFloat("ads_offset_up", 0.0f);

This enables:

  • Designer-driven balancing
  • Runtime tuning without recompilation
  • Visual ADS offset configuration
  • Spread and recoil tuning

Runtime Stat Aggregation

Base weapon stats remain immutable.

Effective values are computed dynamically inside the weapon:

float totalDamage = GetBaseDamage();

FOR_EACH_MAP(m_Attachments, i)
{
    totalDamage *= m_Attachments[i]->GetDamageModifier();
}

Fire sound resolution:

const char* fireSound = pAttachment->GetFireSound();

This centralized resolution model guarantees:

  • Reversible attachment behavior
  • No stat mutation corruption
  • Identical aggregation on client and server
  • Deterministic multiplayer prediction

Compatibility Filtering

Attachments maintain a list of compatible weapon classes:

CUtlVector<const char*> m_CompatibleWeapons;

Compatibility is checked at runtime to prevent invalid attachment combinations. This ensures structural integrity and prevents mismatched configurations.


Visual & Entity Integration

Attachments are full CBaseAnimating entities and:

  • Precache their models
  • Integrate with bone merge systems
  • Participate in client rendering
  • Support custom lighting origins
  • Maintain viewmodel offset data

They are not just logical modifiers but fully integrated visual components.


Networking & Prediction Safety

Because Source Engine relies heavily on client-side prediction, attachment state must remain synchronized.

Networked fields ensure modifier consistency:

CNetworkVar(float, m_flDamageModifier);
CNetworkVar(float, m_flFireRateModifier);
CNetworkVar(float, m_flSpreadModifier);
CNetworkString(m_szFireSound, MAX_PATH);

This guarantees:

  • Deterministic aggregation logic
  • No client/server divergence
  • Stable lag compensation behavior
  • Predictable fire parameter resolution

All effective behavior is computed centrally within the weapon using replicated attachment data.


Factory & Registration System

Attachments self-register using a factory macro:

REGISTER_ATTACHMENT_CLASS(CAttachment_Silencer, ATTACHMENT_SILENCER)

This system:

  • Maps attachment type to creation function
  • Avoids large switch statements
  • Supports dynamic instantiation
  • Enables save/restore reconstruction

Save/restore helpers:

void SaveWeaponAttachments(CSave& save, CUtlVector<CBaseWeaponAttachment*>& attachments);
void RestoreWeaponAttachments(CRestore& restore, CUtlVector<CBaseWeaponAttachment*>& attachments);

This ensures persistence across game sessions.


Design Principles

  • Composition over inheritance
  • Centralized behavior resolution
  • Aggregation over mutation
  • Data-driven configuration
  • Deterministic multiplayer safety
  • Factory-based extensibility

Summary

The Modular Weapon Attachment System transforms the static Source SDK 2013 weapon architecture into a scalable, network-safe, data-driven framework.

By combining:

  • Constructor-based configuration
  • NetworkVar replication
  • Runtime aggregation
  • Factory registration
  • Entity-level integration

the system enables advanced gameplay customization while preserving Source Engine prediction integrity and architectural cleanliness.