#!/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()