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:
106
docs/WEAPON_DAMAGE.md
Normal file
106
docs/WEAPON_DAMAGE.md
Normal 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.
|
||||
Reference in New Issue
Block a user