Files
SandTools/make_crafting_wiki.py
DownloadPizza e2a2984925 tools: SAND .wbt + game-data extraction scripts
Python tooling for decoding walker saves and mining game data:
sand.py / build_wbt.py / walker_hashes.py / harvest_hashes.py (.wbt
codec + hashes), extract_*/loot_probe/odin_read/unitybundle (asset
parsing), make_*_wiki + render_wiki (wiki generation), recover_key.
Paths point at the local extracted/, wiki/, and Walkers symlink.
2026-06-11 14:43:57 +02:00

133 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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("* <code>walker_compCrafting_Open_1x1</code> — '''S&amp;H Armaments Workbench''' (1×1)")
out.append("* <code>walker_compCrafting_Small_Wood_1x1</code> — '''S&amp;H Compact Armaments Workshop''' (1×1)")
out.append("* <code>walker_compCrafting_Wood_2x1</code> — '''S&amp;H Armaments Workshop''' (1×2)")
out.append("There is also a '''KF Sewing Workshop''' (<code>walker_compCraftingUtility_Wood_2x1</code>) "
"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 "
"(<code>Recipes_Utility_Workbench_T1</code>, <code>Recipes_Armament_Workbench_T1/T2</code>). "
"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&rarr;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 <code>CraftingRecipeBundle</code> 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()