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.
6.8 KiB
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)(RVA0x4A4B870); skipped whenisMelee(iterator flag at+0x4c), - headshot:
HeadShotMultiplierDataComponent.value([+0x10]) whenisHeadShot, - 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 byHitEventComponentand snapshotPlainHitEventComponentData(damageAmount @+0x2C). - Applied to
HealthDataComponent(TypeDefIndex 7319):ushort count @+0x10,DamageType[] damageMask @+0x18,float value @+0x20.GetTotal()RVA0x4BAC900. - Networked as
DamageEvent(TypeDefIndex 7299):int damageAmount @+0xC,targetId,blueprintId,bool isLethal; batched inDamageEventMessage. Client-side resolution callers (OnDamageAvatar(hpBeforeDamage),ConsumeDamage, armorGetArmorAbsorbMax/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 (theHealthDataComponentsetters 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 (
DamagePhysicalclass0x187CE73B8, module0x187D6B778;PlainDamgeDealerclass0x187CE3638, module0x187D82300) 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
ScriptStringslots resolve to ≥1 ref), then confirmed"item_revolverSmall_dusters"(slot0x1807CC1808) has 0 — so damage is not keyed by item-id string in code. - Constant scans (rip-relative
movss, andmov dword[mem],imm32float-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.