Skip to content

Shape Selection & Damage Models

How the game executable chooses which .SH model to draw for a game object — the whole-model damage swap that replaces an aircraft with a wreck when it is destroyed, and the per-class variant set (A10_A.SH, A10_C.SH, …) that supplies those wrecks.

Provenance: Ghidra static analysis of the game executable with FA.SMS symbols applied; recovered from DumpAllFunctions.txt (scripts/ghidra/). Confidence markers follow spec-authoring.md: confirmed · inferred · unknown.

Two axes of shape variation

FA varies an object's geometry along two independent axes, handled by two different mechanisms — keeping them straight is the key to reading this system:

Axis Granularity Mechanism Where
Detail (distance) and articulation/damage within a model intra-shape .SH bytecode opcodes — JumpToLOD, JumpToDetail, JumpToDamage, and the x86 _PL* selectors SH.md → Engine Notes
Whole-model swap when the object is destroyed inter-shape separate .SH files (_A_D) chosen at render time this document

So A10.SH already contains its own LOD ladder and gear/flap articulation (intra-shape); the sibling files A10_A.SHA10_D.SH are destroyed-state models that the engine substitutes when the aircraft dies. The two never overlap.

The variant set is generated, not authored

The .PT type record names only the primary shape and the shadow (for the A-10: shape → a10.SH, shadow_shape → a10_s.SH). The lettered variants appear in no file — the engine derives their names at type-load time.

_SetupOT (0x4A6EB0) — reached for every object type via _SetupPT/_SetupNT/_SetupJT_SetupOT — copies the base filename and overwrites the last character of the stem with a/b/c/d, producing the sibling names, then resolves each through the resource manager (FUN_004a71e0_RMAccess) into a loaded shape pointer stored on the type record:

a10.SH  (template)  ──▶  a10_a.SH → +0x17   a10_b.SH → +0x1B
                         a10_c.SH → +0x25   a10_d.SH → +0x29
        base kept at +0x0F

How many variants an object gets depends on its obj_class (type +0x0D, the same bitfield documented in PT.md):

obj_class Class Variants generated
& 0xC000 Fighter (0x8000) / Bomber (0x4000) full _a _b _c _d set — confirmed
0x2000 Ship per-ship <name>_a.SH proxy — confirmed
0x400 / 0x800 / 0x1000 / 0x200 Tank / AAA / SAM / Vehicle shared generic TANK_A.SH / AAA_A.SH / SAM_A.SH / VEHI_A.SHconfirmed

The _b slot (+0x1B) is additionally gated on a type flag (+0x0E & 0xC0); the _c/_d slots are built only for aircraft (the & 0xC000 branch). This is why every aircraft carries four siblings while a tank references one shared shape.

Runtime selection — gated on destruction

The choice is made per object, per frame, in the two draw-enqueue passes. Both always draw the base shape (+0x0F); then, only when the object is destroyed — entity health word +0x0E == 0 (the same word ShapeSetup (0x4AB450) turns into the global _destroyed) and the entity carries the 0x100000 flag — they add a second, whole-model wreck shape:

// _T_AddYourObjs  (world / terrain pass)      // _GRAPHICAddYourObjs (graphics pass)
if (entity[0x0E] == 0 && (entity[1] & 0x100000)) {
    variant = (type[0x33] == 2) ? type[0x25]   //   variant = (type[0x33]==2) ? type[0x29]
                                : type[0x17];   //                              : type[0x1B];
    GRAddBrentObj(variant, x, y, z);            //   GRAddBrentObj(variant, ...);
}

The per-type field +0x33 selects between two damage-model sets: {_A, _B} when +0x33 != 2, {_C, _D} when +0x33 == 2. Within a set, one variant is drawn in the world/terrain pass (_A or _C) and its partner in the graphics pass (_B or _D). Distance never enters this decision — LOD is entirely intra-shape.

