# 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).

## Ghidra deep-dive (decompilation) — what it added

Imported `GameAssembly.dll` into Ghidra 11.1.2 (headless, JDK17) and bulk-decompiled
~17,200 combat/system functions to C (`ghidra/` — gitignored; pipeline = `methods.tsv`
from `script.json` → `scripts/decomp_targets.py` → `decomp.c` → `find_damage_writes.py`).
Findings, all consistent with the capstone analysis above:

- **`GetDamage` is a pure component read.** Decompiled body is the 8-way switch returning
  `*(undefined4 *)(component + 0x10)` (the per-type `value`), or `0`. No constants.
- **`PlainDamgeDealerComponent` is network-replicated.** Its `CopyTo` (RVA 0x4BB1A50)
  copies `+0x10`(damageAmount)/`+0x14`(damageType)/`+0x15`(isMelee) — i.e. the component
  travels in **snapshots from the server**; the client receives the damage, doesn't compute it.
- **No client producer.** Across all 17k decompiled functions, nothing writes a damage value
  into a `PlainDamgeDealer`/`DamageXxx` component from a literal or from `GetDamage`:
  `GetDamage` (0x4BAC520) has no real combat caller (only `DeathController$$OnHit` UI +
  the calc itself), and the rich calc `<GetDamage>d__12` factory (0x4BAC460) has **zero**
  callers. So the damage *computation* is not exercised in client-reachable code.

> **Backend identified:** that server is **Azure PlayFab** (Economy v2). The runtime route to
> these numbers — and whether a base value is catalog-authored in `DisplayProperties` — is in
> [BACKEND_PLAYFAB.md](BACKEND_PLAYFAB.md), along with how to pull it without tripping BattlEye.

**Conclusion:** the per-weapon damage numbers are **server-authoritative runtime data**.
They live as Entitas component `value` floats created/assigned by server-side code through
fully-generic dispatch (no static class/index/string anchor) and reach the client only via
network snapshots / `DamageEventMessage`. There is no static constant in `GameAssembly.dll`
that an xref, pattern scan, or decompile-grep can tie to "revolver melee = N". Getting the
literal number requires a **runtime** read (in-game weapon-inspect UI, which calls
`GetDamage`), not static extraction.

## 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`.
- `reverse/ghidra_decomp_targets.py` (Ghidra headless post-script: decompile a target
  list to C) + `reverse/find_damage_writes.py` (scan decompiled C for the populate
  fingerprint). Ghidra install/project/output live under the gitignored `ghidra/`.

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.
