Files
sanctuary-beta/scripts/compile_collections.py
2026-09-15 09:29:11 +02:00

83 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Compile/check the reviewed Markdown taxonomy; no downloads or generated recipes."""
import argparse,json,re
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
DATA=ROOT/'mods/sanctuary/src/main/resources'
TARGET=DATA/'data/sanctuary/sanctuary_recipe_collections'
LEGACY=json.loads((ROOT/'scripts/data/collections-legacy-beta034.json').read_text())
# World-only blocks with an item ID do not become inventory discovery seeds.
WORLD_ONLY={'bedrock','end_portal_frame','reinforced_deepslate','spawner','trial_spawner','vault','budding_amethyst','suspicious_sand','suspicious_gravel','petrified_oak_slab','farmland','dirt_path'}
def sections(text,pattern):
parts=re.split(pattern,text,flags=re.M)
return {code:body for code,body in zip(parts[1::2],parts[2::2])}
def ids(text):return re.findall(r'`(minecraft:[a-z0-9_/.]+)`',text)
def catalogue():
design=(ROOT/'docs/collections-minecraft.md').read_text().split('## Correspondance des advancements')[0]
entries=sections(design,r'^### C(\d{2})\s*$')
inventory=sections((ROOT/'docs/collections-minecraft-inventaire.md').read_text(),r'^## C(\d{2})\s*$')
assert len(entries)==len(inventory)==67
all_recipes=[];all_members=[];result={};labels={'fr_fr':{},'en_us':{}}
for code,body in entries.items():
name=re.search(r'\*\*(.*?) · (.*?)\*\*',body)
cid=re.search(r'`(sanctuary:[a-z0-9_]+)`',body).group(1)
recipes_text,members_text=inventory[code].split('### Recettes',1)[1].split('### Blocs et objets',1)
recipes=re.findall(r'^\| `(minecraft:[^`]+)` \|',recipes_text,re.M)
rows=re.findall(r'^\| `(minecraft:[^`]+)` \| [^|]+ \| ([^|]+) \|',members_text,re.M)
all_recipes+=recipes;all_members.extend(i for i,k in rows)
blocks=[i for i,k in rows if k.strip().startswith('Bloc')]
items=[i for i,k in rows if k.strip() in ('Bloc et objet','Objet')]
triggers=[];adv=[]
for line in body.splitlines():
if line.startswith('**Amorces de découverte :**'):triggers+=ids(line)
if line.startswith('**Advancements qui ouvrent cette collection :**'):adv+=ids(line)
triggers=sorted(set(triggers+items)-{'minecraft:'+i for i in WORLD_ONLY})
triggers=[i for i in triggers if not i.startswith('minecraft:infested_')]
entry={'source':f'collections-minecraft.md#c{code}', 'items':triggers if code!='65' else [],'advancements':adv,'recipes':recipes,'members':{'blocks':blocks,'items':items}}
if code=='65':entry['operator_only']=True
short=cid.split(':')[1]
if short in LEGACY:
prior=LEGACY[short]
for key in ('item_tags','recipe_types'):
if prior.get(key):entry[key]=prior[key]
entry['additional_recipes']=prior.get('recipes',[])
result[short]=entry
for lang,value in zip(('fr_fr','en_us'),name.groups()):labels[lang]['sanctuary.recipes.collection.'+cid.replace(':','.')]=value
assert len(all_recipes)==len(set(all_recipes))==2042
assert len(all_members)==len(set(all_members))==1815
assert sum(len(e['members']['items']) for e in result.values())==1658
assert sum(len(e['members']['blocks']) for e in result.values())==1286
# Only existing vanilla recipes remain in compatibility additions; never derive suffixes.
for entry in result.values():
if 'additional_recipes' in entry:entry['additional_recipes']=sorted(set(entry['additional_recipes'])&set(all_recipes)-set(entry['recipes']))
# Cross-check the second, advancement-centric table, so neither direction can drift.
table=(ROOT/'docs/collections-minecraft.md').read_text().split('## Correspondance des advancements')[1].split('## Variantes et cas particuliers')[0]
mapping={ids(line)[0]:set(re.findall(r'\[C(\d{2})\]',line)) for line in table.splitlines() if line.startswith('|') and ids(line)}
assert len(mapping)==126
for adv,codes in mapping.items():assert codes=={e['source'][-2:] for e in result.values() if adv in e['advancements']},adv
# Explicit Sanctuary extensions keep the reviewed vanilla census intact.
additions=json.loads((ROOT/'scripts/data/collections-sanctuary.json').read_text())
for name,extra in additions.items():
assert name in result,name
for recipe in extra.get('additional_recipes',[]):
assert re.fullmatch(r'sanctuary:[a-z0-9_/]+',recipe),recipe
assert (DATA/'data/sanctuary/recipe'/(recipe.split(':')[1]+'.json')).is_file(),recipe
result[name]['additional_recipes']=sorted(set(result[name].get('additional_recipes',[])+extra.get('additional_recipes',[])))
result[name]['items']=sorted(set(result[name]['items']+extra.get('additional_items',[])))
return result,labels
def main():
parser=argparse.ArgumentParser();parser.add_argument('--write',action='store_true');args=parser.parse_args()
entries,labels=catalogue();TARGET.mkdir(parents=True,exist_ok=True)
for name,entry in entries.items():
path=TARGET/(name+'.json');expected=json.dumps(entry,ensure_ascii=False,indent=2)+'\n'
if args.write:path.write_text(expected)
else:assert path.read_text()==expected,f'Outdated collection: {path}'
for lang,names in labels.items():
path=DATA/f'assets/sanctuary/lang/{lang}.json';contents=json.loads(path.read_text())
if args.write:contents.update(names);path.write_text(json.dumps(contents,ensure_ascii=False,indent=2)+'\n')
else:assert all(contents.get(k)==v for k,v in names.items()),lang
print('Collections035: 67 families, 126 advancements, 2042 unique recipes, 1815 block/item IDs; Markdown, runtime JSON and FR/EN agree.')
if __name__=='__main__':main()