+0x33 is not authored in the type file — it is written at the moment of destruction. PLANEBreakUp (0x49D730, the flight model's break-up handler) sets damage_set = _Rand(2) + 1 (so 1 or 2) as it raises the destroyed flags, then chooses ground vs. air debris. A value of 2 routes the object to the {_C, _D} set, 1 to {_A, _B} — i.e. the wreck pair is picked at random per kill, which is why the same airframe shows either the torn-airframe or the flat-silhouette wreck across replays.

Shape-selection flow: type-load variant generation and per-frame damage-model selection.

Worked example — the A-10 family in FA_2.LIB

File Faces Bounds (ft) Role Evidence
A10.SH 81* X ±81 in-flight model (articulated, x86 _PL* selectors) base slot +0x0F; gear/flap trampolines
A10_A.SH 432 X −87..80 destroyed set {A,B} — world pass slot +0x17
A10_B.SH 98 X ±13 destroyed set {A,B} — graphics pass slot +0x1B
A10_C.SH 398 X −45..84 (asymmetric) destroyed set {C,D} — world pass slot +0x25; torn airframe
A10_D.SH 80 X −54..27, Z −2..3 (flat) destroyed set {C,D} — graphics pass slot +0x29; ground wreckage
A10_S.SH 6 Z = 0 shadow .PT shadow_shape

*A10.SH reports 81 extracted faces because the rest of its geometry sits behind x86 selector blocks the fx codec skips — see SH.md → X86Unknown Region. Its true face count is comparable to A10_A.

The geometry corroborates the trace: A10_C is laterally asymmetric (one side torn away) and A10_D is flat (a burnt ground silhouette) — destruction, not detail reduction.

Object-type record — shape fields

Recovered subset of the OT/NT/PT/JT type record (the struct _SetupOT operates on; distinct from the per-instance entity struct). Offsets are from the type-record base.

Offset Size Field Meaning Confidence
+0x0D 2 obj_class class bitfield (drives variant generation) confirmed
+0x0E 1 type_flags & 0xC0 enables the _b slot inferred
+0x0F 4 shape base shape → loaded pointer confirmed
+0x13 4 shape_name filename used as the suffixing template confirmed
+0x17 4 shape_a _a variant (loaded) confirmed
+0x1B 4 shape_b _b variant (loaded) confirmed
+0x25 4 shape_c _c variant (loaded), aircraft only confirmed
+0x29 4 shape_d _d variant (loaded), aircraft only confirmed
+0x33 4 damage_set == 2 selects the {_C,_D} set; written _Rand(2)+1 by PLANEBreakUp confirmed

Instance-side fields this system reads (see structs.md): entity +0x05 → type record, +0x0E health word (0 = destroyed), +0x01 & 0x100000 damage-model flag.

Functions

VA Symbol Role
0x4A6EB0 _SetupOT generate the lettered variant names and load every shape slot
0x4A7200 / 0x4A7220 / 0x4A7230 _SetupNT / _SetupPT / _SetupJT per-type-kind entry points → _SetupOT
0x4A71E0 LoadShapeSlot resolve one slot's name to a loaded shape via _RMAccess
0x4A6B10 ResolveTypeRecord return the type-record pointer via the MM handle at +0x0F (_SetupOT's first step)
0x4A6AE0 _RMAccess@8 resource-manager load/lock by name
0x4A7A40 _T_AddYourObjs@0 world/terrain draw-enqueue; selects _A/_C
0x4431B0 _GRAPHICAddYourObjs@4 graphics draw-enqueue; selects _B/_D
0x4D057C _GRAddBrentObj@40 queue a shape into the render-sort list (consumes the SH header)
0x4AB450 @ShapeSetup@4 per-object shape-state init; derives _destroyed from the health word

Open questions

1. damage_set (+0x33) derivation — resolved

+0x33 is written at destruction time by PLANEBreakUp (0x49D730) as _Rand(2) + 1, so the {_A,_B} vs {_C,_D} wreck pair is chosen at random per kill rather than fixed per aircraft (see above). No longer open.

Status: resolved — re-static.

  • objects.md — the entity/object system that owns the type record and runs the destruction path.
  • SH.md — the shape format; intra-shape LOD/damage/articulation opcodes and the x86 _PL* selectors (the other variation axis).
  • PT.md — the plane-type record whose obj_class and shape fields feed this system.
  • structs.md — the per-instance entity struct (health word, type pointer, flags).
  • renderer.md — the render-sort pipeline GRAddBrentObj feeds.