Wardogs — Projectile flight: thrust, drag and gravity
Project:
Kiyogitpy/wardogs-data-docs· Live site
How a projectile moves between the muzzle and the hit: launch velocity, the per-step force model (gravity, drag, thrust, wind), the integrator, and the conditions that end the flight. It answers one question first raised by an outside review: DA_155mm_Standard_ProjectileData sets bActorHandlesProjectileMovement: true, so does an artillery shell fly by different rules, and is drag modelled at all?
Short answer. Drag is modelled, in native code, and the artillery shell uses the same force model as every other projectile. The flag does not switch to a different physics; it switches who runs the step. For a normal projectile the projectile subsystem integrates the step itself (§3). For an actor-driven projectile the subsystem skips the integration and the actor’s mover mode (UWDProjectileMove, or UWDArtilleryProjectileMove) asks the same native helpers for gravity, drag, wind and thrust each tick (§7). What stays open is which mover mode the Blueprint actor BP_155mm_Standard selects and whether its Blueprint changes anything (§11); that needs the Blueprint export, not more native analysis.
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 restored image WardogsClient-Win64-Shipping.restored.exe (SHA-256 a5c3db10…2b4f0cc37) is the reading copy. The static-analysis image was byte-compared against it for every function cited here (§10): all are byte-identical except the weather wind getter 0x6F78270, which begins with a protection-layer dispatch stub in the analysis image and is read from the restored image, as IDA claim exposure prescribes. The decompilation corpus was used read-only to locate functions and field names; the constants and offsets were re-read from the bytes with _tools/projectile_flight_check.py. No game was run and no process was observed. Nothing here was fitted to calculator or in-game measurements.
| 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
Per fixed step of dt seconds, for a projectile with stats S (FWDProjectileStats), velocity v (cm/s) and position x (cm):
a = g + a_thrust + a_drag + a_wind (cm/s^2, vector)
v1 = v + a * dt then, if S.MaxSpeed > 0: |v1| <= S.MaxSpeed
dx = (v + v1) / 2 * dt (trapezoid)
sweep a trace x -> x + dx; on a hit the step stops at the hit fraction f
| Term | Formula | Section |
|---|---|---|
Gravity g | (0, 0, G) when S.bAffectedByGravity (default true), G = UPhysicsSettings default object +0x38 (project gravity, cm/s²); else 0. Scaled by a fuel curve while thrusting | §4 |
Drag a_drag | -(100/m) · ½ρ · Cd · (π/4)(D/100)² · (|v|/100)² · v̂ with ρ = 1.2, Cd = DragModel.Value × Curve(|v|/100), D cm, m kg | §5 |
Thrust a_thrust | ThrustSpeed · v̂ (cm/s², despite the name) while fuel remains and the start delay has passed | §6 |
Wind a_wind | 0.75 · Wind · clamp((T² − X²)/X², 0, 1), T = absolute path travelled | §6.4 |
| Step | dt = floor(1000 / Rate) · 0.001 s, Rate = SimulationFrameRate = 30 (DefaultGame.ini), so 33 ms | §3 |
Launch speed is the weapon’s InitialBulletSpeed (cm/s), not the projectile’s TypicalSpeed (§2). ThrustSpeed is a cm/s² acceleration, not a cruise speed (§6). All lengths are centimetres, all times seconds, the drag level is metres per second and mass is kilograms (§9).
2. Launch velocity
Native. The fire path 0x6F1DDC0 builds the spawn request. Its velocity is a unit direction times unk_146BBD500(weaponStats), and that getter is movss xmm0,[rcx+0xDA0]; ret (0x6BBD500): FWDWeaponStats.InitialBulletSpeed (SDK +0xDA0). The same request stores the projectile’s TypicalSpeed (FWDProjectileStats +0x1FC, copied at 0x6F1DFB0) and EffectiveRange × 100 (0x6BBD420: [stats+0xDA4] × 100.0, so EffectiveRange is metres and the request holds centimetres).
TypicalSpeed is not read by the integrator (0x6F3C060) or by the mover mode’s move generator (0x76D8A00). Its readers found are the suppression scaler (0x6F3B0E0, which scales suppression by speed / TypicalSpeed when the round is slower than TypicalSpeed; read at 0x6F3B9BE) and the ammo modifier multiplier (0x6BBAB70, which multiplies +0x1FC by a modifier value). The constructor default is 60000 cm/s (0x699DEE0).
Ammo modifier applier (round 39, static). 0x6BBAB70(stats, out, modifierAsset) multiplies fields of a per-ammo copy of the projectile stats by the values in AttributeModificationMultipliers; each key is matched by a gameplay-tag global. The tag-to-field map read from the tag registration functions: BaseDamage +0x30, ExplosiveRadius +0x34, MinimumExplosiveDamage +0x38, EffectiveExplosiveRadius +0x3C, ExplosiveDamageFallOff +0x40, Length +0x68, Diameter +0x6C, Mass +0x70, DragModelValue +0x78, TypicalSpeed +0x1FC, EffectiveRangeMultiplier +0x200, PenetrationEffectivenessMultiplier +0x208, ThrustSpeed +0x2B0, Suppression.BaseValue +0xB8, Suppression.DistanceFalloff.Min and Max +0xBC and +0xC0, Suppression.FriendlySafeRadius +0x150, Suppression.FriendlyMultiplier +0x154, TravelDistanceWhizBy +0x2D4, DistanceToPlayerWhizBy +0x2D8, TracerChance +0x1F4 (assigned, not multiplied) and PenetrationExitSpeedMultiplier +0x20C. The five DA_AmmoModifier_* assets set only EffectiveRangeMultiplier, PenetrationEffectivenessMultiplier, PenetrationExitSpeedMultiplier and TracerChance, so no ammo type changes TypicalSpeed, and InitialBulletSpeed is a weapon field the applier never touches. The weapon-component spawn routine 0x6B747F0 builds the request range as EffectiveRange x 100 (0x6BBD420) times GetProjectileStats(projectile, ammoTag)+0x200, so the armor-piercing 1.2 and hollow-point 0.8 multiply the range value that the spawn request carries (the value section 6.4 reads as X; inferred). The infantry fire path 0x6F1DDC0 calls GetProjectileStats with the global at 0xD2692F8, which is never written and is used as the empty gameplay tag elsewhere, and 0x6BBA900 returns the base stats when the tag is 0. Constructor defaults of the projectile stats (0x699DEE0): MaxProjectileLifetime (+0x188) 10.0 s (0x41200000, store at 0x699E09B), TypicalSpeed 60000, EffectiveRangeMultiplier 1.0. The bullet projectile assets do not set MaxProjectileLifetime, so bullets live 10 s.
| Quantity | Value | Source |
|---|---|---|
155 mm artillery InitialBulletSpeed | 30000 cm/s (300 m/s) | Data, DA_TNK_01_Artillery_Weapon_Stats |
ArtilleryBarrelTest InitialBulletSpeed | 46400 cm/s | Data, DA_ArtilleryBarrelTest_Stats |
155 mm TypicalSpeed | 15000 cm/s (150 m/s) | Data, DA_155mm_Standard_ProjectileData |
155 mm ThrustSpeed | 22000 (unused, see §6.1) | Data |
So the artillery shell leaves the barrel at 300 m/s while its own TypicalSpeed says 150 m/s. The two are different fields with different jobs. Whether InitialBulletSpeedModifier (UWDWeaponComponent +0x114, SDK) is folded into the weapon stats before 0x6F1DDC0 is open (§11).
3. Which code runs the step
Native. UWDProjectileSubsystem is a tickable world subsystem (vtable 0xB440498, constructor 0x6F2AAA0; its tickable sub-object vtable 0xB440798, slot 0 = Tick = 0x6F3BC70). Each live projectile is one 800-byte (0x320) FWDProjectileInstance (SDK). Instance fields used here, relative to the instance:
| Offset | Field | Note |
|---|---|---|
+0x08 | Location (3 doubles) | FWDProjectileMovementData.Location |
+0x20 | Velocity (3 doubles) | |
+0x38 | CurrentFuel (float) | initialised from S.TotalFuel (0x6F30E50: inst+56 = stats+540) |
+0x40 | TotalAbsoluteMove (3 doubles) | per-axis sum of |dx| |
+0x58 | TotalAbsoluteWindDrift (3 doubles) | |
+0xC4 | float X (wind threshold, §6.4) | initialised from the spawn request (0x6F30E50, request +180) |
+0xC8 | state byte | 1, 2 = flying (simulated); 3 = post-impact countdown (float at +0xDC); 4 = not simulated (meaning not traced) |
+0xD8 | max lifetime (float) | inst+216 = stats+392 = MaxProjectileLifetime (0x6F30E50) |
+0x128 | actor handles movement | inst+296 = stats+496 = bActorHandlesProjectileMovement (store at 0x6F3A535–0x6F3A53C, in 0x6F39940) |
+0x290 | ElapsedTime (double) | |
+0x2E0 | step accumulator (double, ms) |
Tick (0x6F3BC70; the function with the fixed-step accumulator, slot 0 of the tickable sub-object vtable):
step_ms = floor(1000 / Rate) Rate = int at tickable +0x40C (subsystem +0x444)
for each instance:
acc_ms += DeltaTime * 1000
if acc_ms >= step_ms: acc_ms -= step_ms
dt = step_ms * 0.001 (xmm6 passed in xmm2 to 0x6F3CB80)
if inst.+0x128 (actor handles movement): no native integration, instance kept alive
else: 0x6F3CB80(inst, dt) integrates; a false return removes the instance
The actor-driven exemption is explicit in three places: the spawn code copies the flag to inst+0x128 (0x6F39940), Tick branches on it at 0x6F3BE31, and the step driver 0x6F3D130 returns “alive” for it before it would call 0x6F3CB80 (0x6F3D164: cmp byte [rbx+0x128],0). 0x6F3CB80 itself returns 0 immediately when inst+0x128 is set.
Rate is SimulationFrameRate, and it is 30. (Round 23.) The int at tickable +0x40C (subsystem +0x444) is FWDProjectileSystemSettings.SimulationFrameRate (SDK +0x1A4), the last field of the ProjectileSystemSettings struct the subsystem holds at +0x2A0:
| Step | Where | Value |
|---|---|---|
| Class default | the struct constructor 0x6F2A600 stores 0x1E at struct +0x1A4 (0x6F2A792); the subsystem constructor 0x6F2AAA0 calls it on +0x2A0 (0x6F2ACE3) | 30 |
| Config | Wardogs/Config/DefaultGame.ini line 303, [/Script/WDGame.WDGameSettings], ProjectileSystemSettings=(…,MaxConsecutiveFullRefundedSubsteps=10,SimulationFrameRate=30) (SHA-256 56fda182…33f1bd, the same file as round 8 config) | 30 |
| Load | subsystem Initialize (0x6F37F12–0x6F37F25) resolves the WDGameSettings object (0x6C03540) and copies its +0x458 struct into +0x2A0 with the copy-assign 0x6F2C180, which copies +0x1A4 at 0x6F2C427–0x6F2C432 | 30 |
No other write to subsystem +0x444 (or to the tickable +0x40C) exists in 0x6F20000–0x6F50000; the settings object’s own constructor initialises the same struct through the same 0x6F2A600 (0x6C02F3A). The field is read at four sites, each computing floor(1000/Rate) the same way (cvtdq2pd, divsd by 1000.0, roundpd mode 3 = truncate; Rate = 0 gives a step of 0): Tick (0x6F3BCB7), and 0x6F2C72C, 0x6F3D798, and 0x6F34343, which stores the step (entry+0x100) and the instance accumulator (entry+0x108) in the per-handle tracer-presentation entry (map at +0x140, stride 0x168). So:
- Fixed, not variable:
dt = floor(1000/30) = 33ms =0.033s,Tickaccumulates real frame time and runs at most one step per tick (acc ≥ 33 → acc −= 33; an accumulator within an epsilon of zero is snapped to 0). A frame longer than 33 ms leaves a backlog that drains one step per later tick. - Not replicated as a value: nothing in the traced readers sends it. Each machine reads its own
DefaultGame.ini(the same shipped file), so server and client agree by configuration. - Only for native-stepped projectiles. Actor-driven projectiles (
bActorHandlesProjectileMovement, §7: the 155 mm shells, 120/125 mm, 122 mm, 93 mm and others) use the mover time step instead.DefaultNetworkPrediction.inisetsPreferredTickingPolicy=FixedandFixedTickFrameRate=30, i.e.1/30s = 33.3 ms if the projectile mover ticks on the fixed policy (not confirmed forBP_155mm_Standard). Against_tools/shell_sim.pythe difference between 33 and 33.3 ms is 0.01 m. - Build check: the settings constructor, the copy,
InitializeandTickare byte-identical in the analysis image and the older reference image; the readers above were read in the reference image.
0x6F3CB80 loops the substep 0x6F3C060 until the step’s time is consumed (each call returns the unused fraction (1 − f)·dt after a hit) and then runs the hit and replication bookkeeping.
4. Gravity
Native, in the integrator (0x6F3C060, flag read at 0x6F3C16B) and in the helper 0x6F2E4B0 that the mover mode also calls:
if S.bAffectedByGravity (+0x2E0): g = (0, 0, G) with G = float [ [Class(PhysicsSettings)+0x110] + 0x38 ]
else: g = (0, 0, 0)
0x42AA2C0 resolves the class /Script/Engine.PhysicsSettings (the strings PhysicsSettings and /Script/Engine are loaded at 0x42AA2F4–0x42AA315); +0x110 is UClass::ClassDefaultObject and +0x38 is the first property of UPhysicsSettings, DefaultGravityZ. So the projectile gravity is the project’s physics gravity, in cm/s². The x and y components are the zero vector (0xCCA3468, 0xCCA3478). bAffectedByGravity defaults to true (0x699DEE0, store at +0x2E0), and the 155 mm assets do not override it.
Value. DefaultGravityZ is −980.0 cm/s², read from [/Script/Engine.PhysicsSettings] in the engine base config packed in the game paks; the game’s own DefaultEngine.ini sets no gravity key and the WorldSettings of the nine map files set no bGlobalGravitySet or GlobalGravityZ (see Artillery shell flight data). Not checked: gravity-override volumes in the world-partition cells of the four large maps.
Fuel curve. While thrust is active (§6.1) and S.FuelGravityAffect (+0x228, a FRuntimeFloatCurve) has keys, gravity is multiplied by Curve(CurrentFuel / TotalFuel) (0x6F2E5A0–0x6F2E5C9: xmm1 = fuel / [stats+0x21C], then the curve evaluator 0x3E07720). With no thrust the multiplier is 1.
5. Drag
Native, 0x6F2E240 (called as (subsystem, out, stats, &velocity)):
Cd = DragModel.GetValueAtLevel( float(|v| * 0.01) ) // 0x6BBA8A0 -> 0x48AA830
k = D^2 * rho * Cd * (pi/8 * 1e-4) // D = stats+0x6C, rho = 1.2
F = k * (|v|^2 * 1e-4) // newtons, = 0.5 rho Cd (pi/4)(D/100)^2 (|v|/100)^2
m = max(stats+0x70, 0.001) // kg
a_drag = -100 * F / m * v_hat // cm/s^2, opposite to v
v_hat = v / |v| (zero vector if |v|^2 <= 1e-8)
Constants (each re-read from .rdata by the check tool):
| Constant | Value | RVA |
|---|---|---|
π/8 × 10⁻⁴ | 3.92699076e-05 (float) | 0xB440AF0 |
10⁻⁴ | 9.99999955e-05 (double) | 0x9C083B0 |
−100 | double | 0xA4E07F8 |
air density ρ | 1.2 (float), kg/m³ | 0xA3BEF68; the weather subsystem’s getter 0x6F77F90 returns 1.2 from 0xCED2C4C |
| level scale | 0.01 (double) | 0x9AD4640 |
| minimum mass | 0.001 (float) | 0x9AAFD68 |
Reading the code: ρ starts as the default float 1.2; if the world has a WDWeatherSubsystem (class name string at 0x6F777D6, looked up through 0x6F29130, world subsystem map at +0x9D0) it is replaced by the getter’s result, which is the constant 1.2. So ρ = 1.2 in every case in this build.
The drag coefficient lookup. 0x6BBA8A0 takes stats+0x68 (the FWDProjectileDimensions), adds 0x10 to reach DragModel (FBHScalableFloat, stats +0x78) and tail-calls the scalable-float evaluator 0x48AA830, with the level float(|v| × 0.01): speed in metres per second. The evaluator (0x48A8480) returns Value × RichCurve.Eval(level) when the curve-table row resolves, else Value. So:
Cd(v) = DragModel.Value * Curve[row](|v| in m/s)
The row is DragModel.Curve.RowName in CT_Projectile_DragCoefficients, whose Time axis is therefore m/s (the G7 and G1 rows rise sharply at 274–366 on that axis, which is where the speed of sound sits). The 155 mm assets use row GC (Cylinder): 0.71 at 0, 0.66 at 150, 1.10 at 300, 1.60 at 600. DragModel.Value defaults to 1.0 when the cooked asset omits it (the FWDProjectileStats constructor 0x699DEE0 stores 0x3F800000 at +0x78), so rows without a Value (for example .50 Cal) use the bare curve.
Diameter and mass. D = stats+0x6C is in centimetres (the area is converted with the 10⁻⁴ factor; .50 Cal carries 1.2954) and m = stats+0x70 in kilograms. Constructor defaults: Length 1.5, Diameter 0.9, Mass ≈ 0.00746.
Mach and altitude. There is no altitude or temperature term; ρ is a constant, and the only speed dependence is the Cd curve.
6. Thrust, fuel and wind
6.1 Thrust
Native, 0x6F30280 (also called from the mover move generator). Active when
S.bShouldThrust (+0x219) AND CurrentFuel > 0
AND ( |S.ThrustStartDelay (+0x2B4)| <= 1e-8 OR S.ThrustStartDelay < Elapsed )
then, with bHasThrustInstability (+0x2B8, read as +696 at 0x6F30280) false:
a_thrust = ThrustSpeed (+0x2B0) * v_hat // cm/s^2, along the current velocity
and zero otherwise. ThrustSpeed is therefore an acceleration in cm/s² along the velocity, not a speed. With bHasThrustInstability true the function adds a lateral term proportional to |v|, to ThrustInstabilityYawScale (+0x2BC) and to ThrustInstabilityPitchScale (+0x2C4), built in the basis of the velocity; the two scalar factors come from two runtime functions (0x1498F816A, 0x1498F815E) that were not resolved. No 155 mm asset sets bHasThrustInstability.
Constructor defaults (0x699DEE0): bShouldThrust false, TotalFuel 1000, FuelBurnRate 10, ThrustSpeed 0, ThrustStartDelay 0, MaxSpeed 0. The cooked 155 mm assets set ThrustSpeed: 22000 but never set bShouldThrust, so the 155 mm shell does not thrust; the 22000 is inert (the same holds for 84mm, 120x570mm, 122mm and others that carry ThrustSpeed without bShouldThrust). Assets that do thrust:
| Asset | bShouldThrust | ThrustSpeed | TotalFuel | FuelBurnRate | ThrustStartDelay | MaxSpeed | Burn time TotalFuel/FuelBurnRate |
|---|---|---|---|---|---|---|---|
DA_72mm | true | 17500 | 180 | 30 | 0.35 s | 20000 | 6.0 s |
DA_93mm | true | 17000 | 100 | 40 | 0.04 s | 22000 | 2.5 s |
DA_122_mm_HAVOC | true | 20000 | 1000 (default) | 10 (default) | 0 | 0 | 100 s (defaults) |
All from Data plus the defaults above. DA_DebugHomingRocketData sets 20 s, fuel 100, burn rate 1.0 (see Items §7c).
6.2 Fuel burn
Native (0x6F3C060, FuelBurnRate read at 0x6F3C842): after a step, if the thrust vector is non-zero,
CurrentFuel -= FuelBurnRate (+0x220) * dt * f // f = step fraction completed
so fuel is consumed per second of powered flight at FuelBurnRate units per second, and thrust ends when CurrentFuel <= 0.
6.3 Speed cap
S.MaxSpeed (+0x2CC): after v1 = v + a·dt, if MaxSpeed > 0 the vector is clamped to that length (MaxSpeed read at 0x6F3C449, then 0x165ED30, FVector::GetClampedToMaxSize, tolerance 1e-4). Default 0 means no cap. Cooked values: 72mm 20000 cm/s, 93mm 22000 cm/s.
6.4 Wind
Native. 0x6F30680 (mover path) and the matching inline code in 0x6F3C060:
t = clamp( (T^2 - X^2) / (2X^2 - X^2), 0, 1 ) // X = inst+0xC4, T^2 = |TotalAbsoluteMove|^2 (inst+0x40..)
a_wind = Wind * t * 0.75 // 0.75 = float at 0xCED1BA8
Wind is a 3-vector held by WDWeatherSubsystem (0x6F78270 returns the override vector at +0xB8 if its flag at +0xD0 is set, else the current vector at +0xA0). TotalAbsoluteWindDrift accumulates a_wind·dt. The wind therefore fades in with the distance already travelled and is at full strength once T² ≥ 2X². Inferred: X is EffectiveRange × 100 cm, because the fire path stores that value in the spawn request next to the velocity (§2); not confirmed. For the artillery weapon that would put the onset at 10 km.
7. The actor-driven path (bActorHandlesProjectileMovement)
Native. AWDProjectile (SDK, size 0x3B0) owns a UBHMoverComponent (+0x2D8). Its movement modes derive from UWDProjectileMove (vtable 0xB676380); the artillery shell’s mode would be UWDArtilleryProjectileMove (vtable 0xB6720E8, SDK fields InputMultiplier +0x90 and bIgnoreVerticalInput +0x94), and UWDGlideProjectileMove (vtable 0xB673188) derives from that. Their overridden slots:
| Slot | UWDProjectileMove | Role |
|---|---|---|
| 98 | 0x76D8A00 | GenerateMove: the force model |
| 99 | 0x76D9110 | simulation tick: applies the move and traces |
| 94 | 0x76D7B90 | shared with Artillery and Glide |
GenerateMove (0x76D8A00) looks up the same native FWDProjectileInstance through the subsystem (0x6F34FE0, keyed by the projectile handle read from the owning actor), reads its stats with 0x6BBA900, and then calls the same helpers as §3 in this order:
a_grav = 0x6F2E4B0(subsystem, inst, stats) // gravity (+ fuel curve)
a_drag = 0x6F2E240(subsystem, stats, &syncState.Velocity) // drag, §5
a_wind = 0x6F30680(subsystem, stats, &Location, X, |TotalAbsoluteMove|^2) // wind, §6.4
a += 0x6F30280(subsystem, inst, stats, &Velocity, ...) // thrust, only if a sync state exists
v1 = v + (a_grav + a_drag + a_wind [+ a_thrust]) * dt // dt = mover time step
then writes the proposed linear velocity into the FProposedMove (flag byte 0x80 at +1). When 0x6F307C0 returns true, 0x6F2E620 (4370 bytes, probably the guided-flight steering driven by FWDProjectileGuidance, stats +0x158; not traced) supplies the base velocity instead of the current one. The tick 0x76D9110 integrates exactly as §1: displacement (v_new − v_old)·dt/2 + v_old·dt, a sweep trace named Trace_Mover_Projectile (0x143CB56B0), and impact handling through the native instance (0x6F36F40).
UWDArtilleryProjectileMove’s move generator (0x76CC730) calls the base GenerateMove, then reads the WDProjectileSyncState component at +0x80 and passes the proposed velocity through 0x1670CB0 (build a rotation from the state, transform the vector), writing the result back. It looks like a steering rotation applied after the force model; InputMultiplier and bIgnoreVerticalInput are the SDK tunables. That function was not traced further.
What this means for the review question. The actor does not run independent physics. If the Blueprint BP_155mm_Standard uses a UWDProjectileMove-family mode, its flight is the model of §1 with the 155 mm stats. If the Blueprint replaces the mode or moves the actor in its own event graph, that part is Blueprint bytecode and is documented from the Blueprint export, not here. Native hooks the actor can call regardless: GetCurrentVelocity, GetCurrentLocation, GetCurrentTravelTime, GetExpectedTravelTime, GetSpawnLocation, GetSpawnVelocity, GetProjectileData (SDK AWDProjectile functions), plus the WDProjectileSubsystem queries (IsProjectileHandlingMovementExternally).
8. Worked values for the 155 mm shells
Derived from §1 and §5 with the cooked data. Gravity G = −980 cm/s² (assumed, see §4). a_drag below is the deceleration only, at the speed shown, with ρ = 1.2 and the linear interpolation of the GC (Cylinder) row.
| Asset | DragModel.Value | D cm | m kg |
|---|---|---|---|
DA_155mm_Standard (and _SB, Expensive_SB) | 80 | 8.4 | 70 |
DA_155mm_Expensive | 2.5 | 10.5 | 19 |
| Speed | Cd Standard | a_drag Standard | Cd Expensive | a_drag Expensive |
|---|---|---|---|---|
| 50 m/s | 53.7 | 6.4 m/s² | 1.68 | 1.2 m/s² |
| 100 m/s | 51.4 | 24.4 m/s² | 1.61 | 4.4 m/s² |
| 150 m/s | 52.8 | 56.4 m/s² | 1.65 | 10.2 m/s² |
| 200 m/s | 56.2 | 106.8 m/s² | 1.76 | 19.2 m/s² |
| 300 m/s | 88.0 | 376 m/s² | 2.75 | 67.7 m/s² |
Two consequences that follow from the equations (not measurements):
- Setting
a_drag = 9.8 m/s²(drag balancing gravity alone) gives a terminal speed of about 62.6 m/s for the Standard shell and about 147.5 m/s for the Expensive shell. The Expensive figure sits next to itsTypicalSpeedof 150 m/s. That is an observation about the numbers, not evidence thatTypicalSpeedis derived from drag. - A shell launched at 300 m/s with
Value = 80loses most of its speed within a few seconds; the Expensive shell withValue = 2.5keeps a long, flat flight.DragModel.Value = 80on the Standard asset (and on the_SBcopies) is a very large multiplier next to the other 155 mm and 120 mm assets (2.5 to 11.5); whether it is intended is a data question.
These numbers hold only if the shell flies through the native force model (§7). They say nothing about the Blueprint.
9. Units
| Quantity | Unit | Evidence |
|---|---|---|
Position, displacement, Diameter, Length | centimetres | UE world units; 0xB440AF0 and 0x9C083B0 apply 10⁻⁴ to cm² |
Velocity, InitialBulletSpeed, TypicalSpeed, MaxSpeed | cm/s | |v| × 0.01 is passed as the drag level; the 10⁻⁴ factor converts |v|² to m²/s² |
ThrustSpeed | cm/s² (acceleration), mis-named | added to a and multiplied by dt (0x6F3C060) |
| Gravity | cm/s² | UPhysicsSettings default, UE convention |
Mass | kg | a = F/m then × 100 (m/s² to cm/s²), 0x6F2E240 |
| Air density | kg/m³ (1.2) | |
Drag curve Time axis | m/s | level is |v| × 0.01 |
EffectiveRange | metres | 0x6BBD420 multiplies by 100.0 |
FuelBurnRate | fuel units per second | 0x6F3C060 multiplies by dt |
Times (ArmingDelay, ThrustStartDelay, MaxProjectileLifetime) | seconds | compared with ElapsedTime (seconds) |
Docs that said otherwise: GameMechanics §3 called Thrust “the sustained speed”; it is an acceleration (corrected there). docs/UNITS.md already had InitialBulletSpeed in cm/s and projectile Mass in kg; its curve-axis list now includes the drag curve.
10. Arming, lifetime and detonation
- No arming distance.
FWDProjectileStats(SDK) hasbHasArmingDelay(+0x180) andArmingDelay(+0x184), both time-based; there is no minimum arming distance field. Defaults: no delay. - Arming delay. In the hit-event builder (
0x6F344D0), at0x6F346ED–0x6F34711:bHasArmingDelayand(ArmingDelay − 0.0001) > ElapsedTimetake the branch that attaches the global tag at0xD3815F8to the hit event (an unarmed marker; its consumer was not traced). 155 mm Standard has no delay; 155 mm Expensive has 1.3 s; 120x800 0.1 s, 84 mm 0.03 s, 72 mm 0.13 s, 93 mm 0.099 s, 40 mm 0.07 s. - Lifetime.
alive = (inst+0xD8 ≥ ElapsedTime) AND inst+0x28C == 0(0x76E07D0).MaxProjectileLifetimedefault 10 s; 155 mm 40 s. The native subsystem applies it only to projectiles it integrates; for actor-driven projectiles the native check is skipped (0x6F3D130) and the actor’s own lifetime fields apply (SDKbHandlesOwnLifetime,ExtendedLifetime). - Detonation by impact. The sweep trace of §1 ends the step at the hit fraction
f; the impact is processed by0x6F36F40. The hit event also carries|v| × Mass(0x6F346D1–0x6F346E9). Penetration and ricochet usebShouldPenetrate(default true, the 155 mm assets set false) andMinImpactAngleForRicochet(default 10°, the 155 mm assets set 91°, which disables ricochet). - Pre-simulation.
bShouldPreSimulate(default true; the 155 mm assets set false) makes the spawn code run up to three substeps at once (0x6F37510).
11. Function table
All RVAs are file RVAs. “Same” means byte-identical in the static-analysis image; the check tool prints it with WARDOGS_ANALYSIS_EXE set.
| RVA | Size | Role | Analysis image |
|---|---|---|---|
0x6F3BC70 | 870 | UWDProjectileSubsystem tickable Tick: fixed-step accumulator | same |
0x6F3CB80 | 1449 | step driver: loops the substep, bookkeeping | same |
0x6F3C060 | 2840 | substep integrator: gravity + thrust + drag + wind, clamp, trace | same |
0x6F2E240 | ~0x270 | drag acceleration | same |
0x6F2E4B0 | ~0x170 | gravity with the fuel-gravity curve | same |
0x6F30280 | 1022 | thrust acceleration | same |
0x6F30680 | ~0x13A | wind acceleration | same |
0x6F78270 | small | weather wind vector getter | differs: dispatch stub in the analysis image; read from the restored image |
0x6F77F90 | 9 | weather air-density getter (1.2) | same |
0x6F30E50 | 555 | instance initialisation (fuel, lifetime, X) | same |
0x6F39940 | 5116 | spawn: copies bActorHandlesProjectileMovement to inst+0x128 | same |
0x6F3D130 | ~0x94 | per-instance step driver; actor-driven returns early | same |
0x6F37510 | 572 | post-spawn setup, pre-simulation | same |
0x76E07D0 | 36 | lifetime test | same |
0x6F1DDC0 | 1075 | fire path: spawn request, launch velocity | same |
0x6BBD500 | 8 | InitialBulletSpeed getter (+0xDA0) | same |
0x6BBD420 | 17 | EffectiveRange × 100 (+0xDA4) | same |
0x6BBA900 | 618 | stats lookup with ammo modifier | same |
0x6BBAB70 | 1243 | ammo modifier multipliers on stats fields | same |
0x6BBA8A0 | 0x40 | drag-model level eval (|v|×0.01) | same |
0x48AA830, 0x48A8480 | 0x90, 430 | FScalableFloat::GetValueAtLevel | same |
0x165ED30 | 0xE0 | FVector::GetClampedToMaxSize | same |
0x699DEE0 | 0x395 | FWDProjectileStats default constructor | same |
0x6F3B0E0 | 2949 | suppression scale using TypicalSpeed | same |
0x76D8A00 | 986 | UWDProjectileMove GenerateMove | same |
0x76D9110 | 2727 | UWDProjectileMove simulation tick | same |
0x76CC730 | 140 | UWDArtilleryProjectileMove move generator | same |
Vtables: UWDProjectileSubsystem 0xB440498 (tickable 0xB440798), UWDProjectileMove 0xB676380, UWDArtilleryProjectileMove 0xB6720E8, UWDGlideProjectileMove 0xB673188. Several of the cited addresses (0x6F2E240, 0x6F2E4B0, 0x6F30680, 0x6F3D130, 0x76E07D0) have no row in the decompilation corpus; they were read as raw bytes. The corpus’s decompiled output of 0x6F3C060 and 0x6F3CB80 shows some helpers as unk_* calls; their bodies are the ones listed.
Reproduce:
python _tools/projectile_flight_check.py
WARDOGS_ANALYSIS_EXE=<analysis image> python _tools/projectile_flight_check.py
12. What is open
- Which mover mode
BP_155mm_Standarduses, and what its Blueprint does. The Blueprint export (see Artillery shell flight data) showsBP_155mm_Standarduses modeMover.Mode.Projectile.Standard(WDProjectileMove, emptyDA_DummyMovement) and its bytecode only spins the mesh and computes travel-time fractions, so §7 applies to it.BP_155mm_Expensiveuses theArtillerymode (WDArtilleryProjectileMove) withInputMultiplier0.02, whose rotation step is not traced. If the actor’s Blueprint callsSet Velocity, adds its own forces, or overrides the mode, §8 does not apply to it. - World gravity. Resolved: −980.0 cm/s² (§4). Open only: override volumes in world-partition cells, and any gravity scale applied by the artillery mode.
Rate. Resolved in round 23:SimulationFrameRate= 30 (class default andDefaultGame.ini), so the native step is 33 ms (§3). Still open: the mover time step of the actor-driven path (probably theFixedTickFrameRate=30fixed policy, 33.3 ms), and whether any server config overrides the value.- Subsystem flag
+0xB0. When set,Tickonly advancesElapsedTimeand checks the lifetime, without integrating (0x6F3BE3A). Its meaning (client replay, server verification, or a mode switch) is not established. - Launch speed modifiers. Whether
InitialBulletSpeedModifieror attachment modifiers changeInitialBulletSpeedbefore the spawn request (§2). Round 39: ammo-type modifiers do not (§2, ammo modifier applier);component+0x114is applied to the pellet speed in the shotgun path (Weapon Spread §11). Xof the wind ramp (§6.4): initial value and meaning (inferredEffectiveRange × 100); the unit of the wind vector. Round 39: the spawn request range isEffectiveRange × 100 ×the ammo’sEffectiveRangeMultiplier(AP 1.2, HP 0.8; §2), still not seen to be the same field asX.- Thrust instability (§6.1): the two scalar functions
0x1498F816Aand0x1498F815E. - Artillery mode rotation (§7): what
WDProjectileSyncState +0x80holds and howInputMultiplierfeeds it. - Arming tag
0xD3815F8(§10): consumer and meaning; whether the 155 mm shell is blocked from detonating by it. DragModel.Value = 80on the Standard 155 mm (and_SB) assets against 2.5 on the Expensive asset: data intent unknown; §8 shows its effect. The survey of every projectile asset (round 23) finds 80 only on the 155 mm Standard family andDA_155mm_Expensive_ProjectileData_SB, which carries the Standard numbers (70 kg, 8.4 cm).- Actor-driven lifetime. Which code ends an actor-driven projectile at
MaxProjectileLifetime(40 s for 155 mm). - Runtime check. Nothing here was observed running; the equations are static readings.
Related: Damage Pipeline §7.8 (speed scale after drag), Combat Data Flow, Units, native defaults, artillery range check (the model against logged impacts and the calculator table).