# 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. **Confirmed 2026-06-16 (offline, `bundle/dump_blueprint.py`): the `Damage*DataComponent`s are NOT authored on the item/ammo EntityBlueprints in the bundles.** Decoded `item_grenadeContact`, `item_shotgunAmmo`, `item_pistolAmmo`, `item_shotgun`, `item_revolverSmall_dusters` — every one has only generic components (InteractActions, Count, ItemName, ItemType, NiceName, View, ViewSize, colliders, Physics); **zero** `Damage{Physical,…}DataComponent` / `MeleeDataComponent` / AoE. The only `.value` floats present are `ViewSizeDataComponent` (~0.3–0.97), not damage. So `GetDamage`'s `DamageXxxDataComponent.value` reads are populated **at runtime (server-authoritative)** — there is no per-weapon damage constant in the bundles *or* as a static anchor in the DLL. **Ways to get actual base numbers, ranked:** - ✅ **In-game empirical measurement** (controlled damage tests) — the only clean route. - ⚠️ Live-client runtime memory (the components hold real values once spawned) — but that's process inspection → **BattlEye / no injection** → off-limits. - ❌ Static extraction (bundles) — values absent (proven above). - ❌ Static decompile constant — none exists (generic Entitas dispatch). - ❌ Master-server query — no damage field / no stats endpoint (see [MASTER_SERVER.md](MASTER_SERVER.md)). - ❓ Game-server (not master) entity-snapshot capture *might* carry component values, but unverified and the server may only transmit results, not per-weapon stats. ## 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 GetDamage(weapon, ammo, distance, isHeadShot, isAoE)` | `0x4BAC460` | the per-shot calc factory (state machine `d__12`) | **The damage formula** = `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 `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 `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.