weapon damage: full static map of the model (values are runtime/server, not statically anchorable)

Mapped the damage system end-to-end statically and documented it for cross-patch
tracking (docs/WEAPON_DAMAGE.md):

- Model: 8 DamageXxxDataComponent (value @+0x10) on item/ammo, read by
  HealthAndDamageExtensions.GetDamage (RVA 0x4BAC520); per-shot formula in
  <GetDamage>d__12.MoveNext (RVA 0x4BB3DB0) = base x range-falloff x headshot,
  melee skips range falloff.
- Delivery: PlainDamgeDealerComponent{damageAmount,damageType,isMelee} -> HitEventInfo
  -> reduces HealthDataComponent.value; networked via DamageEvent.

Verified the base numbers are in NO asset (blueprints/ammo/projectiles/CheatItemDefs/all
bundles UTF-16). Established WHY the literal constants aren't statically anchorable: this
build accesses every component via fully-generic Entitas dispatch (no static class/index/
string reference in producing code; typed setters all dead build-wide; item-id strings
have 0 refs, verified via a calibrated string-xref) and damage resolution is server-
authoritative. So the value is a runtime component, not a reachable static constant.

Corrects the earlier draft that overstated "no value exists".

Tools: reverse/il2cpp_re.py (+find_rip_refs_batch, scan_movss_consts),
bundle/component_census.py, bundle/dump_blueprint.py.
This commit is contained in:
DownloadPizza
2026-06-11 16:27:51 +02:00
parent b61b2526fc
commit afbf79ac26
5 changed files with 649 additions and 0 deletions

106
docs/WEAPON_DAMAGE.md Normal file
View File

