#!/usr/bin/env python3 """Generate a MediaWiki crafting page from extracted/crafting_recipes.json.""" import json, re, os EX = '/home/downloadpizza/sand_tools/extracted' WIKI = '/home/downloadpizza/sand_tools/wiki' SRC = os.path.join(EX, 'crafting_recipes.json') NAMES = os.path.join(EX, 'item_names.json') OUT = os.path.join(WIKI, 'Crafting.mediawiki') STATION_SHORT = { 'Recipes_Utility_Workbench_T1': 'Sewing (Utility)', 'Recipes_Armament_Workbench_T1': 'Armaments T1', 'Recipes_Armament_Workbench_T2': 'Armaments T2', } # real in-game display names (I2 localization); fall back to humanize() if absent _NAMES = json.load(open(NAMES))['items'] if os.path.exists(NAMES) else {} def display(item_id): n = _NAMES.get(item_id, {}).get('name') return n if n else humanize(item_id) STATIONS = { 'Recipes_Utility_Workbench_T1': ('Utility / Sewing recipes (fabric, armour, food, valuables)', 1), 'Recipes_Armament_Workbench_T1': ('Armaments recipes — Tier 1 (basic ammo & weapons)', 2), 'Recipes_Armament_Workbench_T2': ('Armaments recipes — Tier 2 (advanced ammo & weapons)', 3), 'TestRecipesBundle': ('Test recipes (debug)', 99), } ALIAS = {'item_coinCrown': 'Crown'} def camel_split(s): return re.findall(r'[A-Za-z][a-z]*\d*', s) def words(tok): return ' '.join(w.upper() if re.match(r'^[A-Za-z]\d+$', w) # T2, C4 else w[:1].upper() + w[1:] for w in camel_split(tok)) def humanize(item_id): if item_id in ALIAS: return ALIAS[item_id] s = item_id is_item = s.startswith('item_') if is_item: s = s[5:] toks = s.split('_') if toks[0].startswith('resource'): toks[0] = toks[0][len('resource'):] # item_* IDs use camelCase base + underscore variants; bare IDs use underscore as space if is_item and len(toks) > 1: return words(toks[0]) + ' (' + ', '.join(words(v) for v in toks[1:]) + ')' return ' '.join(words(t) for t in toks) def link(item_id): return f'[[{display(item_id)}]]' def ingredients(lst): return ' + '.join(f"{i['amount']} × {link(i['itemId'])}" for i in lst) def outputs(lst): return ' + '.join(f"{o['amount']} × {link(o['itemId'])}" for o in lst) def fmt_time(t): return f"{int(t)}s" if t == int(t) else f"{t}s" def table(recs): rows = ['{| class="wikitable sortable"', '! Output !! Ingredients !! data-sort-type="number" | Time'] for r in recs: rows.append('|-') rows.append(f"| {outputs(r['outputs'])} || {ingredients(r['inputs'])} " f"|| {fmt_time(r['craftTimeSeconds'])}") rows.append('|}') return '\n'.join(rows) def main(): data = json.load(open(SRC))['recipes'] order = sorted(data, key=lambda k: STATIONS.get(k, (k, 50))[1]) real = [k for k in order if STATIONS.get(k, ('', 50))[1] < 50] total = sum(len(data[k]) for k in real) out = [] out.append("Crafting in '''SAND''' is performed at workbenches placed on your walker. Each " "workbench tier unlocks a set of recipes that turn salvaged resources into ammo, " "weapons, armour and utility items.") out.append('') out.append(f"This page lists all {total} craftable recipes (plus developer test recipes), " "extracted directly from the game files. Item names are the in-game (English) names.") out.append('') out.append('__TOC__') out.append('') out.append('== Workbenches ==') out.append("Crafting compartments that can be placed on a walker (from the buildable " "compartment database):") out.append("* walker_compCrafting_Open_1x1 — '''S&H Armaments Workbench''' (1×1)") out.append("* walker_compCrafting_Small_Wood_1x1 — '''S&H Compact Armaments Workshop''' (1×1)") out.append("* walker_compCrafting_Wood_2x1 — '''S&H Armaments Workshop''' (1×2)") out.append("There is also a '''KF Sewing Workshop''' (walker_compCraftingUtility_Wood_2x1) " "associated with the utility/fabric recipes, but it is '''not''' in the buildable " "compartment list in this build.") out.append('') out.append("'''Note:''' The recipe sets below come from the game's three recipe bundles " "(Recipes_Utility_Workbench_T1, Recipes_Armament_Workbench_T1/T2). " "Which workbench compartment loads which bundle is decided in game code at runtime and " "is '''not''' stored in the asset data, so the exact compartment→recipe pairing " "below is inferred from the bundle names and item types, not confirmed.") out.append('') out.append('== Recipes ==') out.append(f'All {total} production recipes in a single sortable table. ' 'Click a column header to sort (e.g. by station or craft time).') out.append('') out.append('{| class="wikitable sortable"') out.append('! Station !! Output !! Ingredients !! data-sort-type="number" | Time') for k in real: station = STATION_SHORT.get(k, k) for r in data[k]: out.append('|-') out.append(f"| {station} || {outputs(r['outputs'])} || {ingredients(r['inputs'])} " f"|| {fmt_time(r['craftTimeSeconds'])}") out.append('|}') out.append('') out.append('== Notes ==') out.append('* Recipes are extracted from the game\'s CraftingRecipeBundle assets; ' 'item names are the in-game English names.') out.append('* "Time" is the base crafting duration in seconds.') out.append('* Developer/debug recipes are omitted.') out.append('') out.append('[[Category:Crafting]]') text = '\n'.join(out) os.makedirs(WIKI, exist_ok=True) open(OUT, 'w').write(text) print(f"wrote {OUT} ({len(text)} chars, {total} recipes in one table)") if __name__ == '__main__': main()