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.
This commit is contained in:
132
wikigen/make_crafting_wiki.py
Normal file
132
wikigen/make_crafting_wiki.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/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&H Armaments Workbench''' (1×1)")
|
||||
out.append("* <code>walker_compCrafting_Small_Wood_1x1</code> — '''S&H Compact Armaments Workshop''' (1×1)")
|
||||
out.append("* <code>walker_compCrafting_Wood_2x1</code> — '''S&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→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()
|
||||
Reference in New Issue
Block a user