@@ -0,0 +1,106 @@
# Weapon damage — static map of the model (for tracking across updates)
Goal: locate weapon damage (esp. the `_dusters` revolver's **melee** value) in the static
files, documented so it can be re-checked on patches.
**Status:** the damage *model* is fully mapped statically (below). The per-weapon *base
numbers* are **not** present in any asset, and are **not reachable by static anchor** in
`GameAssembly.dll` because every ECS component is accessed through fully-generic Entitas
dispatch (runtime integer index, no static class/index reference in calling code) and the
damage *resolution* is server-authoritative. What you can diff across updates is the model
and the formula function (RVAs below); the literal constants need a different method (see
end). This corrects an earlier draft that wrongly concluded "no value exists" — the values
**are** live at runtime; they just aren't statically anchorable constants.
## Damage model (all static, all in `il2cpp/dump.cs` + verified by disasm)
Per-type damage lives as a `float value` (object offset **+0x10**) on 8 components on the
**item/ammo** entity: `Damage{Physical,Cold,Heat,Rad,Fire,Poison,Siege,True}DataComponent`
(`: BaseFloatValueComponent`). `MeleeDataComponent` (TypeDefIndex 5825) is a marker only.
Read path — `Hologryph.Sand.Shared.Damage.HealthAndDamageExtensions`:
| Method | RVA | Role |
|---|---|---|
| `GetDamage(ItemEntity, DamageType)` | `0x4BAC520` | jump-table over the 8 types → returns that type's `DamageXxxDataComponent.value` (`[comp+0x10]`), or `0` if absent |
| `GetDamageModifier(ItemEntity, DamageType)` | `0x4BAC340` | per-type `DamageModifierXxx` value |
| `GetAoEDamage(ItemEntity, DamageType)` | (via calc) | explosive AoE (`AoEDamageDataComponent.AoEDamage` struct: radius + 8 floats) |
| `IEnumerable<WeaponDamage> GetDamage(weapon, ammo, distance, isHeadShot, isAoE)` | `0x4BAC460` | the per-shot calc factory (state machine `<GetDamage>d__12`) |
**The damage formula** = `<GetDamage>d__12.MoveNext` (**RVA `0x4BB3DB0`**) — decompiled here:
per damage type it takes base `GetDamage(weapon,type)` (and ammo), then multiplies by
- **range falloff**: `RangeDamageModifierDataComponent``GetModifierByDistance(distance)`
(RVA `0x4A4B870`); **skipped when `isMelee`** (iterator flag at `+0x4c`),
- **headshot**: `HeadShotMultiplierDataComponent.value` (`[+0x10]`) when `isHeadShot`,
- plus `GetDamageModifier` / `GetAoEDamage`.
So **melee damage = base per-type value × headshot mult** (no distance falloff). The base is
the item's `DamagePhysical`/`DamageSiege` value. `WeaponDamage` struct = `{float damageAmount,
DamageType damageType}`.
## Damage delivery / application (static)
- A shot/attack carries **`PlainDamgeDealerComponent`** (game's typo "Damge"; TypeDefIndex
7343, `IGameContextComponent`): `float damageAmount @+0x10`, `DamageType damageType @+0x14`,
`bool isMelee @+0x15`. Created at runtime per shot (not in any asset).
- On impact → **`HitEventInfo`** (`Effects`, TypeDefIndex 7208): `float damageAmount @+0x30`,
`ammoId`, `ProjectileType`, `HitType`; carried by `HitEventComponent` and snapshot
`PlainHitEventComponentData` (`damageAmount @+0x2C`).
- Applied to **`HealthDataComponent`** (TypeDefIndex 7319): `ushort count @+0x10`,
`DamageType[] damageMask @+0x18`, `float value @+0x20`. `GetTotal()` RVA `0x4BAC900`.
- Networked as **`DamageEvent`** (TypeDefIndex 7299): `int damageAmount @+0xC`, `targetId`,
`blueprintId`, `bool isLethal`; batched in `DamageEventMessage`. Client-side resolution
callers (`OnDamageAvatar(hpBeforeDamage)`, `ConsumeDamage`, armor `GetArmorAbsorbMax`/
`IsBlocksDamage`/`GetDamageExtra`) are **display/feedback only** → the authoritative
computation is server-side.
## The base numbers are not in any asset (verified, multiple ways)
| Check | Result |
|---|---|
| All 1446 EntityBlueprints, Odin-decoded (`bundle/component_census.py`) | no `DamageXxx`/`PlainDamgeDealer`/`AoEDamage` component on any item, ammo, or projectile |
| `item_revolverSmall_dusters` full decode (`bundle/dump_blueprint.py`) | 8 components, all presentation/physics; actions only `WeaponPickup`/`WeaponSwap` |
| All 35 bundles, UTF-16 + ascii grep of the component type-names | 0 occurrences anywhere |
| `CheatItemDefinitionsData` (`ItemDefinition`) | only `{Name, Type, StorageStack}` — no damage field |
## Why the literal constants aren't statically anchorable
All of these were checked and eliminated as ways to reach the code that *sets* a weapon's
damage value:
- **Typed Entitas setters are dead.** `AddDamagePhysical(entity,float)` etc. — 0 callers.
This is build-wide, not damage-specific (the `HealthDataComponent` setters are also
uncalled), so "setter has no callers" proves nothing — this build mutates components
in-place / via generic add, never the generated typed setters.
- **Component class & module globals** (`DamagePhysical` class `0x187CE73B8`, module
`0x187D6B778`; `PlainDamgeDealer` class `0x187CE3638`, module `0x187D82300`) are
referenced **only by the generated extension methods** — no producer/init references them.
- **Item-id strings have 0 code references.** Verified the string-xref method is sound
(60/60 sampled `ScriptString` slots resolve to ≥1 ref), then confirmed
`"item_revolverSmall_dusters"` (slot `0x1807CC1808`) has **0** — so damage is not keyed
by item-id string in code.
- **Constant scans** (rip-relative `movss`, and `mov dword[mem],imm32` float-immediates)
don't isolate weapon damage from UI/physics/geometry noise.
Net: an item's damage component is created from a pooled factory (set up once at context
init) and added via `entity.AddComponent(component)` where the component's index is derived
from the object itself — there is **no static reference in the producing code** that an xref
can follow. That, plus server-authoritative resolution, is why the constant can't be reached
by static anchoring (in my tooling **or** Ghidra — Ghidra reads bodies but can't defeat the
dynamic dispatch without already knowing which system to read).
## Re-deriving / tracking on updates
Tooling (regenerates the map from a new `dump.cs` + bundles):
- `reverse/il2cpp_re.py` — PE map, dump.cs method index, `find_xrefs` (call rel32),
`find_rip_refs` / `find_rip_refs_batch` (RIP-relative data xrefs), `disasm_method` /
`analyze` (resolves calls, reads float consts), `scan_movss_consts`.
- `bundle/component_census.py`, `bundle/dump_blueprint.py`.
To diff the *formula* across patches: re-locate `HealthAndDamageExtensions.GetDamage` and
`<GetDamage>d__12.MoveNext` by signature and compare. To get the actual *numbers*: they are
runtime component values shown in the in-game weapon-inspect UI (`ShowWeaponInfo` /
`ShowPhysicalDamageInfo` call `GetDamage`), so a live read is the reliable source; static
extraction would require following the dynamic Entitas dispatch by hand through the (server-
side) shot/attack init — not achievable via xref/anchor.