#!/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',[]))) for kind,values in extra.get('members',{}).items(): assert kind in ('blocks','items'),kind for value in values:assert re.fullmatch(r'sanctuary:[a-z0-9_/]+',value),value result[name]['members'][kind]=sorted(set(result[name]['members'][kind]+values)) 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()