Wardogs — Weapon spread, accuracy and shot direction
Project:
Kiyogitpy/wardogs-data-docs· Live site
How the firing direction of one shot is derived from the player’s aim: where the aim comes from, the zeroing tilt, the cone angle (accuracy plus movement-driven hip spread), how a random point in that cone is drawn, how shotgun pellets are placed, what the server receives, and whether recoil is applied before or after.
Short answer.
- The shot starts from the view rotation at the muzzle position (§3). A zeroing pitch is added (§4).
- A cone half-angle is added around that direction:
0.5 × accuracy angle(a few hundredths of a degree) plus a hip spread of 1 to 7 degrees that depends on stance and speed and is exactly 0 when the aim blend is 1.0 (fully aimed) (§5, §6). - The direction is drawn with the engine’s
FRandomStream::VRandCone(an elliptical-cone routine; here a circle), seeded fresh for every shot from the C runtimerand()(§7). - The result is rounded to 16-bit pitch and yaw and packed into a fire record; only the shotgun record carries a seed (§9).
- No per-shot bloom exists in the spread state. The spread value only follows movement, stance, air and aim blend, smoothed at 25 per second (§6). Firing does not raise it.
- Recoil is applied after the projectile is spawned, so a shot is not moved by its own kick; earlier kicks already moved the view and are part of the aim (§10).
Build: ++Wardogs+Live-CL-516586 · Engine: UE 5.7 · Native addresses are file RVAs (image base 0x140000000).
Evidence level: static-analysis image; virtualized dispatch not removed. Everything below was read with capstone and pefile from the flat executables; the decompilation corpus and the IDA database were not used. The static-analysis image was byte-compared against the older reference image (WardogsClient-Win64-Shipping.restored.exe) for every function cited (§13): all are byte-identical except 0x6B956B0 and 0x1670B80, which begin with a protection-layer dispatch stub in the analysis image and were read from the reference image, as IDA claim exposure prescribes. No game was run and no process was observed: no runtime confirmation exists. Anything not statically resolvable is labelled open (§14).
| Label | Meaning |
|---|---|
| Native | Read from instructions or .rdata constants in the executable; RVA cited |
| Data | Read from the cooked export under data/ |
| SDK | Class or struct layout from sdk/raw |
| Derived | Arithmetic on Native and Data facts only |
| Inferred | Role deduced from how the code uses a field; not named anywhere |
1. Summary of the model
For one shot (client side, 0x6B94700):
Q0 = view rotation at the muzzle socket position (§3)
Z = ZeroTable[range] + atan2(h, 100·range) · 180/π degrees, 0 if range ≤ 0 (§4)
Q1 = Q0 ⊗ Quat(Pitch = Z) zeroing, local pitch
F0 = Q1.RotateVector(1,0,0)
θ = 0.5 · A · Mw · Mc + (pellets ≤ 1 ? S·π/180 : 0) cone half-angle, radians (§5)
A = WeaponAccuracy in radians (MOA × 2.909e-4, mrad × 1e-3)
Mw = Weapon.AccuracyMultiplier, Mc = Character.WeaponAccuracyMultiplier
S = hip spread in degrees, ext+0x3AC (§6)
F = VRandCone(F0, θ) with a stream seeded by rand() (§7)
Rot = quantise16(ToOrientationRotator(F)) pitch, yaw only (§8)
Hip spread state, updated every tick of the weapon’s item extension (0x6C9FEE0):
x = clamp(|v_xy| / vref[stance], 0, 1) vref = {Prone 50, Crouch 250, Stand 400} cm/s
target = 0 if aim blend == 1.0
= InAirHipFireSpread (10° default) if airborne
= SpreadMultiplier · Value · CT_Weapon_Spread[row(stance)](x) otherwise
S' = S + (target − S) · clamp(25·dt, 0, 1) (FInterpTo; set to 0 at once when aim blend == 1.0)
2. The stats and data fields
| Field | Where | Offset | Meaning for spread |
|---|---|---|---|
WeaponAccuracy.AccuracyType | FWDWeaponStats (SDK FWDWeaponAccuracy) | +0x0 | 0 MinutesOfAngle, 1 Milliradians. Native default is 1 (constructor 0x699E6C0: mov byte [rcx],1), so an asset that omits it is in milliradians |
WeaponAccuracy.AccuracyValue | same | +0x4 | Full cone angle in that unit; default 1.0. Getter 0x6BBD3D0 converts to radians: MOA × 0.000290888, mrad × 0.001, other types 0 |
RandomDeviationDegree | same | +0xCE0 | Recoil only (rotates the recoil pattern, §10); not read by the shot-direction code |
ShotgunData | same | +0xD40 (0x60 bytes) | Pellet pattern (§11) |
InitialBulletSpeed | same | +0xDA0 | Muzzle speed; getter 0x6BBD500. Not a spread input |
SpreadValues (stance → FBHScalableFloat) | WDItemExtensionData_RangedWeapon | +0xA8 | Hip spread curve per stance: Value × row of CT_Weapon_Spread |
InAirHipFireSpread | same | +0x1DC | Spread while airborne, degrees. Native default 10.0 (immediate 0x41200000 stored at 0x6C9102D in the data constructor); only GrenadeLauncher sets it (10.0) |
bDoesWeaponUseSpread, SpreadValue | WDVehicleWeaponExtensionDataAsset | +0x88, +0x90 | Vehicle weapon extensions: spread = Value x curve row at level 0, eased toward at 25 per second by 0x764BED0, zero in full ADS; stored at actor +0x440 (§14 item 6) |
NumberOfPellets, AmmunitionFactor | FWDProjectileStats in the projectile data | +0x18C, +0x190 | Pellet count and a choke scale |
MaxPelletCount | UWDGameSettings.ProjectileSystemSettings | settings +0x5E4 | Pellet normaliser (§11); value not in the export |
Attribute.Gameplay.Weapon.SpreadMultiplier | weapon item attributes | tag global 0xD36EBB0 | Multiplies the hip spread target |
Attribute.Gameplay.Weapon.AccuracyMultiplier | weapon item attributes | tag global 0xD36EBB8 | Multiplies the accuracy cone |
Attribute.Gameplay.Character.WeaponAccuracyMultiplier | character attributes | tag global 0xD37E8C0 | Multiplies the accuracy cone |
Attribute.Gameplay.Weapon.Length | weapon item attributes | tag global 0xD36EBC0 | Not a spread input (interface slot +0xC8, default 80) |
Tag strings were read from the static initialisers that load them at 0x12AB10B (AccuracyMultiplier), 0x12AB56B (SpreadMultiplier), 0x12AB6AB (Length) and 0x12CB33B (WeaponAccuracyMultiplier).
3. Where the aim comes from
Native. UWDWeaponComponent vtable slot +0x578 is start-fire (0x6B980A0), which arms the timer whose body is 0x6B94700 (Timer_FireWeapon). That body asks the IWDWeaponOwner interface (obtained from the component’s outer through 0x1921820) for the projectile spawn transform:
- For a held weapon the owner is
UWDItemExtension_RangedWeapon; its interface sits at ext+0xF0with vtable0xB389AB8. Slot+0x18isGetProjectileSpawnTransform=0x6C987A0. It takes the muzzle socket world transform (0x6C97DD0) and, when the owning character passes a predicate (0x6E70B10; Inferred: locally controlled), replaces its rotation with the character’s view rotation (character virtual+0x840, thenFRotator::Quaternion0x166D220). Translation and scale stay those of the socket. - Slot
+0x20(GetProjectileSpawnTransforms, array) is the empty-array stub0x15B8F80for a held weapon, so0x6B94700takes the single-transform branch (0x6B90780, which post-processes the transform with0x6B90890, a function that includes a trace; not decoded, §14). A weapon that returns several transforms (vehicle weapons) rolls an independent cone for each, with its ownrand()seed, but the same half-angle.
So the aim direction is the camera/view rotation at the moment the timer fires. Because the view rotation is where recoil is applied (§10), earlier kicks are already in it.
4. Zeroing tilt
Native (0x6B90610, called from 0x6B94700 with the spawn transform):
base = ZeroTable[ComponentZeroingRange] 0x6B956B0, TMap<int32,float> at component +0xFF8
if range ≤ 0: return base range at component +0xF8C, constructor value −1
h = (EyePoint − transform.Translation) · transform.UpVector
Z = base + atan2(h, 100·range) · 57.29578 atan2 = 0x15AE590
EyePoint is the first output of a character virtual (+0x720) and the check 0x6E70B70 must pass; otherwise Z = base. Inferred: h is the height of the eye above the bore along the muzzle’s up axis (cm), so the bore is tilted up by the angle at which the bullet’s straight line meets the sight line at range metres. The rotation is composed as Q1 = Q0 ⊗ Quat(Pitch=Z) (Hamilton product with the spawn rotation first; read from the shuffle sequence at 0x6B94AD4–0x6B94B84), so Z is a pitch about the weapon’s own right axis. How ZeroTable is filled is open (§14); 0x6B956B0 is the function that begins with a dispatch stub in the analysis image and was read from the reference image.
5. The cone half-angle
Native (0x6B94BB9–0x6B94C14):
accuracy_rad = 0x6BBD3D0(stats at component +0x140) (§2)
mult = 0x6C974E0 = Weapon.AccuracyMultiplier (default 1) × Character.WeaponAccuracyMultiplier (default 1)
cone_acc = 0x6B95630 = accuracy_rad × mult (interface slot +0xC0 supplies mult)
S_deg = interface slot +0xA8 = GetSpreadAngle (0x3CD6240: movss xmm0,[this+0x2BC] = ext +0x3AC)
θ = 0.5 · cone_acc + (NumberOfPellets > 1 ? 0 : S_deg · 0.0174533)
NumberOfPellets is read from the projectile data’s stats (0x6BBA900 returns the possibly ammo-modified FWDProjectileStats; +0x18C). A multi-pellet round ignores the hip spread curve completely; its spread comes from ShotgunData (§11). The accuracy angle is treated as a full cone angle (the 0.5 turns it into a half-angle); the spread is already a half-angle. While that listener list is non-empty, θ is also stored in degrees in the per-shot result at frame +0x270 as max(0, 0.5·accuracy_deg + S_deg) (0x6B93FD7–0x6B9408A); its consumer is not traced (§14).
Data. Accuracy half-angles run from 0.003 degrees (SR_04; the bow is 0) to 0.57 degrees (MMGL); a typical rifle is 0.02 to 0.04 degrees (table in §12). The only cooked effect on either accuracy multiplier is GE_MMGL_RotaryPassengerSway (Character.WeaponAccuracyMultiplier ×c 2.5). No cooked effect changes Weapon.AccuracyMultiplier.
6. Hip spread state: movement, stance, air, aim blend
Native. UWDItemExtension_RangedWeapon has a virtual Tick(float) (primary vtable 0xB389290, slot +0x6D0 = 0x6C9E7D0). After the base tick (0x3CB6400) it calls UpdateSpread = 0x6C9FEE0 with dt in xmm1. Steps:
- Resolve the owning character (
0x675CDB0); return if it is invalid or its virtual+0x818is false (a predicate on the machine that simulates the weapon; name open). - Fetch the ranged-weapon data asset (
0x4BD0D20). - Aim blend.
0x6E6E260returns the weapon behaviour component’s aim blend (+0x3E4, read by0x6B86240; the blend that Weapons §5b describes). If it equals 1.0 exactly (ucomiss/je), spread is set to 0 at once (0x6CA01A8) and nothing else is evaluated. A partly aimed weapon (0 < blend < 1) is treated as hip fire at full value. - Airborne (
0x5296290: mover component virtual+0xD8):target = data.InAirHipFireSpread(+0x1DC).SpreadMultiplieris not applied on this path. - Otherwise, grounded:
stance=0x5293150: the mover’s stance byte (Prone 0,Crouch 1,Stand 2; value 3 maps to 2; 2 if no mover).v= length of the 2D velocity returned by character virtual+0x360.vref={50, 250, 400}[stance](cm/s). The three floats0x42480000,0x437A0000,0x43C80000are stored by the static initialiser at0x6CA01F7into an array whose pointer is at0xCEC1528(count at0xCEC1530); an index of 3 or more uses 400. If|vref| ≤ 1e-8the ratio is 0 or 1 by comparison.x = clamp(v / vref, 0, 1).curve = SpreadValues[stance].GetValueAtLevel(x)=Value × CT_Weapon_Spread[row](x)(FBHScalableFloat0x48AA830; every cookedValueis 1.0). Stored at ext+0x3B0.target = SpreadMultiplier × curve(0x6C97710: attribute0xD36EBB0, default 1.0 when the item has no attribute container). Stored back at ext+0x3B0.
S' = FInterpTo(S, target, dt, 25.0)(0x5384040; the25.0constant is0x41C80000at0x9BAB148):S' = S + (target − S) · clamp(25·dt, 0, 1). A step of 1/60 s closes 41.7 % of the gap; the time constant is 0.04 s. Stored at ext+0x3AC.
The two stored values (+0x3AC current, +0x3B0 target) are also copied, with +0x378, into a state record by the function at 0x6C9DB60 (a prediction/sync snapshot; Inferred). Quirk (Derived): if the stance has no entry in SpreadValues, step 5 skips the lookup, so +0x3B0 keeps its previous value and is multiplied by SpreadMultiplier again every tick. All 37 weapons have all three stances, so this is not reached in the shipped data.
Not found. Nothing in this routine reads the stamina, suppression, damage-taken or fire state, and nothing in the item-extension code range (0x6C80000–0x6CA2000) other than UpdateSpread writes +0x3AC/+0x3B0. In particular there is no per-shot bloom and no recovery timer: after the last shot the value is already at its movement target, and it moves only when speed, stance, air or aim blend change (§14 for the limits of this scan). The one cooked path from combat state into the cone is an effect on Character.WeaponAccuracyMultiplier (§5).
Hip spread curves (CT_Weapon_Spread, degrees, X = speed ratio 0 → 1, linear)
| Class | Stand | Crouch | Prone |
|---|---|---|---|
| AR, DMR, LMG, Pistol, Shotgun | 3 → 6 | 2 → 4 | 1.5 → 3 (AR) or 1 → 2 (others) |
| SMG, Launcher | 2 → 4 | 1.5 → 3 | 1 → 2 |
| Sniper | 4 → 7 | 3 → 6 | 2 → 4 |
| Bow | 1 → 2 | 0.5 → 1 | 0.5 → 0.5 |
| MountedMachineGun | 1 → 2 (single row) | ||
| StableMountedMachineGun | 0.5 → 1 (single row) |
Source: CT_Weapon_Spread.json (29 rows). Every infantry weapon uses its own class for all three stances (§12). The PKM (LMG_02) uses the Sniper rows.
7. The random distribution
Native (0x3FD4FA0, called at 0x6B94C3C). This is the engine routine FRandomStream::VRandCone(Dir, HorizontalHalfAngle, VerticalHalfAngle), called with both angles equal to θ:
Seed' = Seed · 196314165 + 907633515 (mod 2^32) constants 0x0BB38435, 0x3619636B
FRand = float((Seed' >> 9) | 0x3F800000) − 1.0 uniform in [0, 1)
U = FRand; V = FRand two draws, U first
Theta = 2π · U
Phi0 = acos(clamp(2V − 1, −1, 1))
Cone = sqrt(1 / ((sin Theta / H)² + (cos Theta / Vt)²)) = θ for H = Vt = θ
Phi = fmod(Phi0, Cone) 0x15B50A0
R1 = Dir rotated by Phi° about DirRight (0x16706F0, Phi × 57.29578)
Result = normalise(R1 rotated by Theta° about Dir) (Theta° = U × 360)
If θ ≤ 0 the routine returns Dir normalised (0x3FD52F5), so a weapon with zero accuracy and zero hip spread (the bow when aimed) fires exactly along F0.
Derived distribution. Phi0 is uniform on the sphere, and the fmod folds it into [0, θ). For the small θ used here (≤ 7 degrees, a fold count above 25) the folded density is flat to better than 0.1 %: the radius from the aim point is uniform in angle, not in area. The probability of landing within angle r is r/θ; half of all shots land inside θ/2, which is a quarter of the cone’s area. The pattern is centre-heavy (area density ∝ 1/r), not a uniform disc and not Gaussian. There is no rejection sampling.
RNG and seed. The stream is a local FRandomStream (InitialSeed and Seed at +0/+4) created inside 0x6B94700 and filled with one value of the C runtime rand() (ucrtbase!rand through the IAT slot 0x9A28EC8; 0x6B94B89), a new seed for every shot (and for every muzzle transform). srand has a single direct call site (0x46CF6E8), so the game thread’s rand() state is seeded once at start-up (the seed value is not traced; open). The stream is not an engine global, is not derived from the shot id, and is not shared between shots. Recoil uses the same CRT rand() (Weapons §5a), so the two draws are not correlated by design.
8. Quantisation and the final direction
Native (0x6B94C41–0x6B94C59, 0x6B93610):
F(a vector) →FVector::ToOrientationRotator(0x1676A00):Pitch = atan2(z, √(x²+y²)),Yaw = atan2(y, x), in degrees, roll 0.0x1676910(ToOrientationQuat) rebuilds the quaternionQfor the next muzzle.0x76DFB20compresses the rotator: each of pitch and yaw becomesRoundToInt(angle × 65536/360) & 0xFFFF; yaw is the high 16 bits, pitch the low 16 bits.0x76E0810decompresses it (× 0.0054931640625). The shot is then fired with the decompressed rotator, so the client and the server use the same, quantised direction.- The angular step is 360/65536 = 0.00549 degrees (0.096 mrad). This is comparable to the accuracy half-angle of the most precise weapons (0.003 to 0.02 degrees), so for aimed shots the quantisation noise is of the same size as the accuracy cone.
- The projectile subsystem builds the velocity as
Rotator.Vector() × speed(0x6F1DDC0:0x16793E0then a multiply by the speed from0x6BBD500). The spawn wrapper0x6B96580receivesInitialBulletSpeed × component+0x114(0x6B959E0;+0x114is 1.0 from the constructor0x6B8E5A0). Where that factor is applied is open (§14). Flight after launch is in Projectile flight.
9. Client, server and replication
Native.
- The client path is
0x6B94700(compose direction,rand()seed) →0x6B93610(component, shotId, &location, &rotator, seed, …).shotIdis a process-wide counter incremented by0x76E0800. 0x6B93610is shared by client and server. It quantises the rotator (§8), spawns the projectile(s) through0x6B96580, appends a 0x28-byte record to the outgoing list, then (for a local controller) computes and queues the recoil kick (0x6B95C50, §10). Record layout, read from the two writers and the two server readers:+0x00location (3 doubles),+0x18packed rotator (u32),+0x1Cpellet seed (i32, −1 if the round has one projectile),+0x20shot id (u32),+0x24flag (u8).- RPCs:
Server_FireProjectile(uint32 firingSequence, uint16, FAutoProxyFixedTickInfo, TArray<record>)(exec0x6B996D0, implementation vtable+0x510=0x6B97900) andServer_FireBuckshotProjectile(exec0x6B99530, implementation0x6B97820). Both first call0x3CAB0A0(a check on the component at+0xC8; not decoded) and take the refuse path0x6B92180when it is non-zero. Each record is decompressed (0x76E0810) and passed to0x6B93610:0x6B97900passes seed −1;0x6B97820passes the record’s+0x1Cseed. UpdateSpreadis gated by the character predicate in §6 step 1.0x6B93610readsGetSpreadAngleagain only to fill the+0x270echo (§5), and only while a global listener list (0xCEB6D70) is non-empty.
10. Interaction with recoil
Native. In 0x6B93610 the order is: build the transform → (client) cone → quantise → spawn (0x6B96580) → recoil (0x6B95C50, only for a local WDPlayerController, 0x6B96C20). The kick is queued and applied to the view over the following frames by the tick (Weapons §5a).
- A shot is fired along the view as it is before its own kick. Kicks from earlier shots are already added to the control rotation, so recoil moves the centre of the next cone; it does not change the cone size.
- Recoil does not feed the spread state:
UpdateSpreadnever reads it, andRandomDeviationDegreeis a recoil field (the angle by which a recoil-pattern point is rotated), not a bullet-dispersion field. - Whether the aim-sway offset (
WeaponBehaviorComponentaim sway) is part of the view rotation read at0x6C98834is open (§14). The cooked sway effects (Character.AimSway*,WeaponSway.*) are separate attributes and do not touch the accuracy attributes.
11. Multi-pellet rounds (shotgun)
Native. When NumberOfPellets > 1 (0x6B93BDB, 0x6B940C1), 0x6B93610 copies ShotgunData (0x6BBD5D0: stats +0xD40), replaces it by an attachment-supplied copy if 0x6B98980 returns one (a choke attachment; Inferred), and calls the pellet generator 0x6C25660(out array, aim direction, ShotgunData, N, AmmunitionFactor, seed). The aim direction is the one from §5 (a tiny accuracy cone, no hip spread). The generator is deterministic given the seed:
Seed0 = seed; stream: Seed' = Seed·196314165 + 907633515, FRand = float((Seed'>>9)|0x3F800000) − 1
A, B = outer half-angles (deg): bSeparateOuterConeHalfAngles ? (OuterConeHorizontalHalfAngle, OuterConeVerticalHalfAngle)
: (OuterConeHalfAngle, OuterConeHalfAngle)
I = InnerConeHalfAngleMultiplier
(Rh, Rv) = per-shot random limits: bSeparateRandomConeHalfAnglesRanges ? (lerp(HRange, r2), lerp(VRange, r1))
: (lerp(Range, r1), same value) lerp(range, r) = min + (max − min)·r
Centre = VRandCone(Dir, min(A(1−I), Rh)·π/180, min(B(1−I), Rv)·π/180) one centre per shot
P = clamp(N / MaxPelletCount, 0, 1) · AmmunitionFactor · ChokeFactor
for each of N pellets:
r = FRand
if r > P : pellet = VRandCone(Dir, A·π/180, B·π/180) outer pellet
else : pellet = VRandCone(Centre, I·A·π/180, I·B·π/180) inner (choked) pellet
MaxPelletCount is UWDGameSettings.ProjectileSystemSettings +0x18C (settings +0x5E4, read through 0x6C03540); its value is 16 (packed DefaultGame.ini, Hit Registration §4.6). With N = 8 pellets (7mm Buckshot) and AmmunitionFactor 0.8: M500 P = 0.5 x 0.8 x 0.5 = 0.20, MP43 P = 0.5 x 0.8 x 0.35 = 0.14. The M500 choke attachments carry their own ShotgunData (bOverrideShotgunData true): Improved Cylinder Choke (ID_MUZL_044) choke 0.65, outer half-angle 1.36 degrees, random limit 0 to 1.75, inner multiplier 0.55, so P = 0.26; Full Choke (ID_MUZL_045) choke 0.85, outer 0.95 degrees, random limit 0 to 1.55, inner multiplier 0.6, so P = 0.34. The 54 other muzzle devices that have the field set it to false. The inner cone always fits inside the outer cone, because the centre may move only A(1−I). Each pellet then goes to 0x6B96580 with speed InitialBulletSpeed × component+0x114; the id of every spawned pellet is collected for the RPC.
Data. Native defaults (constructor 0x699E6C0): ChokeFactor 0.8, outer half-angle 10 degrees (horizontal and vertical 10), inner multiplier 0.5, random ranges (0, 0). 7mm Buckshot has NumberOfPellets 8 and AmmunitionFactor 0.8; the rifled slug has no pellet count (one projectile). Cooked shotguns are in §12: M500 (DA_M12G_Stats: choke 0.5, outer 1.56 degrees, random limit 0 to 2 degrees, inner multiplier default 0.5) and MP43 (choke 0.35, outer 1.85 degrees, random limit 0 to 3, inner 0.6). The Judge has no ShotgunData block, so it uses the defaults if its ammunition has more than one pellet.
12. The cooked values per weapon
Generated by _tools/spread_table.py from data/ (python _tools/spread_table.py --write). † marks a value the asset omits, taken from the native default (the accuracy unit is the omitted field and the value is cooked; GrenadeLauncher and Launcher_04 have no WeaponAccuracy block, so both unit and value are the defaults, 1 mrad). Hip half-angle is S before SpreadMultiplier; the cone also carries the accuracy half-angle (column 3). Attachment multipliers are not applied.
| Weapon | Accuracy | Accuracy half-angle (°) | Curve class | Hip half-angle, Stand: still → full speed (°) | Crouch | Prone | Shotgun data | |—|—|—:|—|—|—|—|—| | A91 | 1.5 mrad† | 0.0430 | AR | 3 → 6 | 2 → 4 | 1.5 → 3 | — | | AK74M | 0.825 mrad† | 0.0236 | AR | 3 → 6 | 2 → 4 | 1.5 → 3 | — | | CGM4 | 2.5 mrad† | 0.0716 | Launcher | 2 → 4 | 1.5 → 3 | 1 → 2 | — | | CombatBow | 0 mrad† | 0.0000 | Bow | 1 → 2 | 0.5 → 1 | 0.5 → 0.5 | — | | Glock17 | 3.49 mrad† | 0.1000 | Pistol | 3 → 6 | 2 → 4 | 1 → 2 | — | | GrenadeLauncher | 1 mrad† | 0.0286 | AR | 3 → 6 | 2 → 4 | 1.5 → 3 | — | | Judge | 3.49 mrad† | 0.1000 | Pistol | 3 → 6 | 2 → 4 | 1 → 2 | — | | KH2002 | 1.5 mrad† | 0.0430 | AR | 3 → 6 | 2 → 4 | 1.5 → 3 | — | | Launcher_04 | 1 mrad† | 0.0286 | Launcher | 2 → 4 | 1.5 → 3 | 1 → 2 | — | | LMG_02 | 2.5 mrad† | 0.0716 | Sniper | 4 → 7 | 3 → 6 | 2 → 4 | — | | M249 | 4.3 mrad† | 0.1232 | LMG | 3 → 6 | 2 → 4 | 1 → 2 | — | | M4 | 0.84 mrad† | 0.0241 | AR | 3 → 6 | 2 → 4 | 1.5 → 3 | — | | M500 | 1.5 mrad† | 0.0430 | Shotgun | 3 → 6 | 2 → 4 | 1 → 2 | choke 0.5, outer 1.56°, inner ×0.5, centre jitter 0…2° | | MK22 | 0.16 mrad† | 0.0046 | Sniper | 4 → 7 | 3 → 6 | 2 → 4 | — | | MMGL | 20 mrad† | 0.5730 | Launcher | 2 → 4 | 1.5 → 3 | 1 → 2 | — | | MP43 | 3.08 mrad† | 0.0882 | Shotgun | 3 → 6 | 2 → 4 | 1 → 2 | choke 0.35, outer 1.85°, inner ×0.6, centre jitter 0…3° | | MP9 | 2.1 mrad† | 0.0602 | SMG | 2 → 4 | 1.5 → 3 | 1 → 2 | — | | RFB | 0.69 mrad† | 0.0198 | AR | 3 → 6 | 2 → 4 | 1.5 → 3 | — | | RPG7 | 3 mrad† | 0.0859 | Launcher | 2 → 4 | 1.5 → 3 | 1 → 2 | — | | SKS | 0.73 MOA | 0.0061 | DMR | 3 → 6 | 2 → 4 | 1 → 2 | — | | SMG_03 | 1.3 mrad† | 0.0372 | SMG | 2 → 4 | 1.5 → 3 | 1 → 2 | — | | SR_04 | 0.11 mrad† | 0.0032 | Sniper | 4 → 7 | 3 → 6 | 2 → 4 | — | | SV98 | 0.56 mrad† | 0.0160 | Sniper | 4 → 7 | 3 → 6 | 2 → 4 | — | | SVDM | 0.45 mrad† | 0.0129 | Sniper | 4 → 7 | 3 → 6 | 2 → 4 | zeroed (choke 0, outer 0) | | TAR21 | 0.87 mrad† | 0.0249 | AR | 3 → 6 | 2 → 4 | 1.5 → 3 | — | | Vector | 2 mrad† | 0.0573 | SMG | 2 → 4 | 1.5 → 3 | 1 → 2 | — | | WEPN_017 | 0.78 mrad† | 0.0223 | Sniper | 4 → 7 | 3 → 6 | 2 → 4 | — | | WEPN_026 | 2 mrad† | 0.0573 | Pistol | 3 → 6 | 2 → 4 | 1 → 2 | — | | WEPN_027 | 0.84 MOA | 0.0070 | Pistol | 3 → 6 | 2 → 4 | 1 → 2 | — | | WEPN_028 | 1.16 mrad† | 0.0332 | SMG | 2 → 4 | 1.5 → 3 | 1 → 2 | — | | WEPN_029 | 0.73 mrad† | 0.0209 | AR | 3 → 6 | 2 → 4 | 1.5 → 3 | — | | WEPN_030 | 0.58 mrad† | 0.0166 | AR | 3 → 6 | 2 → 4 | 1.5 → 3 | — | | WEPN_031 | 1.3 mrad† | 0.0372 | SMG | 2 → 4 | 1.5 → 3 | 1 → 2 | — | | WEPN_032 | 2 mrad† | 0.0573 | Pistol | 3 → 6 | 2 → 4 | 1 → 2 | — | | WEPN_033 | 1.5 mrad† | 0.0430 | AR | 3 → 6 | 2 → 4 | 1.5 → 3 | — | | WEPN_034 | 0.73 mrad† | 0.0209 | DMR | 3 → 6 | 2 → 4 | 1 → 2 | zeroed (choke 0, outer 0) | | WEPN_035 | 1.5 mrad† | 0.0430 | Sniper | 4 → 7 | 3 → 6 | 2 → 4 | — | | WEPN_036 | 0.73 mrad† | 0.0209 | AR | 3 → 6 | 2 → 4 | 1.5 → 3 | — | | WEPN_037 | 0.58 mrad† | 0.0166 | AR | 3 → 6 | 2 → 4 | 1.5 → 3 | zeroed (choke 0, outer 0) |
Worked example. AK74M (0.825 mrad†, class AR), standing, no attachments, moving at 200 cm/s: x = 200/400 = 0.5, S = 3 + 0.5·(6−3) = 4.5 degrees, accuracy half-angle 0.5·0.825e-3 rad = 0.0236 degrees, so θ = 4.52 degrees; at 100 m the radius of the cone is 100 m · tan 4.52° ≈ 7.9 m, and half of the shots land within about 3.9 m. Standing still it is 3.02 degrees. With the grip GE_FGP_02 (SpreadMultiplier ×c 0.83) moving: 4.5 · 0.83 = 3.74 degrees. Fully aimed (aim blend 1.0): θ = 0.0236 degrees, which is about 4 quantisation steps (§8): about 4 cm at 100 m. A bipod-mounted GE_FGP_04_BipodState weapon: ×c 0.45; unfolded: ×c 1.15.
Modifier summary (Data: GE values, 341 cooked effects)
| Attribute | Cooked modifiers | Operation | Range |
|---|---|---|---|
Weapon.SpreadMultiplier | 92 (foregrips, muzzle devices, bipod states) | MultiplyCompound | 0.45 (bipod mounted) to 1.15 (bipod unfolded); muzzle devices 0.88 to 0.99; foregrips 0.76 to 0.94 |
Weapon.AccuracyMultiplier | 0 | all weapons at the base value 1 | |
Character.WeaponAccuracyMultiplier | 1 (GE_MMGL_RotaryPassengerSway) | MultiplyCompound | ×2.5 |
| Stamina, suppression, damage taken | no modifier reaches spread or accuracy |
The stance multipliers inside StanceGameplayEffects act on recoil, not on spread (Weapons §5a); the stance enters the spread only through the SpreadValues row (§6). Final per-weapon attribute values are on Weapon Attributes.
13. Function table
All RVAs are file RVAs. “Same” means byte-identical in the static-analysis image.
| RVA | Role | Analysis image |
|---|---|---|
0x6B980A0 | UWDWeaponComponent start-fire (vtable +0x578) | same |
0x6B94700 | Timer_FireWeapon body: aim, zeroing, cone, rand() seed | same |
0x6B93610 | per-shot executor shared by client and server: quantise, spawn, record, recoil | same |
0x6B90610 | zeroing angle Z | same |
0x6B956B0 | ZeroTable[range] lookup | differs: dispatch stub; read from the reference image |
0x6B95630 | accuracy cone accuracy_rad × mult | same |
0x6B959E0 | InitialBulletSpeed × component+0x114 | same |
0x6B90780, 0x6B90890 | single-transform branch; transform post-processing (trace; not decoded) | same |
0x6BBD3D0 | FWDWeaponAccuracy to radians (MOA 0x3998825B, mrad 0x3A83126F) | same |
0x6BBA900 | projectile stats lookup with ammo modifier tag (cache, entry stride 0x328) | same |
0x6BBD500, 0x6BBD420, 0x6BBD5D0 | getters: InitialBulletSpeed, EffectiveRange × 100, ShotgunData | same |
0x6C25660 | shotgun pellet generator | same |
0x6C9E7D0 | ranged-weapon item extension Tick (primary vtable 0xB389290 +0x6D0) | same |
0x6C9FEE0 | UpdateSpread | same |
0x3CD6240 | GetSpreadAngle (interface slot +0xA8; also in three other vtables) | same |
0x6C974E0 | accuracy multiplier (interface slot +0xC0) | same |
0x6C987A0 | GetProjectileSpawnTransform (interface slot +0x18) | same |
0x6C98290 | GetProjectileData (interface slot +0x90) | same |
0x6C97710 | item attribute getter with default | same |
0x3FD4FA0 | FRandomStream::VRandCone (elliptical) | same |
0x1670B80 | FQuat::RotateVector | differs: dispatch stub; read from the reference image |
0x1676910, 0x1676A00 | ToOrientationQuat, ToOrientationRotator | same |
0x16793E0, 0x166D220 | FRotator::Vector, FRotator::Quaternion | same |
0x76DFB20, 0x76E0810, 0x76E0800 | rotator compress, decompress, shot-id counter | same |
0x6B97900, 0x6B97820 | Server_FireProjectile, Server_FireBuckshotProjectile implementations | same |
0x6B996D0, 0x6B99530 | their exec thunks | same |
0x5384040 | FInterpTo | same |
0x48AA830 | FBHScalableFloat::GetValueAtLevel | same |
0x6E6E260, 0x6B86240 | aim blend (+0x3E4) | same |
0x5296290, 0x5293150 | airborne test, stance byte | same |
0x699E6C0 | FWDWeaponStats constructor (defaults) | same |
0x6F1DDC0 | velocity from rotator and speed | same |
14. What is open
MaxPelletCountis 16 (DefaultGame.iniProjectileSystemSettings, Hit Registration §4.6); resolved in round 39. With 8 pellets andAmmunitionFactor0.8,P = 8/16 x 0.8 x ChokeFactor= 0.20 on the M500 (choke 0.5), 0.14 on the MP43 (choke 0.35), 0.26 with the Improved Cylinder Choke and 0.34 with the Full Choke on the M500 (§11).- Aim sway and the shot. Round 41: closed. Round 40 showed the shot direction is the camera manager’s final rotation (character virtual
+0x840-> controllerGetPlayerViewPoint0x42C8890). The aim-sway code (Weapons v2 §5a step 5) writes the per-frame sway change timesPlayerCameraOffsetMultiplierinto the controller rotation (0x6D54190) inCameramode, the native default (no cooked weapon setsAimSwayMode), so sway is in the view rotation and therefore in the shot direction. InWeaponmode only the weapon attach-point offset moves (animation), not the aim.bSpawnProjectileFromCameraWhenAdsis false on 25 stats assets and not set on the rest. - Per-shot bloom. None found. The negative result covers writers of ext
+0x3AC/+0x3B0in the item-extension code range and the single writerUpdateSpread; a store elsewhere in the 100 MB image was not excluded, and Blueprint-driven attribute changes were not traced. Round 41: no bloom-like field exists in the SDK (WDGame_classes.hpp,WDGame_structs.hpp: no match for bloom, spread increment, per-shot spread or consecutive shots); the only per-shot cooked key found in the weapon and item assets isFocusEnergyReductionPerShot(focus energy, not spread); the whole weapon attribute set (Attribute.Gameplay.Weapon.*, 12 attributes) has no per-shot term. Treated as absent. - Replication details. Only the wire record layout and seed handling are described here (§9); anything beyond that is not covered.
ZeroTable(component+0xFF8). Round 41: the completion callback0x6B8C8D0copies the trace result (0x6B95520is only the result’s copy constructor: success flag+8, then a 0x320-byte path record at+0x20), and storesZeroTable[range] = degrees(atan2(-d, 100 x range))withdthe double at result+0x68(the path record’s field at+0x48; read as the vertical offset of the predicted path at that range, in cm, negative when the bullet has dropped; the field name is not resolved). The samedis stored as a float in the map at+0x1048(keyed by range), and the angle again in the map at+0x1098, keyed by the pair (ammunition tag, range) via0x6B8CA20andGetTypeHash.atan2is0x15AE680. So the zero angle for a range is the angle of the line from the muzzle to the point where the game’s own predicted path lands, which is what a gun needs to be raised by to hit there. Open: which structure holdsd.- Vehicle and mounted weapons. Closed in round 42. Both actor classes implement the same weapon-owner interface at actor
+0x2B8, andGetSpreadAngleis its slot 21 (+0xA8).AWDVehicleWeaponExtension: slot0x7646C90returns 0 unless the data asset hasbDoesWeaponUseSpread(+0x88), else the float at actor+0x440; the updater0x764BED0(every tick, from0x764A780) sets it to 0 when the operator’s ADS blend (UWDWeaponBehaviorComponent +0x3E4) is exactly 1.0, otherwise moves it towardSpreadValue(data asset+0x90, level 0) byclamp(dt x 25, 0, 1)of the gap each tick.AWDMountedWeapon: slot 21 is0x15B83B0(return 0.0), so its native spread is always 0. The infantry updater0x6C9FEE0is not theirs. Evidence: round 42. Earlier round 41 assumption (float at actor+0x574) was wrong. - Character virtuals
+0x360(2D velocity),+0x720(eye point),+0x818(gate),+0x840(view rotation) are named by role only. - Frame
+0x270echo (θin degrees). Round 41: the loop at0x6B94480in the shot function0x6B93610calls virtual slot+0x60on every registered listener of the global list0xCEB6D70, passing the frame; the listeners are registered byUWDAnybrainSubsystem(list references in0x6DF3430,0x6DF4AB0, and its vtable slots 87 and 880x6E00470/0x6DFEB50). So the echo is a per-shot report to the Anybrain subsystem, not a gameplay input (the subsystem’s purpose was not read). The snapshot0x6C9DB60is a block inside the 3 KB function0x6C9D170; it packs+0x360,+0x370,+0x378, the stance byte and the two spread values into a record; the consumer of that record was not found. component+0x114(muzzle-speed scale, default 1.0). Round 40: the only writer is the exposed functionSetInitialBulletSpeedModifier(0x6B99A30); the readers are the spawn wrapper0x6B959E0and the shot function0x6B93610(0x6B9412B). The callers found are the bow actionsB_Action_BowDraw.TryActivateFireandB_Action_BowFireTestNew.OnReleased, which passDrawVelocityCurve(draw alpha): theCombatBowcurveCF_DrawVelocityReleaseCurveruns from 0.05 at 0 to 1.0 at 0.4 (FullDrawTime1.4 s). No firearm Blueprint was found calling it.- AI shooters (
AIWeaponData,AEB_EnemyAccuracyScaling) were not traced; they may use a different direction path. Round 39:AEB_EnemyAccuracyScalingis an audio-event behaviour (EnemyAccuracyScaling_Vehiclein the vehicle weapon fire events), not an accuracy value.AIWeaponDataon 12 stats assets holdsMinAIRange,MaxAIRange(150 to 650) andShootCountOfBurst(1 to 8) only; no AI spread value exists (round 36 AI). srandseed. Round 41: closed.0x46CF6AA-0x46CF6E8: unless the command line carries-Deterministic,-UseFixedTimeStepor-FixedSeed(wide strings at0xA77A640,0xA77A660,0xA77A68E), both seeds areQueryPerformanceCounterlow dwords (IAT0x9A27628, two calls): the first goes to the C runtimesrand(IAT0x9A28ED0), the second to the engine seeder0x15C48B0. With one of those flags both are the fixed value inr15d(the register is zero in the usual start-up; not verified). So the seed is not a constant.- Cooked effects only. Attribute changes made at runtime by Blueprint or by effects outside the 341 exported ones are not visible.
- Runtime check. Nothing here was observed running; the equations are static readings.
Related: Weapons §5a recoil, Game Features: Weapons, Weapon Attributes, Projectile flight, Damage Pipeline, Combat Data Flow, Units.