Combat Subsystem¶
Binary:
MW4Mercs.exe| Classified VAs: 141 | Exhaustive index: MW4Mercs.exe
In-mission weapon fire, launch/guidance, hit resolution, and damage application.
Role and boundary¶
Combat owns the player-mech fire pipeline and the damage path it feeds:
input -> command -> fire eligibility -> weapon update
-> missile spawn -> flight/guidance -> hit resolution
-> damage application -> combat state
The AI uses the same missile, guidance, and damage primitives; the AI combat detail only decides when to engage. Combat publishes events to the renderer for muzzle flashes and explosions but does not own presentation. The network layer owns the wire carriers; combat owns the payload semantics those carriers transport.
Input and command layer¶
The input path starts at the window procedure FUN_0072E580, which maintains
a 256-bit key/mouse bitmap at 0x0087DDC4. FUN_007238D0/FUN_007239A0
set and clear bits; FUN_007239F0 copies the bitmap to 0x0087DDA4 once per
frame. FUN_00723A30/FUN_00723B40 query the action state for a virtual key.
Commands are dispatched through the generic dispatcher FUN_004D5B70. It
reads the table object at [this+4], whose entries are 12 bytes
{id, name, handler} and are indexed by event[+4] - 1; it calls the handler
with the receiver in ECX. The command table is at 0x0077B9D8: 131 entries
with ids 6 through 136.
The fire-related command handlers are state trackers, not firing code:
| Command (id) | Handler | Counter |
|---|---|---|
FireWeapon (82) |
0x00556D80 |
[this+0x46C] |
WeaponGroup1 (42) |
0x00554BF0 |
[this+0x470] |
WeaponGroup2 (43) |
0x00554C70 |
[this+0x474] |
WeaponGroup3 (44) |
0x00554CF0 |
[this+0x478] |
| fourth group | - | [this+0x47C] |
ToggleGroupWeaponMode (143) |
0x00554B90 |
- |
NextWeaponGroup / PrevWeaponGroup (48/49) |
0x005553D0 / 0x00555590 |
- |
NextEnemy (67) |
0x00555E30 |
target designator |
The group handlers test the byte at [this+0x589]. When it is zero they apply
the weapon-control gate (FUN_0061ACF0/FUN_0061AD20, mask 8 of the object
at 0x007F2938) and increment/decrement the counter; when it is non-zero they
take the ungated decrement/clamp path. The byte is only cleared by the
init/reset functions FUN_0054A880 and FUN_00549A70.
Fire driver¶
FUN_005541C0 is the per-frame fire driver. For each group counter greater
than zero it calls FUN_00553F20(group). That function dispatches through
[this+0x8E0 + group*4]: zero selects FUN_00553F90, one selects
FUN_00553DB0.
The driver is gated by FUN_00553D20 ("any trigger active") and by the update
chain in FUN_0054E988:
[+0x4AC] != 0;- low byte of
[+0x4B0] == 0; FUN_00553D20() != 0;[*(+0xE0)+0x14] == 2;[*(+0x384)+0x10] & 0x80000 == 0;[*(+0x384)+0x2F8] == 0;- low byte of
[+0x5AC] == 0.
FUN_00553BC0 is the group-select action. If any gate fails, the counters
remain set but no missile is spawned.
Launch transform and missile spawn¶
The launch transform is FUN_005E219B (__thiscall, RET 8; this is the
weapon entity). It builds a launch configuration on a 0x1C4-byte frame and
dispatches the spawn. Its inputs are:
- the weapon record from the pool at
[this+0x198](via0x00507F90); - the target reference at
[this+0x214](via0x0041FD80; target id at+0x14/+0x16); - the cached target position at
[this+0x228..0x230]; - the launcher object at
[this+0x1D4]for the no-target fallback (position from the launcher frame+0xCand scalar+0x18via0x0040CA40and0x0040CBB0); - the fire-mode flag at
[this+0x224], which selects config flag2or0x800002; - the sub-object via
0x005F07C0.
The spawn dispatch is FUN_004D9910 (34 bytes). It takes the config object
from [param_1]'s vtable +4, reads the type id at +0x10, the aim point at
+0xB8..+0xC4, and the pool key at +0x54, maps the type id through the
global table at 0x007B8C0C, and calls the registered factory at
[descriptor+0x20].
The missile factory is FUN_005E3FF0 (registered id 0x43D through
0x005E3E3E). It looks up the pool by config+0x54: a hit reuses the object
through FUN_005E4400, a miss allocates 0x2D4 bytes and calls
FUN_005E4080.
FUN_005E4080 is the missile constructor. It copies the aim block
+0xB8/+0xBC/+0xC0 into the missile's target-position cache
+0x26C..+0x274, copies the timer +0xC4 into +0x294, and reads the id
pair +0xB4 (u8) / +0xB6 (u16). When the pair equals the default sentinel
pair at 0x007DC694/0x007DC696, the constructor skips target resolution:
a no-lock missile has no target link. A real pair resolves through the roster
lookup 0x004CDFA0 -> 0x004CEEB0, copies the target's world position from
[object+0xD8]+0x80, and registers the link through 0x005E6590.
The aim point is therefore the target position when locked, or the launcher-derived muzzle point when not. The config carries the target id pair, the aim point, the timer, and the fire-mode flag.
Missile flight and guidance¶
FUN_005E45E0 is the per-frame missile tick (__thiscall, RET 8, one
double now). It updates the timer at +0x290 by adding
(float)(now - +0xE4), then dispatches on [[missile+0xE0]+0x14] - 6 through
the table at 0x005E49CC:
| Type | Step bodies |
|---|---|
| 6 | 0x5E5700 + 0x5E5280 + 0x5E49F0 + 0x5E4CF0 -> terminal |
| 7 | 0x5E5BD0 -> after-switch |
| 8 | 0x5E58B0 -> after-switch |
| 9 | 0x5E5AF0 -> after-switch |
| 10 | timed body: if +0x290 < lifetime ([owner+0x7C] via the +0x78 getter) -> 0x5E5BD0 + after-switch; otherwise vtable +0x18 (destroy) and return |
| 11 | 0x5E59B0 -> after-switch |
| 12 | 0x5E5A50 -> after-switch |
| other | straight to the terminal block |
The after-switch path is 0x5E5280 + 0x5E49F0: look-at and guidance.
FUN_005E49F0 is gated on [missile+0x299] == 0 and
[missile+0x290] < [missile+0x294]; it steers toward
[[missile+0xD8]+0x8C/+0x9C/+0xAC].
The terminal detonation gates are:
[+0x299] != 0-> return.0x0041FD80(+0x258) != 0(a guidance container is present) -> return.|ref.y (+0x270) - prev.y (+0x288)| > 1e-4-> return (the reference point must be stable this tick).|len^2(ref - targetPos)| <= 1e-4-> skip.|vel^2| <= 1e-4-> skip.dot(dir, velDir) < 0.999-> return.
On detonation, [+0x299] is set to one; the exit point
targetPos + dir*1000 is written to both +0x26C..+0x274 and
+0x284..+0x28C; the impact snapshot copies 48 bytes from [target+0x20],
builds the offset vector from prevRef - targetPos, and calls
0x0041FDE0 and 0x004DAA20. The rate fields are frozen:
+0x168/+0x16C = 0, +0x170 = approxSqrt(len^2(+0x168..)),
+0x174..+0x17C = 0, +0x180/+0x184 = 0, and
+0x188 = approxSqrt(len^2(+0x180..)), where
approxSqrt(x) = (bits(x) >> 1) + 0x1FC00000.
Hit resolution and damage¶
FUN_0058D930 is the mech's damage-mode event handler. It is reached through
the generic dispatcher FUN_004D5B70 with the mech as the dispatcher's
this. It switches on the signed byte at msg[0x1C] - 5 over types 5..12
through the jump table at 0x0058DC5C.
The handler resolves the event against the mech's damage map at
mech+0x370. The map is a sorted keyed list of damage-target parts; the part
data is at node +0xC and the key at node +0x18. FUN_00416B60 is the
lookup. The types select the keys in this order:
| Type | Keys |
|---|---|
| 5 | 8, 5, 4, 6 (all appended) plus the ABL notify 0x00621FD0 when [netinfo+0x17] == 1 |
| 6 | 1, 0 |
| 7 | 2 (else 5), then 0 |
| 8 | 3 (else 4), then 1 |
| 9 | 6, 4, 5 when present |
| 10 | 7 when present |
| 11 | none |
| 12 | 1, 0 |
| other | none |
The tail looks up keys 9 and 10, counts the appended entries
(FUN_00417A40), and when the count is positive divides the event amount
msg+0x14 by the count in place. It then walks the entries and calls
entry->vtable[0x10](msg, 0). If entry->[0x34] == part9->[0x50] it also
applies part 9; the same applies for part 10. A null entry stops the walk but
still counts toward the split. The list is destroyed with FUN_00417980.
The part objects are armor-attribute pools with vtable 0x7740BC:
+0x10max,+0x14current;+0x18type-3 scale,+0x20incoming-damage scale;+0x24owner/zone object,+0x28parent object;+0x2C/+0x30strings,+0x34id;+0x38/+0x3C/+0x40mirror out-pointers;+0x44state,+0x48fraction,+0x4Carmor class.
The apply method FUN_004E6520 (__thiscall, RET 8) mutates the amount in
place:
msg+0x14 *= [part+0x20]; clamp to0.1if below0.1.- Notify the owner through
[part+0x24]->vtable[0x80](msg, part). - On the gated path, if armor remains and the type/class pair matches the
damage-reduction matrix, scale by
0.667; set the parent hit-flash time whencur == max; apply the type-3 scale; subtract the amount fromcur; on underflow, forward the excess to the parent; then notify the owner throughvtable[0x8C]. - Recompute the state with
FUN_004E6480and the fraction (including the parent's current/max when present).
FUN_004E6480 maps the current/max fraction to a damage tier: max == 0 or
frac < 0.01 is wrecked/destroyed (5 when both this zone and its parent are
dead, else 4); frac < 0.1 is 3; frac < 0.5 is 2; frac < 0.75 is
1; otherwise 0.
FUN_006747E0 is the subsystem status readout. It exposes
ExecutionState, CriticalHitsTaken (+0x168), SubsystemIndex (+0x16C),
and InternalLocation (+0x170); the class constructor 0x005F0740 copies
those from a 0x6C-byte record and 0x005F06F0 serializes them back.
Combat state replication¶
Combat state rides the network session carriers:
| Wire | Carrier | Send | Apply |
|---|---|---|---|
0x59 |
per-frame element replication | FUN_005BD3D0 -> FUN_006805F0 |
FUN_00680B30 -> FUN_00680730 -> FUN_0067FDF0 |
0x58 |
per-slot world update | FUN_0067D5E0 |
FUN_0067E930 |
0x18 |
entity state update | FUN_0053D160 |
FUN_0053D0B0 (mech entity type 0x429) |
0x47 |
presence sync | - | FUN_00539160 / FUN_005BE210 |
The damage record is written by the mech element writer FUN_005F79D0 and
read by the loader FUN_005F7ED0. A pool record carries the class tag, max,
current, name, id, state, class, and group; a component record carries the
class tag, type id, name, key, pool key, auxiliary table, target id pair, and
aim point. Both sections are framed as [u32 count][records] and are gated on
the mech's damage/engaged bytes.
Invariants and failure modes¶
- Fire handlers only change counters; the per-frame driver is the only path that spawns missiles.
- The
[this+0x589]byte selects the gated or ungated counter path; it is cleared only by init/reset. - A no-lock missile has no target link; the sentinel id pair suppresses resolution.
- The launch aim point is target-derived when locked and launcher-derived when not; the two cases must not be mixed.
- The missile timer is a float updated from a double delta; the type table selects the per-tick step plan.
- Terminal detonation requires a stable reference Y, non-zero relative
distance and velocity, and a direction dot of at least
0.999. - Damage is split across the selected parts; the amount is divided in place and a null entry stops the walk.
- Armor class and type select the damage-reduction matrix; the parent receives only overflow.
- Damage tiers are fraction-based;
max == 0is always wrecked/destroyed. - The wire carriers are owned by the network layer; combat owns the payload field meanings.
Key entry points¶
| VA | Detail | What it does |
|---|---|---|
0x0072E580 |
input | WndProc and key/mouse bitmap |
0x004D5B70 |
command | generic command dispatcher |
0x0077B9D8 |
command | 131-entry command table |
0x00556D80 |
command | FireWeapon counter |
0x00554BF0/0x00554C70/0x00554CF0 |
command | weapon-group counters |
0x005541C0 |
fire | per-frame fire driver |
0x00553F20 |
fire | per-group fire dispatch |
0x005E219B |
launch | launch transform |
0x004D9910 |
launch | spawn dispatch |
0x005E3FF0 |
launch | missile factory |
0x005E4080 |
launch | missile constructor |
0x005E45E0 |
flight | missile tick |
0x005E49F0 |
flight | missile guidance |
0x005E49CC |
flight | missile type table |
0x0058D930 |
damage | mech damage-mode handler |
0x004E6520 |
damage | armor-pool apply |
0x004E6480 |
damage | damage-tier state |
0x006747E0 |
damage | subsystem status readout |
0x005F79D0 |
replication | damage/component record writer |
0x005F7ED0 |
replication | damage/component record loader |
Details¶
| Detail | VAs | Responsibility |
|---|---|---|
| fire-damage | 141 | Weapon fire eligibility, launch transform, missile/guidance, hit resolution, and damage. |
Coverage¶
All 141 classified VAs are in the MW4Mercs.exe index, which lists role, size, source, and evidence. 71 are shared (shared.md). 21 are outbound boundary calls (SUBSYSTEM_SEAMS.md).