- 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
9.2 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).
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.json → scripts/decomp_targets.py → decomp.c → find_damage_writes.py).
Findings, all consistent with the capstone analysis above:
GetDamageis a pure component read. Decompiled body is the 8-way switch returning*(undefined4 *)(component + 0x10)(the per-typevalue), or0. No constants.PlainDamgeDealerComponentis network-replicated. ItsCopyTo(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/DamageXxxcomponent from a literal or fromGetDamage:GetDamage(0x4BAC520) has no real combat caller (onlyDeathController$$OnHitUI + the calc itself), and the rich calc<GetDamage>d__12factory (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 gitignoredghidra/.
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.