#!/usr/bin/env python3 """Extract SAND world 'Factory Production Line' (conveyor) recipes. These are the fixed single-recipe world structures (e.g. 1 Raw Aurogen Crystal -> 10 Energy Rods), DISTINCT from the player workbench recipes in extracted/crafting_recipes.json. Same data shape, different home: Mechanic .... a "Factory Production Line": an item conveyor that runs ONE recipe. ECS ......... Hologryph.Sand.Shared.Game.Features.Crafting ProductionLineComponent (input/output entity indices) ProductionLineRecipeComponent -> CraftingRecipe recipe ProductionLine{Input,Output}DataComponent (isLargeItem flags) CraftingRecipe = { CraftingIngredient[] inputIngredients, CraftingIngredient[] outputIngredients, float craftingTimeSeconds } CraftingIngredient = { string itemId, int amount } Where ....... epb_assets_all.bundle, prefabs named game_conveyor_*_epb.prefab (Assets/Content/Game/game_conveyor/...). Each prefab GameObject has an EntityBlueprint (Sirenix Odin SerializedMonoBehaviour) component; the recipe lives in its Odin-serialized `serializationData`. Islands ..... energy_grid_*_epb.prefab (Wunderinsel, Kaiserplatz, DeusExMachine, LittleFactory1-3, LittleFactoryArmory*) place/group conveyors per location. They reference conveyors by entity ref, NOT item-id string, so recipe->island mapping needs the island prefab placements (islands_assets_all.bundle) -- not done here. Extraction path: UnityPy (typetree via IL2CPP generator) reads each EntityBlueprint MonoBehaviour -> serializationData.SerializedBytes -> odin_read.parse -> walk for the CraftingRecipe node. Mirrors extract_loot.py (loot tables are Odin too). Writes extracted/production_lines.json. """ 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" # symlink -> StreamingAssets/aa/... OUT = "/home/downloadpizza/sand_tools/extracted/production_lines.json" UNITY = "6000.0.40f1" def recipes_in(node, out): """Collect every CraftingRecipe-shaped dict in an odin_read tree.""" if isinstance(node, dict): if "inputIngredients" in node or "outputIngredients" in node: out.append(node) for v in node.values(): recipes_in(v, out) elif isinstance(node, list): for v in node: recipes_in(v, out) def ingredients(arr): """Flatten an Odin CraftingIngredient[] (possibly nested in $items) -> [(itemId, amount)].""" if isinstance(arr, dict): arr = arr.get("$items", arr) out = [] for x in (arr or []): rows = x if isinstance(x, list) else [x] for y in rows: if isinstance(y, dict) and "itemId" in y: out.append((y.get("itemId"), y.get("amount"))) return out def entity_blueprint(go): """Return the EntityBlueprint MonoBehaviour object_reader on a prefab GameObject, or 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": return r return None def main(): 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")) result = {} for path, obj in env.container.items(): base = path.split("/")[-1].replace("_epb.prefab", "") if not base.startswith("game_conveyor_") or obj.type.name != "GameObject": continue eb = entity_blueprint(obj.read()) if eb is None: 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: continue parsed = odin_read.parse(bytes(sb)) found = [] recipes_in(parsed["roots"], found) recipes_in(parsed["items"], found) for r in found: result[base] = { "inputs": [{"itemId": i, "amount": a} for i, a in ingredients(r.get("inputIngredients"))], "outputs": [{"itemId": i, "amount": a} for i, a in ingredients(r.get("outputIngredients"))], "craftTimeSeconds": r.get("craftingTimeSeconds"), } out = { "_source": "epb_assets_all.bundle game_conveyor_*_epb EntityBlueprint " "(Odin) -> ProductionLineRecipeComponent.recipe (CraftingRecipe)", "_count": len(result), "production_lines": dict(sorted(result.items())), } json.dump(out, open(OUT, "w"), indent=1) print(f"wrote {OUT} ({len(result)} conveyor recipes)") for k, r in out["production_lines"].items(): inp = " + ".join(f"{i['amount']}x {i['itemId']}" for i in r["inputs"]) or "?" outp = " + ".join(f"{i['amount']}x {i['itemId']}" for i in r["outputs"]) or "?" print(f" {k.replace('game_conveyor_',''):24s} {inp:48s} -> {outp} ({r['craftTimeSeconds']}s)") if __name__ == "__main__": main()