Files
SandTools/wikigen/make_crafting_wiki.py
DownloadPizza a44e4db1c3 refactor: group scripts into walker/ wikigen/ bundle/
Organize the 16 loose scripts by concern:
  walker/  -- .wbt save tooling (sand, build_wbt, walker_hashes,
              harvest_hashes, recover_key)
  wikigen/ -- MediaWiki page generators (make_*_wiki, render_wiki)
  bundle/  -- Unity/Odin asset extraction (unitybundle, odin_read,
              extract_*, loot_probe, dump_loot_bytes)

The only cross-script imports (build_wbt->walker_hashes,
extract_loot->odin_read) live within the same folder, so each
script's dir on sys.path[0] keeps them resolving with no code
changes. All data paths are absolute, so the moves don't affect
I/O. Named the code dir wikigen/ to avoid colliding with the
generated wiki/ output dir; ignore the regenerable wiki_site/ render.
2026-06-11 14:49:33 +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()