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

6.8 KiB
Raw Blame History

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: RangeDamageModifierDataComponentGetModifierByDistance(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.