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:
90
bundle/component_census.py
Normal file
90
bundle/component_census.py
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Census of ECS components across every EntityBlueprint in epb_assets_all.bundle.
|
||||
|
||||
Decodes all 1446 blueprints' Odin payloads and tallies which $type components
|
||||
appear and how often. Use it to answer "is component X authored in data at all?"
|
||||
firsthand (Odin stores type names as UTF-16, so an ascii grep gives false
|
||||
negatives — this decodes properly). Optional filter substring narrows output.
|
||||
|
||||
python bundle/component_census.py # combat-ish components
|
||||
python bundle/component_census.py Damage # only names containing 'Damage'
|
||||
python bundle/component_census.py '' # everything
|
||||
"""
|
||||
import os, sys, json, UnityPy
|
||||
from collections import Counter
|
||||
from UnityPy.helpers.TypeTreeGenerator import TypeTreeGenerator
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import odin_read
|
||||
|
||||
GAME = "/mnt/d/SteamLibrary/steamapps/common/Sand Playtest"
|
||||
META = os.path.join(GAME, "Sand_Data/il2cpp_data/Metadata/global-metadata.dat")
|
||||
DLL = os.path.join(GAME, "GameAssembly.dll")
|
||||
BUNDLES = "/home/downloadpizza/sand_tools/bundles"
|
||||
UNITY = "6000.0.40f1"
|
||||
COMBAT = ("Damage", "Health", "Weapon", "Attack", "Melee", "Shoot", "Projectile",
|
||||
"Penetra", "Overheat", "Hit", "AoE", "Armor", "Resist")
|
||||
|
||||
|
||||
def walk(n):
|
||||
if isinstance(n, dict):
|
||||
t = n.get("$type")
|
||||
if t:
|
||||
yield t
|
||||
for k, v in n.items():
|
||||
if k != "$type":
|
||||
yield from walk(v)
|
||||
elif isinstance(n, list):
|
||||
for v in n:
|
||||
yield from walk(v)
|
||||
|
||||
|
||||
def main():
|
||||
filt = sys.argv[1] if len(sys.argv) > 1 else None # None -> combat preset
|
||||
gen = TypeTreeGenerator(UNITY)
|
||||
gen.load_il2cpp(open(DLL, "rb").read(), open(META, "rb").read())
|
||||
env = UnityPy.load(os.path.join(BUNDLES, "epb_assets_all.bundle"),
|
||||
os.path.join(BUNDLES, "sand_monoscripts.bundle"))
|
||||
comp = Counter(); ndone = 0; nodecache = {}
|
||||
for path, obj in env.container.items():
|
||||
if obj.type.name != "GameObject":
|
||||
continue
|
||||
eb = None
|
||||
for c in obj.read().m_Components:
|
||||
co = c.read(); r = co.object_reader
|
||||
if r.type.name == "MonoBehaviour" and co.m_Script.read().m_ClassName == "EntityBlueprint":
|
||||
eb = r; break
|
||||
if eb is None:
|
||||
continue
|
||||
sc = eb.read().m_Script.read()
|
||||
full = (sc.m_Namespace + "." if sc.m_Namespace else "") + sc.m_ClassName
|
||||
if full not in nodecache:
|
||||
nodecache[full] = json.loads(gen.get_nodes_as_json(sc.m_AssemblyName, full))
|
||||
try:
|
||||
sb = eb.read_typetree(nodecache[full]).get("serializationData", {}).get("SerializedBytes")
|
||||
except Exception:
|
||||
continue
|
||||
if not sb:
|
||||
continue
|
||||
try:
|
||||
p = odin_read.parse(bytes(sb))
|
||||
except Exception:
|
||||
continue
|
||||
seen = set()
|
||||
for root in ("roots", "items"):
|
||||
for t in walk(p.get(root)):
|
||||
seen.add(t)
|
||||
for t in seen:
|
||||
comp[t] += 1
|
||||
ndone += 1
|
||||
print("blueprints parsed:", ndone, " distinct components:", len(comp))
|
||||
for t, n in comp.most_common():
|
||||
short = t.split(".")[-1]
|
||||
if filt is None:
|
||||
if any(w in short for w in COMBAT):
|
||||
print("%4d %s" % (n, t))
|
||||
elif filt == "" or filt in t:
|
||||
print("%4d %s" % (n, t))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
95
bundle/dump_blueprint.py
Normal file
95
bundle/dump_blueprint.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fully decode one or more EntityBlueprints (by base name) from epb_assets_all.bundle.
|
||||
|
||||
Prints every Odin component ($type) and every scalar field, so we can see firsthand
|
||||
whether a weapon blueprint carries any damage magnitude. Usage:
|
||||
|
||||
python bundle/dump_blueprint.py item_revolverSmall_dusters [other_base ...]
|
||||
"""
|
||||
import os, sys, json, UnityPy
|
||||
from UnityPy.helpers.TypeTreeGenerator import TypeTreeGenerator
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import odin_read
|
||||
|
||||
GAME = "/mnt/d/SteamLibrary/steamapps/common/Sand Playtest"
|
||||
META = os.path.join(GAME, "Sand_Data/il2cpp_data/Metadata/global-metadata.dat")
|
||||
DLL = os.path.join(GAME, "GameAssembly.dll")
|
||||
BUNDLES = "/home/downloadpizza/sand_tools/bundles"
|
||||
UNITY = "6000.0.40f1"
|
||||
|
||||
|
||||
def entity_blueprint(go):
|
||||
for c in go.m_Components:
|
||||
co = c.read(); r = co.object_reader
|
||||
if r.type.name == "MonoBehaviour" and co.m_Script.read().m_ClassName == "EntityBlueprint":
|
||||
return r
|
||||
return None
|
||||
|
||||
|
||||
def walk(node, prefix=""):
|
||||
"""Yield (path, type_label, scalar) for every node; flags floats."""
|
||||
if isinstance(node, dict):
|
||||
t = node.get("$type")
|
||||
if t:
|
||||
yield (prefix, t, None)
|
||||
for k, v in node.items():
|
||||
if k == "$type":
|
||||
continue
|
||||
yield from walk(v, prefix + "." + str(k))
|
||||
elif isinstance(node, list):
|
||||
for i, v in enumerate(node):
|
||||
yield from walk(v, prefix + f"[{i}]")
|
||||
else:
|
||||
yield (prefix, None, node)
|
||||
|
||||
|
||||
def main():
|
||||
targets = sys.argv[1:] or ["item_revolverSmall_dusters"]
|
||||
gen = TypeTreeGenerator(UNITY)
|
||||
gen.load_il2cpp(open(DLL, "rb").read(), open(META, "rb").read())
|
||||
env = UnityPy.load(os.path.join(BUNDLES, "epb_assets_all.bundle"),
|
||||
os.path.join(BUNDLES, "sand_monoscripts.bundle"))
|
||||
|
||||
want = {t: None for t in targets}
|
||||
for path, obj in env.container.items():
|
||||
base = path.split("/")[-1].replace("_epb.prefab", "")
|
||||
if base in want and obj.type.name == "GameObject":
|
||||
want[base] = obj
|
||||
|
||||
for base, obj in want.items():
|
||||
print("\n" + "=" * 70)
|
||||
print("BLUEPRINT:", base, "" if obj else " *** NOT FOUND ***")
|
||||
print("=" * 70)
|
||||
if not obj:
|
||||
continue
|
||||
eb = entity_blueprint(obj.read())
|
||||
if eb is None:
|
||||
print(" no EntityBlueprint component")
|
||||
continue
|
||||
script = eb.read().m_Script.read()
|
||||
full = (script.m_Namespace + "." if script.m_Namespace else "") + script.m_ClassName
|
||||
nodes = json.loads(gen.get_nodes_as_json(script.m_AssemblyName, full))
|
||||
sb = eb.read_typetree(nodes).get("serializationData", {}).get("SerializedBytes")
|
||||
if not sb:
|
||||
print(" no SerializedBytes")
|
||||
continue
|
||||
parsed = odin_read.parse(bytes(sb))
|
||||
# list components
|
||||
comps = set()
|
||||
floats = []
|
||||
for root in ("roots", "items"):
|
||||
for p, t, sc in walk(parsed.get(root)):
|
||||
if t:
|
||||
comps.add(t)
|
||||
if isinstance(sc, float) and sc not in (0.0,) :
|
||||
floats.append((p, sc))
|
||||
print(" components (%d):" % len(comps))
|
||||
for c in sorted(comps):
|
||||
print(" -", c)
|
||||
print(" non-zero float fields (%d):" % len(floats))
|
||||
for p, v in floats:
|
||||
print(" %-60s = %s" % (p, v))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user