- workbench_bundles.py: each crafting press's CraftingWorkbenchDataComponent.recipeBundles resolved (full press = Armament T1+T2+Utility; small press = T1+Utility, no T2) - discord_recipes.py: Discord-formatted Tier1/Tier2 workbench + conveyor recipe tables
105 lines
3.9 KiB
Python
105 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""For every EntityBlueprint carrying a CraftingWorkbenchDataComponent, list the
|
|
CraftingRecipeBundle assets it references (recipeBundles). Resolves the Odin
|
|
ext_index refs against the blueprint's serializationData.ReferencedUnityObjects
|
|
(PPtrs) -> CraftingRecipeBundle MonoBehaviour -> m_Name.
|
|
"""
|
|
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"
|
|
WB = "CraftingWorkbenchDataComponent"
|
|
|
|
|
|
def find_recipe_indices(node, out):
|
|
"""Find a node of $type CraftingWorkbenchDataComponent and collect ext_index refs."""
|
|
if isinstance(node, dict):
|
|
if node.get("$type", "").endswith("CraftingWorkbenchDataComponent"):
|
|
def grab(n):
|
|
if isinstance(n, odin_read.Ref):
|
|
if n.kind == "ext_index":
|
|
out.append(n.val)
|
|
elif isinstance(n, dict):
|
|
for v in n.values():
|
|
grab(v)
|
|
elif isinstance(n, list):
|
|
for v in n:
|
|
grab(v)
|
|
grab(node)
|
|
for v in node.values():
|
|
find_recipe_indices(v, out)
|
|
elif isinstance(node, list):
|
|
for v in node:
|
|
find_recipe_indices(v, out)
|
|
|
|
|
|
def main():
|
|
gen = TypeTreeGenerator(UNITY)
|
|
gen.load_il2cpp(open(DLL, "rb").read(), open(META, "rb").read())
|
|
# load the workbench prefabs (epb), the recipe-bundle assets they reference
|
|
# (craftingrecipes), and monoscripts so m_Script resolves
|
|
paths = [os.path.join(BUNDLES, b) for b in
|
|
("epb_assets_all.bundle", "craftingrecipes_assets_all.bundle",
|
|
"sand_monoscripts.bundle")]
|
|
env = UnityPy.load(*paths)
|
|
nodecache = {}
|
|
|
|
# path_id -> CraftingRecipeBundle name, to resolve the workbench's ext refs
|
|
bundle_names = {}
|
|
for o in env.objects:
|
|
if o.type.name == "MonoBehaviour":
|
|
try:
|
|
d = o.read()
|
|
if d.m_Script.read().m_ClassName == "CraftingRecipeBundle":
|
|
bundle_names[o.path_id] = d.m_Name
|
|
except Exception:
|
|
pass
|
|
|
|
for path, obj in env.container.items():
|
|
if obj.type.name != "GameObject":
|
|
continue
|
|
go = obj.read()
|
|
eb = None
|
|
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":
|
|
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:
|
|
tt = eb.read_typetree(nodecache[full])
|
|
except Exception:
|
|
continue
|
|
sd = tt.get("serializationData", {})
|
|
sb = sd.get("SerializedBytes")
|
|
if not sb:
|
|
continue
|
|
try:
|
|
parsed = odin_read.parse(bytes(sb))
|
|
except Exception:
|
|
continue
|
|
idxs = []
|
|
for root in ("roots", "items"):
|
|
find_recipe_indices(parsed.get(root), idxs)
|
|
if not idxs:
|
|
continue
|
|
refs = sd.get("ReferencedUnityObjects", [])
|
|
print("\n%s" % go.m_Name)
|
|
for i in idxs:
|
|
pid = refs[i].get("m_PathID") if i < len(refs) else None
|
|
print(" recipeBundle[%d] -> %s" % (i, bundle_names.get(pid, "<unresolved pid %s>" % pid)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|