Files
SandTools/docs/WEAPON_DAMAGE.md
DownloadPizza 5946e0910b docs: item sales value + PlayFab/master-server backend map
- SALES_VALUE.md: price model (PriceDto/ItemDto.SellPrice/PriceDataComponent), server-authoritative
- BACKEND_PLAYFAB.md: backend = PlayFab Economy + ws:// master server (per-region);
  observed no-playtest boot behavior, capture findings, OnRegionsLoadFailed dialog path
- WEAPON_DAMAGE.md: cross-link to backend doc
2026-06-12 01:17:07 +02:00

9.2 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).

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.jsonscripts/decomp_targets.pydecomp.cfind_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 <GetDamage>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, 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 <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.