crafting: resolve workbench->recipe-bundle mapping + Discord recipe tables
- 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
This commit is contained in:
91
bundle/discord_recipes.py
Normal file
91
bundle/discord_recipes.py
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Emit Discord-ready (monospace code-block) tables of all craftable recipes:
|
||||
the workbench recipes (T1 + T2, both on the Trampler; T2 also at world workbenches)
|
||||
and the world conveyor production lines with their island locations.
|
||||
"""
|
||||
import json, collections
|
||||
|
||||
NAMES = json.load(open("extracted/item_names.json"))["items"]
|
||||
REC = json.load(open("extracted/crafting_recipes.json"))["recipes"]
|
||||
PL = json.load(open("extracted/production_lines.json"))["production_lines"]
|
||||
PLACE = json.load(open("extracted/conveyor_placements.json"))
|
||||
ISL = json.load(open("extracted/island_names.json"))["islands"]
|
||||
|
||||
|
||||
def nm(item_id):
|
||||
e = NAMES.get(item_id)
|
||||
if e and e.get("name"):
|
||||
return e["name"]
|
||||
return item_id # fall back to raw id (no i2 term)
|
||||
|
||||
|
||||
def fmt_side(ings):
|
||||
# merge duplicate item slots (some recipes list the same id in two output slots)
|
||||
merged = {}
|
||||
order = []
|
||||
for i in ings:
|
||||
k = i["itemId"]
|
||||
if k not in merged:
|
||||
merged[k] = 0
|
||||
order.append(k)
|
||||
merged[k] += i["amount"]
|
||||
return " + ".join("%d %s" % (merged[k], nm(k)) for k in order)
|
||||
|
||||
|
||||
def table(rows, headers):
|
||||
cols = list(zip(*([headers] + rows))) if rows else [[h] for h in headers]
|
||||
w = [max(len(str(c)) for c in col) for col in cols]
|
||||
line = lambda r: "| " + " | ".join(str(c).ljust(w[i]) for i, c in enumerate(r)) + " |"
|
||||
sep = "|" + "|".join("-" * (w[i] + 2) for i in range(len(headers))) + "|"
|
||||
out = [line(headers), sep] + [line(r) for r in rows]
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def recipe_rows(keys):
|
||||
rows = []
|
||||
for key in keys:
|
||||
for r in REC[key]:
|
||||
rows.append([fmt_side(r["inputs"]), fmt_side(r["outputs"]), "%gs" % r["craftTimeSeconds"]])
|
||||
return rows
|
||||
|
||||
|
||||
# ---- island placement map: conveyor base name -> [in-game island names] ----
|
||||
def island_disp(prefab):
|
||||
d = ISL.get(prefab, {})
|
||||
return d.get("toponym") or prefab.replace("island_", "")
|
||||
|
||||
|
||||
conv_to_islands = collections.defaultdict(list)
|
||||
for prefab, d in PLACE["islands"].items():
|
||||
if prefab.startswith("island_test"):
|
||||
continue
|
||||
for rc in d["recipes"]:
|
||||
conv_to_islands[rc["conveyor"]].append(island_disp(prefab))
|
||||
for u in d["product_conveyors_without_matching_recipe"]:
|
||||
# name-mismatch: singular placed instance maps to its plural recipe key
|
||||
conv_to_islands[u["conveyor"] + "s"].append(island_disp(prefab))
|
||||
|
||||
OUT = []
|
||||
OUT.append("## Workbench Crafts\n")
|
||||
|
||||
for title, keys in [("Tier 1", ["Recipes_Utility_Workbench_T1", "Recipes_Armament_Workbench_T1"]),
|
||||
("Tier 2", ["Recipes_Armament_Workbench_T2"])]:
|
||||
OUT.append("**%s**" % title)
|
||||
OUT.append("```")
|
||||
OUT.append(table(recipe_rows(keys), ["Inputs", "Output", "Time"]))
|
||||
OUT.append("```")
|
||||
|
||||
OUT.append("\n## World Conveyor Production Lines (single-recipe, fixed locations)\n")
|
||||
# the game_conveyor_base* lines are test-island stubs (placeholder recipes); skip them
|
||||
rows = []
|
||||
for conv, r in sorted(PL.items()):
|
||||
if conv.startswith("game_conveyor_base"):
|
||||
continue
|
||||
islands = conv_to_islands.get(conv, [])
|
||||
loc = ", ".join(sorted(set(islands))) if islands else "(defined, not placed)"
|
||||
rows.append([fmt_side(r["inputs"]), fmt_side(r["outputs"]), "%gs" % r["craftTimeSeconds"], loc])
|
||||
OUT.append("```")
|
||||
OUT.append(table(rows, ["Inputs", "Output", "Time", "Location (island)"]))
|
||||
OUT.append("```")
|
||||
|
||||
print("\n".join(OUT))
|
||||
104
bundle/workbench_bundles.py
Normal file
104
bundle/workbench_bundles.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user