Files
SandTools/docs/WEAPON_DAMAGE.md
DownloadPizza afbf79ac26 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.
2026-06-11 18:08:03 +02:00

107 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.