Record Sanctuary sources through beta.060 and Git validation
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Preserve local Sanctuary web references outside build, with a portable index."""
|
||||
import argparse
|
||||
from collections import Counter, defaultdict
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from urllib.parse import quote
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ARCHIVES = ROOT / 'archives/web'
|
||||
MEDIA = {'.png', '.jpg', '.jpeg', '.svg', '.webp', '.pdf'}
|
||||
SKIP = {'node_modules', 'site-packages', '.git', '.gradle', 'source', 'sources', 'src'}
|
||||
|
||||
|
||||
def sha(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open('rb') as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b''):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def version_hint(path):
|
||||
match = re.search(r'alpha\.(\d+(?:\.\d+)?)', str(path))
|
||||
if match:
|
||||
return 'alpha.' + match.group(1)
|
||||
match = re.search(r'(?:alpha|atlas)(\d+)', str(path))
|
||||
if match:
|
||||
value = match.group(1)
|
||||
return 'alpha.' + (value[:2] + '.' + value[2:] if len(value) == 3 and value.startswith('30') else value)
|
||||
match = re.search(r'beta(\d+)', str(path))
|
||||
return 'beta.' + match.group(1) if match else None
|
||||
|
||||
|
||||
def collect():
|
||||
selected = {}
|
||||
|
||||
def add(path, destination, category, status):
|
||||
if path.is_symlink() or not path.is_file():
|
||||
return
|
||||
if destination in selected:
|
||||
raise ValueError('Duplicate archive path: ' + destination)
|
||||
selected[destination] = (path, category, status)
|
||||
|
||||
for folder in sorted((ROOT / 'build').iterdir()):
|
||||
if not folder.is_dir() or 'isolated' in folder.name:
|
||||
continue
|
||||
historic = re.match(r'^(alpha\d+|atlas30[567])(?:\D|$)', folder.name)
|
||||
current = folder.name in {'beta003-preview', 'beta004-preview'}
|
||||
if not historic and not current:
|
||||
continue
|
||||
for path in sorted(folder.rglob('*')):
|
||||
relative = path.relative_to(folder)
|
||||
if set(relative.parts) & SKIP or any('venv' in part for part in relative.parts):
|
||||
continue
|
||||
is_atlas_data = folder.name in {'atlas305', 'atlas306', 'atlas307'} and path.suffix == '.json'
|
||||
if path.suffix.lower() not in MEDIA and not is_atlas_data:
|
||||
continue
|
||||
destination = 'assets/' + path.relative_to(ROOT / 'build').as_posix()
|
||||
category = 'beta-previews' if current else 'alpha-atlas' if folder.name.startswith('atlas') else 'alpha-research'
|
||||
add(path, destination, category, 'development-output')
|
||||
|
||||
for path in sorted((ROOT / 'build').iterdir()):
|
||||
if path.is_file() and path.suffix.lower() in MEDIA and re.match(r'^(Sanctuary-.*alpha\.|alpha\d+)', path.name):
|
||||
add(path, 'assets/' + path.name, 'alpha-figures', 'release-named' if path.name.startswith('Sanctuary-') else 'development-output')
|
||||
for path in sorted((ROOT / 'build').glob('Sanctuary-Atlas-0.1.0-alpha.30.[567].zip')):
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
if archive.testzip() is not None:
|
||||
raise ValueError('Invalid atlas: ' + path.name)
|
||||
manifest = json.loads(archive.read('manifest.json'))
|
||||
for name, expected in manifest['files_sha256'].items():
|
||||
if hashlib.sha256(archive.read(name)).hexdigest() != expected:
|
||||
raise ValueError('Atlas manifest mismatch: ' + name)
|
||||
add(path, 'atlases/' + path.name, 'alpha-release-atlas', 'original-manifest-verified')
|
||||
|
||||
for path in sorted((ROOT / 'docs').rglob('*')):
|
||||
if path.suffix.lower() in MEDIA | {'.md', '.json'}:
|
||||
add(path, 'references/' + path.relative_to(ROOT).as_posix(), 'documentation', 'snapshot-at-archive-date')
|
||||
for name in ['README.md', 'CHANGELOG.md', 'THIRD_PARTY_NOTICES.md', 'LICENSE']:
|
||||
add(ROOT / name, 'references/' + name, 'documentation', 'snapshot-at-archive-date')
|
||||
for name in ['terrain_atlas.py', 'structure_atlas.py', 'finishing_atlas.py', 'archive_terrain_atlas.py', 'terrain-atlas-requirements.txt', 'archive_web_assets.py']:
|
||||
add(ROOT / 'scripts' / name, 'references/scripts/' + name, 'reproduction-script', 'snapshot-at-archive-date')
|
||||
for path in sorted((ARCHIVES / 'blocodex-navigation-v1').iterdir()):
|
||||
add(path, 'blocodex-navigation/' + path.name, 'blocodex-concept', 'approved-concept-fictional-data')
|
||||
for path in sorted((ARCHIVES / 'references').glob('*.json')):
|
||||
add(path, 'references/integration/' + path.name, 'integration-intake', 'metadata-only')
|
||||
for path in sorted((ROOT / 'mods/sanctuary/src/main/resources/assets/sanctuary/textures/gui/progression').glob('*.png')):
|
||||
add(path, 'assets/progression-icons/' + path.name, 'creator-icons', 'original-user-asset')
|
||||
title = ROOT / 'ressources-pack/helloworld/assets/minecraft_title.png'
|
||||
add(title, 'assets/title/minecraft_title.png', 'title-reference', 'project-reference')
|
||||
return selected
|
||||
|
||||
|
||||
def gallery(rows, identifier):
|
||||
groups = defaultdict(list)
|
||||
for row in rows:
|
||||
if Path(row['path']).suffix.lower() in MEDIA:
|
||||
groups[row['category'] + ' / ' + (row['version_hint'] or 'référence')].append(row)
|
||||
escape = html.escape
|
||||
link = lambda path: quote(path, safe='/')
|
||||
parts = ['<!doctype html><html lang="fr"><meta charset="utf-8">',
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
||||
'<title>Archives Sanctuary</title><style>',
|
||||
'body{font:16px/1.5 system-ui,sans-serif;max-width:1200px;margin:32px auto;padding:0 20px;background:#f3f0e9;color:#252c25}',
|
||||
'a{color:#275c50}details{margin:16px 0;border-top:1px solid #b2b9ac;padding-top:12px}summary{cursor:pointer;font-weight:600}',
|
||||
'.images{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));gap:16px}',
|
||||
'figure{margin:12px 0;padding:10px;background:#fff}img{max-width:100%;height:180px;object-fit:contain;display:block;margin:auto}figcaption{overflow-wrap:anywhere;font-size:13px}',
|
||||
'li{margin-bottom:6px}.links{display:flex;gap:18px;flex-wrap:wrap}</style>',
|
||||
'<h1>Archives Sanctuary</h1>',
|
||||
'<p>Collection ' + escape(identifier) + ' · références pour le site et le Galactium.</p>',
|
||||
'<div class="links"><a href="blocodex-navigation/index.html">Ouvrir le Blocodex interactif</a>',
|
||||
'<a href="manifest.json">Inventaire et empreintes</a><a href="README.md">Lire la notice</a></div>',
|
||||
'<p>La maquette contient des données fictives. Les images de recherche et les étapes intermédiaires ne décrivent pas toutes le jeu livré.</p>',
|
||||
'<h2>Atlas de livraison complets</h2><ul>']
|
||||
for row in rows:
|
||||
if row['category'] == 'alpha-release-atlas':
|
||||
parts.append(f'<li><a href="{link(row["path"])}">{escape(Path(row["path"]).name)}</a> · original vérifié</li>')
|
||||
parts.append('</ul><details><summary>Documents et sources</summary><ul>')
|
||||
for row in rows:
|
||||
if row['category'] in {'documentation', 'reproduction-script', 'integration-intake'} and Path(row['path']).suffix.lower() not in MEDIA:
|
||||
parts.append(f'<li><a href="{link(row["path"])}">{escape(row["path"])}</a></li>')
|
||||
parts.append('</ul></details><h2>Images et planches</h2>')
|
||||
for group, entries in sorted(groups.items()):
|
||||
parts.append(f'<details><summary>{escape(group)} · {len(entries)} fichiers</summary><div class="images">')
|
||||
for row in entries:
|
||||
url = link(row['path'])
|
||||
image = '' if row['path'].endswith('.pdf') else f'<img loading="lazy" src="{url}" alt="{escape(Path(row["path"]).stem)}">'
|
||||
parts.append(f'<figure><a href="{url}">{image}<figcaption>{escape(row["path"])}<br>{escape(row["status"])}</figcaption></a></figure>')
|
||||
parts.append('</div></details>')
|
||||
return '\n'.join(parts) + '\n</html>\n'
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--id', required=True)
|
||||
args = parser.parse_args()
|
||||
if not re.fullmatch(r'\d{4}-\d{2}-\d{2}-web-v\d+', args.id):
|
||||
parser.error('Expected YYYY-MM-DD-web-vN')
|
||||
output = ARCHIVES / 'snapshots' / args.id
|
||||
bundle = output.parent / ('Sanctuary-Web-Archive-' + args.id.replace('-web-', '-') + '.zip')
|
||||
catalog = ARCHIVES / ('catalog-' + args.id + '.json')
|
||||
if any(path.exists() for path in [output, bundle, catalog]):
|
||||
raise SystemExit('Refusing to overwrite an existing immutable collection')
|
||||
files = collect()
|
||||
if sum(category == 'alpha-release-atlas' for _, category, _ in files.values()) != 3:
|
||||
raise SystemExit('Expected all three alpha.30.5/30.6/30.7 atlas archives')
|
||||
output.mkdir(parents=True)
|
||||
rows = []
|
||||
for destination, (source, category, status) in sorted(files.items()):
|
||||
target = output / destination
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
expected = sha(source)
|
||||
shutil.copy2(source, target)
|
||||
if sha(target) != expected:
|
||||
raise ValueError('Copy differs: ' + destination)
|
||||
rows.append({'path': destination, 'source': source.relative_to(ROOT).as_posix(),
|
||||
'category': category, 'status': status, 'version_hint': version_hint(destination),
|
||||
'bytes': target.stat().st_size, 'sha256': expected})
|
||||
(output / 'index.html').write_text(gallery(rows, args.id), encoding='utf-8')
|
||||
(output / 'README.md').write_text(
|
||||
'# Sanctuary — archive pour le site\n\nOuvrir `index.html` pour parcourir la collection.\n'
|
||||
'La maquette autonome est dans `blocodex-navigation/index.html`.\n\n'
|
||||
'Les images et PDF retrouvés dans les sorties alpha du dépôt jusqu’à alpha.30.7 sont copiés avec leurs chemins. '
|
||||
'Les trois atlas de livraison sont conservés sans modification, données et documentation historique incluses. '
|
||||
'Les aperçus beta.003/004, icônes et maquette sont séparés du fonds alpha.\n\n'
|
||||
'Le manifeste décrit chaque fichier : source relative au dépôt Sanctuary, catégorie, statut, version déduite du nom, taille et SHA-256. '
|
||||
'Une version déduite est une indication de classement, pas une validation de génération. '
|
||||
'Les documents hors atlas sont une photographie au jour de l’archive et contiennent des intentions futures.\n\n'
|
||||
'Les liens des documents historiques peuvent pointer vers des builds non inclus ou des releases en ligne. '
|
||||
'Le catalogue et la maquette fonctionnent hors ligne. Aucun JAR, MRpack, monde, sauvegarde, cache de dépendance ou journal personnel n’est inclus.\n\n'
|
||||
'Cette collection est privée et n’est pas un déploiement du site. Elle conserve les crédits et licences existants ; '
|
||||
'voir `references/THIRD_PARTY_NOTICES.md` et `references/LICENSE`.\n', encoding='utf-8')
|
||||
for name in ['index.html', 'README.md']:
|
||||
path = output / name
|
||||
rows.append({'path': name, 'source': 'generated-by:scripts/archive_web_assets.py',
|
||||
'category': 'archive-index', 'status': 'archive-navigation', 'version_hint': None,
|
||||
'bytes': path.stat().st_size, 'sha256': sha(path)})
|
||||
manifest = {'id': args.id, 'schema': 1, 'scope': 'local-alpha-media-through-30.7-plus-blocodex',
|
||||
'source_head': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip(),
|
||||
'source_worktree_dirty': bool(subprocess.check_output(['git', 'status', '--porcelain'], cwd=ROOT, text=True).strip()),
|
||||
'counts': dict(Counter(row['category'] for row in rows)), 'files': rows}
|
||||
encoded = json.dumps(manifest, ensure_ascii=False, indent=2) + '\n'
|
||||
(output / 'manifest.json').write_text(encoded, encoding='utf-8')
|
||||
catalog.write_text(encoded, encoding='utf-8')
|
||||
with zipfile.ZipFile(bundle, 'x', compression=zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
|
||||
for path in sorted(output.rglob('*')):
|
||||
if not path.is_file():
|
||||
continue
|
||||
info = zipfile.ZipInfo(args.id + '/' + path.relative_to(output).as_posix(), date_time=(2000, 1, 1, 0, 0, 0))
|
||||
info.compress_type = zipfile.ZIP_STORED if path.suffix == '.zip' else zipfile.ZIP_DEFLATED
|
||||
info.external_attr = 0o100644 << 16
|
||||
archive.writestr(info, path.read_bytes())
|
||||
with zipfile.ZipFile(bundle) as archive:
|
||||
assert archive.testzip() is None
|
||||
for row in rows:
|
||||
assert hashlib.sha256(archive.read(args.id + '/' + row['path'])).hexdigest() == row['sha256']
|
||||
assert archive.read(args.id + '/manifest.json').decode() == encoded
|
||||
receipt = {'id': args.id, 'zip': bundle.relative_to(ARCHIVES).as_posix(), 'zip_bytes': bundle.stat().st_size,
|
||||
'zip_sha256': sha(bundle), 'manifest_sha256': sha(catalog), 'file_count': len(rows) + 1,
|
||||
'counts': manifest['counts'], 'checks': ['copies_sha256', 'three_original_atlas_manifests', 'zip_crc', 'zip_members_sha256']}
|
||||
(ARCHIVES / ('receipt-' + args.id + '.json')).write_text(json.dumps(receipt, indent=2) + '\n')
|
||||
bundle.with_suffix('.zip.sha256').write_text(receipt['zip_sha256'] + ' ' + bundle.name + '\n')
|
||||
print(json.dumps(receipt, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile the reviewed 88-species combat catalogue; never edits worlds or historical eggs."""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
# Species: initiative, signature, learned alternative. The engine composes the
|
||||
# same native interactions; these explicit rows prevent silent generic fallbacks.
|
||||
ROWS = """
|
||||
bat MARK MARK DECOY
|
||||
cat STRIKE PUSH INTERCEPT
|
||||
chicken STRIKE PUSH GLIDE
|
||||
cod WATER WATER PUSH
|
||||
cow STRIKE CLEANSE GUARD
|
||||
donkey STRIKE FOOD PUSH
|
||||
fox STRIKE PICKUP DECOY
|
||||
frog STRIKE PULL RESCUE
|
||||
horse CHARGE CHARGE DASH
|
||||
llama SPIT SPIT SPIT
|
||||
mule STRIKE DELIVERY PICKUP
|
||||
pig STRIKE TRACK PUSH
|
||||
rabbit STRIKE DECOY LEAP
|
||||
salmon CHARGE PUSH DASH
|
||||
sheep GUARD GUARD PLATFORM
|
||||
skeleton ARROW ARROW ARROW
|
||||
slime STRIKE REBOUND PLATFORM
|
||||
spider STRIKE WEB CHARGE
|
||||
squid STRIKE CLOUD PUSH
|
||||
tadpole MARK TRACK GUARD
|
||||
tropical_fish MARK MARK DECOY
|
||||
zombie STRIKE ROOT HEAL
|
||||
armadillo GUARD GUARD CHARGE
|
||||
bee STRIKE MARK STRIKE
|
||||
camel STRIKE LEAP GUARD
|
||||
cave_spider STRIKE STRIKE WEB
|
||||
copper_golem STRIKE REDSTONE GUARD
|
||||
dolphin CHARGE DASH RESCUE
|
||||
drowned STRIKE ROOT ANCHOR
|
||||
glow_squid MARK MARK MARK
|
||||
goat STRIKE CHARGE INTERCEPT
|
||||
husk STRIKE CLOUD STRIKE
|
||||
ocelot STRIKE CHARGE INTERCEPT
|
||||
panda STRIKE CHARGE GUARD
|
||||
parrot MARK DECOY MARK
|
||||
pufferfish STRIKE ZONE PUSH
|
||||
polar_bear STRIKE GUARD STRIKE
|
||||
snow_golem SNOWBALL SNOWBALL WATER
|
||||
strider STRIKE DASH RESCUE
|
||||
turtle GUARD GUARD GUARD
|
||||
villager GUARD GUARD FOOD
|
||||
wolf STRIKE INTERCEPT CHARGE
|
||||
bogged ARROW ARROW ZONE
|
||||
parched ARROW ARROW ARROW
|
||||
allay PICKUP DELIVERY PICKUP
|
||||
axolotl STRIKE VANISH HEAL
|
||||
blaze FIREBALL FIREBALL ZONE
|
||||
breeze WIND WIND GUARD
|
||||
camel_husk STRIKE CLOUD RESCUE
|
||||
creeper STRIKE EXPLOSION EXPLOSION
|
||||
endermite STRIKE SWAP CHARGE
|
||||
guardian BEAM BEAM BEAM
|
||||
hoglin STRIKE CHARGE GUARD
|
||||
magma_cube STRIKE ZONE ZONE
|
||||
mooshroom STRIKE FOOD CLOUD
|
||||
nautilus WATER WATER GUARD
|
||||
phantom CHARGE CHARGE GLIDE
|
||||
piglin STRIKE STRIKE ARROW
|
||||
pillager ARROW ARROW ARROW
|
||||
silverfish STRIKE ROOT CAVITY
|
||||
sulfur_cube SPIT ZONE SPIT
|
||||
trader_llama SPIT SPIT RALLY
|
||||
wandering_trader MARK VANISH FOOD
|
||||
zombie_horse STRIKE ANCHOR PUSH
|
||||
zombie_villager STRIKE HEAL HEAL
|
||||
zombified_piglin STRIKE MARK INTERCEPT
|
||||
stray ARROW ARROW ZONE
|
||||
vindicator STRIKE STRIKE STRIKE
|
||||
zoglin STRIKE CHARGE PUSH
|
||||
creaking STRIKE ROOT GUARD
|
||||
elder_guardian BEAM GUARD PUSH
|
||||
enderman STRIKE RESCUE PORTAL
|
||||
evoker FANGS FANGS FANGS
|
||||
ghast FIREBALL FIREBALL FIREBALL
|
||||
happy_ghast GUARD LEAP PLATFORM
|
||||
iron_golem STRIKE GUARD CHARGE
|
||||
piglin_brute STRIKE GUARD DECOY
|
||||
ravager STRIKE CHARGE PUSH
|
||||
shulker SPIT SPIT GUARD
|
||||
skeleton_horse STRIKE DASH RESCUE
|
||||
sniffer STRIKE ROOT TRACK
|
||||
vex CHARGE CHARGE CHARGE
|
||||
witch POTION POTION CLOUD
|
||||
wither_skeleton STRIKE STRIKE GUARD
|
||||
zombie_nautilus STRIKE RESCUE ANCHOR
|
||||
ender_dragon CHARGE BREATH PUSH
|
||||
warden STRIKE BEAM MARK
|
||||
wither SKULL SKULL SKULL
|
||||
"""
|
||||
EN_NAMES = """
|
||||
bat|Echolocation|Diversion
|
||||
cat|Arched back|Swift paw
|
||||
chicken|Great wingbeat|Soft landing
|
||||
cod|Rescue bubble|Splash
|
||||
cow|Emergency milk|Steady shoulder
|
||||
donkey|Supply point|Back off!
|
||||
fox|Snatch|Feint
|
||||
frog|Tongue lash|To the rescue
|
||||
horse|Breakthrough|Clear the way
|
||||
llama|Stopping spit|Volley
|
||||
mule|Urgent delivery|Relay
|
||||
pig|Snout to the ground|Shove
|
||||
rabbit|False trail|Shared leap
|
||||
salmon|Against the current|Wake
|
||||
sheep|Wool cocoon|Cushion
|
||||
skeleton|Aimed shot|Covering fire
|
||||
slime|Bounce|Springboard
|
||||
spider|Taut thread|Surprise descent
|
||||
squid|Ink screen|Rear jet
|
||||
tadpole|Way out!|Guard bubble
|
||||
tropical_fish|Colour flash|False target
|
||||
zombie|Hold on|Second wind
|
||||
armadillo|Rolling shield|Ricochet
|
||||
bee|Pollen|Precise sting
|
||||
camel|Stride|High rampart
|
||||
cave_spider|Tenacious bite|Web nest
|
||||
copper_golem|Trigger|Copper parry
|
||||
dolphin|Evacuation wake|Rescue
|
||||
drowned|Grapple|Shared anchor
|
||||
glow_squid|Glowing ink|Rally signal
|
||||
goat|Battering ram|Counter-charge
|
||||
husk|Dust|Dry grip
|
||||
ocelot|Ambush|Sidestep
|
||||
panda|Roll|Steady seat
|
||||
parrot|Imitation|Targeted warning
|
||||
pufferfish|Prickles|Sudden inflation
|
||||
polar_bear|Cover me|Paw swipe
|
||||
snow_golem|Snow flurry|Cool down
|
||||
strider|Burning passage|Evacuation
|
||||
turtle|Forward shell|Lockdown
|
||||
villager|Makeshift shelter|Distribution
|
||||
wolf|Interception|Pursuit
|
||||
bogged|Poison arrow|Low shot
|
||||
parched|Exhausting bolt|Reserve fire
|
||||
allay|Aerial delivery|Recovery
|
||||
axolotl|Play dead|Convalescence
|
||||
blaze|Salvo|Heat curtain
|
||||
breeze|Gust|Deflection
|
||||
camel_husk|Dusty crossing|Extraction
|
||||
creeper|Detonation|Ambush
|
||||
endermite|Swap|Detour
|
||||
guardian|Focused beam|Sweep
|
||||
hoglin|Gore|Barrage
|
||||
magma_cube|Burning landing|Warm core
|
||||
mooshroom|Shared soup|Spore veil
|
||||
nautilus|Air bell|Shared shell
|
||||
phantom|Dive|Rescue wing
|
||||
piglin|Armed feint|Covered retreat
|
||||
pillager|Covering fire|Crossfire
|
||||
silverfish|Cling|Wall probe
|
||||
sulfur_cube|Acid puddle|Splash
|
||||
trader_llama|Guard circle|Rally
|
||||
wandering_trader|Quiet escape|Emergency cache
|
||||
zombie_horse|Stubborn road|Return kick
|
||||
zombie_villager|Vigil|Relief
|
||||
zombified_piglin|Designation|Shared retaliation
|
||||
stray|Cold arrow|Frost line
|
||||
vindicator|Cleave|Guard breaker
|
||||
zoglin|Continuous charge|Turnaround
|
||||
creaking|Rooting|Sentry
|
||||
elder_guardian|Sanctuary|Recoil wave
|
||||
enderman|Extraction|Passage
|
||||
evoker|Jaws|Fang circle
|
||||
ghast|Explosive fireball|Barrage fire
|
||||
happy_ghast|Hoist|Landing
|
||||
iron_golem|Interposition|Uppercut
|
||||
piglin_brute|Parry and riposte|Challenge
|
||||
ravager|Ram|Stomp
|
||||
shulker|Rising bullet|Shell
|
||||
skeleton_horse|Spectral crossing|Shore relay
|
||||
sniffer|Defensive tilling|Scent
|
||||
vex|Spectral breakthrough|Lightning return
|
||||
witch|Precise throw|Mist
|
||||
wither_skeleton|Funereal slash|Funereal guard
|
||||
zombie_nautilus|Ascent|Mooring
|
||||
ender_dragon|Dragon breath|Protective wings
|
||||
warden|Sonic wave|Deep listening
|
||||
wither|Three skulls|Reaping
|
||||
"""
|
||||
|
||||
def compile_catalogue():
|
||||
rows = {parts[0]: parts[1:] for line in ROWS.strip().splitlines() if (parts := line.split())}
|
||||
names = {parts[0]: parts[1:] for line in EN_NAMES.strip().splitlines() if (parts := line.split('|'))}
|
||||
source = (ROOT / 'docs/familiar-combat-design.md').read_text()
|
||||
entries = {}
|
||||
for line in source.splitlines():
|
||||
match = re.match(r'\| (\d+) · .*?\(`([^`]+)`\)', line)
|
||||
if not match:
|
||||
continue
|
||||
fields = [part.strip() for part in line.split('|')[1:-1]]
|
||||
entries[match[2]] = fields
|
||||
registry = (ROOT / 'mods/sanctuary/src/main/java/fr/koka/sanctuary/companion/CompanionType.java').read_text()
|
||||
rarity = {name.lower(): group.lower() for name, group in re.findall(r'^ (\w+)\(Rarity\.(\w+),', registry, re.M)}
|
||||
assert set(rows) == set(entries) == set(rarity) == set(names) and len(rows) == 88
|
||||
tanks = set('cow sheep armadillo panda polar_bear turtle drowned hoglin nautilus creaking elder_guardian iron_golem piglin_brute ravager zombie_nautilus'.split())
|
||||
fragile = set('bat rabbit cod tadpole tropical_fish bee parrot allay endermite silverfish vex'.split())
|
||||
ranged = {'ARROW','SPIT','SNOWBALL','FIREBALL','WIND','SKULL','BEAM','FANGS','POTION'}
|
||||
# Parameters are reviewable data, bounded again on the server at load time.
|
||||
result = []
|
||||
for species, actions in rows.items():
|
||||
fields = entries[species]
|
||||
role = re.search(r'\*\*(.*?)\*\*', fields[1])[1].rstrip('.')
|
||||
techniques = []
|
||||
for index, action in enumerate(actions):
|
||||
auto = index == 0
|
||||
title_fr = 'Initiative' if auto else re.search(r'\*\*(.*?)\*\*', fields[index+1])[1]
|
||||
title_en = 'Initiative' if auto else names[species][index-1]
|
||||
desc_fr = fields[1] if auto else fields[index+1]
|
||||
desc_fr = re.sub(r'\*\*.*?\*\*\s*:?\s*', '', desc_fr).strip()
|
||||
power = 1.5 if species in fragile else 2.5 if species in tanks else 2
|
||||
if not auto: power *= 2
|
||||
cooldown = (60 if species in tanks else 40) if auto else (360 if index == 1 else 420)
|
||||
windup = (10 if action in ranged else 5) if auto else 20
|
||||
if species in {'ghast','ravager','ender_dragon','warden','wither'}:
|
||||
power += 1; cooldown = 80 if auto else 600; windup = 25 if auto else 40
|
||||
value = dict(action=action,nameFr=title_fr,nameEn=title_en,helpFr=desc_fr,
|
||||
helpEn='',cooldown=cooldown,windup=windup,range=12 if action in ranged else 8,
|
||||
power=power,duration=40 if auto else 80,radius=1.5 if auto else 3,
|
||||
resource='',effect='',count=1)
|
||||
if action == 'STRIKE': value['range'] = 1.7; value['radius'] = 1
|
||||
if action == 'ARROW': value['resource'] = 'arrow'
|
||||
if action == 'SNOWBALL': value['resource'] = 'snowball'
|
||||
if action == 'POTION': value['resource'] = 'splash_potion'
|
||||
if action == 'HEAL': value['resource'] = 'food'; value['power'] = 4
|
||||
if action == 'FOOD': value['resource'] = 'offhand'; value['count'] = 1 if species == 'donkey' else 4
|
||||
if action == 'DELIVERY': value['resource'] = 'offhand'; value['count'] = 4
|
||||
if action == 'CLEANSE': value['resource'] = 'milk'
|
||||
if action == 'GUARD': value['power'] = 2 if auto else 6; value['duration'] = 40 if auto else 100
|
||||
if action == 'GUARD' and species == 'villager': value['resource'] = 'shield'
|
||||
if action == 'PLATFORM' and species == 'sheep': value['resource'] = 'wool'
|
||||
if action == 'ZONE': value['power'] = 1; value['duration'] = 100
|
||||
if action == 'WATER': value['power'] = 10 if auto else 30
|
||||
if action in {'MARK','TRACK','DECOY'}: value['duration'] = 40 if auto else 100
|
||||
if action in {'PUSH','PULL','REBOUND','WIND'}: value['power'] = .25 if auto else .7
|
||||
if species in {'bee','cave_spider','pufferfish','bogged'} and action in {'STRIKE','ARROW','ZONE'}: value['effect'] = 'poison'
|
||||
if species in {'husk','parched'} and not auto: value['effect'] = 'weakness'
|
||||
if species == 'stray': value['effect'] = 'slowness'
|
||||
if species == 'wither_skeleton' and index == 1 or species == 'wither': value['effect'] = 'wither'
|
||||
if species == 'shulker': value['effect'] = 'levitation'
|
||||
if species in {'blaze','magma_cube'}: value['effect'] = 'fire'
|
||||
if species == 'sulfur_cube': value['effect'] = 'acid'
|
||||
if species == 'ghast': value['effect'] = 'explosive'; value['windup'] += 10
|
||||
if species == 'creeper': value['windup'] = 30 if index == 1 else 40; value['power'] = 6; value['radius'] = 3
|
||||
if index and species in {'llama','trader_llama','blaze','pillager','snow_golem','wither'}:
|
||||
value['count'] = 3 if species != 'llama' or index == 2 else 1
|
||||
if index == 2 and species in {'vindicator','polar_bear','evoker','guardian','ghast'}: value['count'] = 3
|
||||
if species == 'turtle' and index == 2: value['power'] = 10; value['effect'] = 'stationary'
|
||||
if species == 'armadillo': value['effect'] = 'projectile'
|
||||
if species == 'happy_ghast' and action == 'GUARD': value['effect'] = 'overhead'
|
||||
if species == 'breeze' and action == 'GUARD': value['effect'] = 'deflect'
|
||||
if species == 'cow' and action == 'GUARD' or species == 'iron_golem' and action == 'GUARD': value['effect'] = 'share_health'
|
||||
if species == 'strider': value['effect'] = 'lava'
|
||||
if species == 'skeleton_horse': value['effect'] = 'water'
|
||||
if species == 'witch' and action == 'CLOUD': value['resource'] = 'splash_potion'; value['effect'] = 'potion'
|
||||
if species == 'wandering_trader' and action == 'VANISH': value['resource'] = 'invisibility'
|
||||
if species == 'axolotl' and action == 'VANISH': value['effect'] = 'play_dead'
|
||||
if species == 'spider' and index == 2: value['effect'] = 'from_above'
|
||||
if species == 'ocelot' and index == 1: value['effect'] = 'flank'
|
||||
if species in {'endermite','vex'} and action == 'CHARGE': value['effect'] = 'phase'
|
||||
if species == 'wither' and index == 2: value['effect'] = 'lifesteal'; value['count'] = 1
|
||||
value['helpEn'] = f"{title_en}: {action.lower().replace('_',' ')} from the companion. Preparation {value['windup']/20:g}s; recovery {value['cooldown']/20:g}s."
|
||||
techniques.append(value)
|
||||
result.append(dict(schema=1,id='sanctuary:'+species,revision=1,entity='minecraft:'+species,
|
||||
rarity=rarity[species],health=24 if species in tanks else 10 if species in fragile else 16,
|
||||
speed=.23 if species in tanks else .34 if species in fragile else .28,
|
||||
distance=7 if actions[0] in ranged else 3,initiative=techniques[0],signature=techniques[1],variant=techniques[2],
|
||||
legacyWork=species,roleFr=role,roleEn=actions[0].lower().replace('_',' ')))
|
||||
target=ROOT/'mods/sanctuary/src/main/resources/data/sanctuary/sanctuary_familiars/vanilla.json'
|
||||
target.parent.mkdir(parents=True,exist_ok=True)
|
||||
target.write_text(json.dumps(dict(schema=1,profiles=result),ensure_ascii=False,indent=2)+'\n')
|
||||
print(f'{len(result)} profiles compiled')
|
||||
|
||||
if __name__ == '__main__':
|
||||
compile_catalogue()
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pin public IANA reference locations. No network access is used by the game."""
|
||||
from pathlib import Path
|
||||
import hashlib,io,json,tarfile,urllib.request
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
VERSION='2026d'
|
||||
URL=f'https://data.iana.org/time-zones/releases/tzdata{VERSION}.tar.gz'
|
||||
archive=ROOT/'build'/f'tzdata{VERSION}.tar.gz'
|
||||
if not archive.exists():archive.write_bytes(urllib.request.urlopen(URL,timeout=30).read())
|
||||
def coord(s):
|
||||
sign=-1 if s[0]=='-' else 1;n=s[1:];degrees=2 if len(n) in (4,6) else 3
|
||||
return sign*(int(n[:degrees])+int(n[degrees:degrees+2])/60+(int(n[degrees+2:])/3600 if len(n)>degrees+2 else 0))
|
||||
with tarfile.open(fileobj=io.BytesIO(archive.read_bytes()),mode='r:gz') as t:
|
||||
assert t.extractfile('version').read().decode().strip()==VERSION
|
||||
zones={}
|
||||
for row in t.extractfile('zone.tab').read().decode().splitlines():
|
||||
if not row or row.startswith('#'):continue
|
||||
_,xy,zone,*_=row.split('\t');split=next(i for i in range(1,len(xy)) if xy[i] in '+-')
|
||||
zones[zone]={'reference':zone,'latitude':round(coord(xy[:split]),4),'longitude':round(coord(xy[split:]),4)}
|
||||
links={}
|
||||
for row in t.extractfile('backward').read().decode().splitlines():
|
||||
fields=row.split()
|
||||
if fields and fields[0]=='Link':links[fields[2]]=fields[1]
|
||||
for _ in range(10):
|
||||
for alias,target in links.items():
|
||||
if alias not in zones and target in zones:zones[alias]=zones[target]
|
||||
output={'source':URL,'version':VERSION,'license':'Public domain (IANA tzdb zone.tab and backward)','archiveSha256':hashlib.sha256(archive.read_bytes()).hexdigest(),'zones':dict(sorted(zones.items()))}
|
||||
dest=ROOT/'mods/sanctuary/src/main/resources/data/sanctuary/realtime/zone-references.json';dest.parent.mkdir(parents=True,exist_ok=True);dest.write_text(json.dumps(output,indent=2)+'\n')
|
||||
print(f'{len(zones)} public reference locations/aliases; {output["archiveSha256"]}')
|
||||
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"basic_wood": {
|
||||
"item_tags": [
|
||||
"minecraft:planks"
|
||||
],
|
||||
"recipes": [
|
||||
"minecraft:stick",
|
||||
"minecraft:crafting_table",
|
||||
"minecraft:chest",
|
||||
"minecraft:bowl",
|
||||
"minecraft:barrel",
|
||||
"minecraft:wooden_axe",
|
||||
"minecraft:wooden_hoe",
|
||||
"minecraft:wooden_pickaxe",
|
||||
"minecraft:wooden_shovel",
|
||||
"minecraft:wooden_sword",
|
||||
"minecraft:oak_button",
|
||||
"minecraft:oak_door",
|
||||
"minecraft:oak_fence",
|
||||
"minecraft:oak_fence_gate",
|
||||
"minecraft:oak_pressure_plate",
|
||||
"minecraft:oak_sign",
|
||||
"minecraft:oak_hanging_sign",
|
||||
"minecraft:oak_slab",
|
||||
"minecraft:oak_stairs",
|
||||
"minecraft:oak_trapdoor",
|
||||
"minecraft:oak_boat",
|
||||
"minecraft:oak_chest_boat",
|
||||
"minecraft:oak_raft",
|
||||
"minecraft:oak_chest_raft",
|
||||
"minecraft:spruce_button",
|
||||
"minecraft:spruce_door",
|
||||
"minecraft:spruce_fence",
|
||||
"minecraft:spruce_fence_gate",
|
||||
"minecraft:spruce_pressure_plate",
|
||||
"minecraft:spruce_sign",
|
||||
"minecraft:spruce_hanging_sign",
|
||||
"minecraft:spruce_slab",
|
||||
"minecraft:spruce_stairs",
|
||||
"minecraft:spruce_trapdoor",
|
||||
"minecraft:spruce_boat",
|
||||
"minecraft:spruce_chest_boat",
|
||||
"minecraft:spruce_raft",
|
||||
"minecraft:spruce_chest_raft",
|
||||
"minecraft:birch_button",
|
||||
"minecraft:birch_door",
|
||||
"minecraft:birch_fence",
|
||||
"minecraft:birch_fence_gate",
|
||||
"minecraft:birch_pressure_plate",
|
||||
"minecraft:birch_sign",
|
||||
"minecraft:birch_hanging_sign",
|
||||
"minecraft:birch_slab",
|
||||
"minecraft:birch_stairs",
|
||||
"minecraft:birch_trapdoor",
|
||||
"minecraft:birch_boat",
|
||||
"minecraft:birch_chest_boat",
|
||||
"minecraft:birch_raft",
|
||||
"minecraft:birch_chest_raft",
|
||||
"minecraft:jungle_button",
|
||||
"minecraft:jungle_door",
|
||||
"minecraft:jungle_fence",
|
||||
"minecraft:jungle_fence_gate",
|
||||
"minecraft:jungle_pressure_plate",
|
||||
"minecraft:jungle_sign",
|
||||
"minecraft:jungle_hanging_sign",
|
||||
"minecraft:jungle_slab",
|
||||
"minecraft:jungle_stairs",
|
||||
"minecraft:jungle_trapdoor",
|
||||
"minecraft:jungle_boat",
|
||||
"minecraft:jungle_chest_boat",
|
||||
"minecraft:jungle_raft",
|
||||
"minecraft:jungle_chest_raft",
|
||||
"minecraft:acacia_button",
|
||||
"minecraft:acacia_door",
|
||||
"minecraft:acacia_fence",
|
||||
"minecraft:acacia_fence_gate",
|
||||
"minecraft:acacia_pressure_plate",
|
||||
"minecraft:acacia_sign",
|
||||
"minecraft:acacia_hanging_sign",
|
||||
"minecraft:acacia_slab",
|
||||
"minecraft:acacia_stairs",
|
||||
"minecraft:acacia_trapdoor",
|
||||
"minecraft:acacia_boat",
|
||||
"minecraft:acacia_chest_boat",
|
||||
"minecraft:acacia_raft",
|
||||
"minecraft:acacia_chest_raft",
|
||||
"minecraft:dark_oak_button",
|
||||
"minecraft:dark_oak_door",
|
||||
"minecraft:dark_oak_fence",
|
||||
"minecraft:dark_oak_fence_gate",
|
||||
"minecraft:dark_oak_pressure_plate",
|
||||
"minecraft:dark_oak_sign",
|
||||
"minecraft:dark_oak_hanging_sign",
|
||||
"minecraft:dark_oak_slab",
|
||||
"minecraft:dark_oak_stairs",
|
||||
"minecraft:dark_oak_trapdoor",
|
||||
"minecraft:dark_oak_boat",
|
||||
"minecraft:dark_oak_chest_boat",
|
||||
"minecraft:dark_oak_raft",
|
||||
"minecraft:dark_oak_chest_raft",
|
||||
"minecraft:mangrove_button",
|
||||
"minecraft:mangrove_door",
|
||||
"minecraft:mangrove_fence",
|
||||
"minecraft:mangrove_fence_gate",
|
||||
"minecraft:mangrove_pressure_plate",
|
||||
"minecraft:mangrove_sign",
|
||||
"minecraft:mangrove_hanging_sign",
|
||||
"minecraft:mangrove_slab",
|
||||
"minecraft:mangrove_stairs",
|
||||
"minecraft:mangrove_trapdoor",
|
||||
"minecraft:mangrove_boat",
|
||||
"minecraft:mangrove_chest_boat",
|
||||
"minecraft:mangrove_raft",
|
||||
"minecraft:mangrove_chest_raft",
|
||||
"minecraft:cherry_button",
|
||||
"minecraft:cherry_door",
|
||||
"minecraft:cherry_fence",
|
||||
"minecraft:cherry_fence_gate",
|
||||
"minecraft:cherry_pressure_plate",
|
||||
"minecraft:cherry_sign",
|
||||
"minecraft:cherry_hanging_sign",
|
||||
"minecraft:cherry_slab",
|
||||
"minecraft:cherry_stairs",
|
||||
"minecraft:cherry_trapdoor",
|
||||
"minecraft:cherry_boat",
|
||||
"minecraft:cherry_chest_boat",
|
||||
"minecraft:cherry_raft",
|
||||
"minecraft:cherry_chest_raft",
|
||||
"minecraft:pale_oak_button",
|
||||
"minecraft:pale_oak_door",
|
||||
"minecraft:pale_oak_fence",
|
||||
"minecraft:pale_oak_fence_gate",
|
||||
"minecraft:pale_oak_pressure_plate",
|
||||
"minecraft:pale_oak_sign",
|
||||
"minecraft:pale_oak_hanging_sign",
|
||||
"minecraft:pale_oak_slab",
|
||||
"minecraft:pale_oak_stairs",
|
||||
"minecraft:pale_oak_trapdoor",
|
||||
"minecraft:pale_oak_boat",
|
||||
"minecraft:pale_oak_chest_boat",
|
||||
"minecraft:pale_oak_raft",
|
||||
"minecraft:pale_oak_chest_raft",
|
||||
"minecraft:bamboo_button",
|
||||
"minecraft:bamboo_door",
|
||||
"minecraft:bamboo_fence",
|
||||
"minecraft:bamboo_fence_gate",
|
||||
"minecraft:bamboo_pressure_plate",
|
||||
"minecraft:bamboo_sign",
|
||||
"minecraft:bamboo_hanging_sign",
|
||||
"minecraft:bamboo_slab",
|
||||
"minecraft:bamboo_stairs",
|
||||
"minecraft:bamboo_trapdoor",
|
||||
"minecraft:bamboo_boat",
|
||||
"minecraft:bamboo_chest_boat",
|
||||
"minecraft:bamboo_raft",
|
||||
"minecraft:bamboo_chest_raft",
|
||||
"minecraft:crimson_button",
|
||||
"minecraft:crimson_door",
|
||||
"minecraft:crimson_fence",
|
||||
"minecraft:crimson_fence_gate",
|
||||
"minecraft:crimson_pressure_plate",
|
||||
"minecraft:crimson_sign",
|
||||
"minecraft:crimson_hanging_sign",
|
||||
"minecraft:crimson_slab",
|
||||
"minecraft:crimson_stairs",
|
||||
"minecraft:crimson_trapdoor",
|
||||
"minecraft:crimson_boat",
|
||||
"minecraft:crimson_chest_boat",
|
||||
"minecraft:crimson_raft",
|
||||
"minecraft:crimson_chest_raft",
|
||||
"minecraft:warped_button",
|
||||
"minecraft:warped_door",
|
||||
"minecraft:warped_fence",
|
||||
"minecraft:warped_fence_gate",
|
||||
"minecraft:warped_pressure_plate",
|
||||
"minecraft:warped_sign",
|
||||
"minecraft:warped_hanging_sign",
|
||||
"minecraft:warped_slab",
|
||||
"minecraft:warped_stairs",
|
||||
"minecraft:warped_trapdoor",
|
||||
"minecraft:warped_boat",
|
||||
"minecraft:warped_chest_boat",
|
||||
"minecraft:warped_raft",
|
||||
"minecraft:warped_chest_raft"
|
||||
]
|
||||
},
|
||||
"brewing": {
|
||||
"advancements": [
|
||||
"minecraft:nether/brew_potion"
|
||||
],
|
||||
"recipe_types": [
|
||||
"minecraft:brewing"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
{
|
||||
"boats_and_rafts": {
|
||||
"additional_recipes": [
|
||||
"sanctuary:oak_boat_2x2",
|
||||
"sanctuary:oak_boat_3x3",
|
||||
"sanctuary:spruce_boat_2x2",
|
||||
"sanctuary:spruce_boat_3x3",
|
||||
"sanctuary:birch_boat_2x2",
|
||||
"sanctuary:birch_boat_3x3",
|
||||
"sanctuary:jungle_boat_2x2",
|
||||
"sanctuary:jungle_boat_3x3",
|
||||
"sanctuary:acacia_boat_2x2",
|
||||
"sanctuary:acacia_boat_3x3",
|
||||
"sanctuary:dark_oak_boat_2x2",
|
||||
"sanctuary:dark_oak_boat_3x3",
|
||||
"sanctuary:mangrove_boat_2x2",
|
||||
"sanctuary:mangrove_boat_3x3",
|
||||
"sanctuary:cherry_boat_2x2",
|
||||
"sanctuary:cherry_boat_3x3",
|
||||
"sanctuary:pale_oak_boat_2x2",
|
||||
"sanctuary:pale_oak_boat_3x3",
|
||||
"sanctuary:poplar_boat_2x2",
|
||||
"sanctuary:poplar_boat_3x3",
|
||||
"sanctuary:bamboo_raft_2x2",
|
||||
"sanctuary:bamboo_raft_3x3",
|
||||
"sanctuary:oak_boat_3x1",
|
||||
"sanctuary:oak_boat_1x3",
|
||||
"sanctuary:oak_boat_2x3",
|
||||
"sanctuary:oak_boat_3x2",
|
||||
"sanctuary:spruce_boat_3x1",
|
||||
"sanctuary:spruce_boat_1x3",
|
||||
"sanctuary:spruce_boat_2x3",
|
||||
"sanctuary:spruce_boat_3x2",
|
||||
"sanctuary:birch_boat_3x1",
|
||||
"sanctuary:birch_boat_1x3",
|
||||
"sanctuary:birch_boat_2x3",
|
||||
"sanctuary:birch_boat_3x2",
|
||||
"sanctuary:jungle_boat_3x1",
|
||||
"sanctuary:jungle_boat_1x3",
|
||||
"sanctuary:jungle_boat_2x3",
|
||||
"sanctuary:jungle_boat_3x2",
|
||||
"sanctuary:acacia_boat_3x1",
|
||||
"sanctuary:acacia_boat_1x3",
|
||||
"sanctuary:acacia_boat_2x3",
|
||||
"sanctuary:acacia_boat_3x2",
|
||||
"sanctuary:dark_oak_boat_3x1",
|
||||
"sanctuary:dark_oak_boat_1x3",
|
||||
"sanctuary:dark_oak_boat_2x3",
|
||||
"sanctuary:dark_oak_boat_3x2",
|
||||
"sanctuary:mangrove_boat_3x1",
|
||||
"sanctuary:mangrove_boat_1x3",
|
||||
"sanctuary:mangrove_boat_2x3",
|
||||
"sanctuary:mangrove_boat_3x2",
|
||||
"sanctuary:cherry_boat_3x1",
|
||||
"sanctuary:cherry_boat_1x3",
|
||||
"sanctuary:cherry_boat_2x3",
|
||||
"sanctuary:cherry_boat_3x2",
|
||||
"sanctuary:pale_oak_boat_3x1",
|
||||
"sanctuary:pale_oak_boat_1x3",
|
||||
"sanctuary:pale_oak_boat_2x3",
|
||||
"sanctuary:pale_oak_boat_3x2",
|
||||
"sanctuary:poplar_boat_3x1",
|
||||
"sanctuary:poplar_boat_1x3",
|
||||
"sanctuary:poplar_boat_2x3",
|
||||
"sanctuary:poplar_boat_3x2",
|
||||
"sanctuary:bamboo_raft_3x1",
|
||||
"sanctuary:bamboo_raft_1x3",
|
||||
"sanctuary:bamboo_raft_2x3",
|
||||
"sanctuary:bamboo_raft_3x2",
|
||||
"sanctuary:oak_boat_1x2",
|
||||
"sanctuary:oak_boat_2x1",
|
||||
"sanctuary:spruce_boat_1x2",
|
||||
"sanctuary:spruce_boat_2x1",
|
||||
"sanctuary:birch_boat_1x2",
|
||||
"sanctuary:birch_boat_2x1",
|
||||
"sanctuary:jungle_boat_1x2",
|
||||
"sanctuary:jungle_boat_2x1",
|
||||
"sanctuary:acacia_boat_1x2",
|
||||
"sanctuary:acacia_boat_2x1",
|
||||
"sanctuary:dark_oak_boat_1x2",
|
||||
"sanctuary:dark_oak_boat_2x1",
|
||||
"sanctuary:mangrove_boat_1x2",
|
||||
"sanctuary:mangrove_boat_2x1",
|
||||
"sanctuary:cherry_boat_1x2",
|
||||
"sanctuary:cherry_boat_2x1",
|
||||
"sanctuary:pale_oak_boat_1x2",
|
||||
"sanctuary:pale_oak_boat_2x1",
|
||||
"sanctuary:poplar_boat_1x2",
|
||||
"sanctuary:poplar_boat_2x1",
|
||||
"sanctuary:bamboo_raft_1x2",
|
||||
"sanctuary:bamboo_raft_2x1"
|
||||
],
|
||||
"additional_items": [
|
||||
"sanctuary:oak_boat_2x2",
|
||||
"sanctuary:oak_boat_3x3",
|
||||
"sanctuary:spruce_boat_2x2",
|
||||
"sanctuary:spruce_boat_3x3",
|
||||
"sanctuary:birch_boat_2x2",
|
||||
"sanctuary:birch_boat_3x3",
|
||||
"sanctuary:jungle_boat_2x2",
|
||||
"sanctuary:jungle_boat_3x3",
|
||||
"sanctuary:acacia_boat_2x2",
|
||||
"sanctuary:acacia_boat_3x3",
|
||||
"sanctuary:dark_oak_boat_2x2",
|
||||
"sanctuary:dark_oak_boat_3x3",
|
||||
"sanctuary:mangrove_boat_2x2",
|
||||
"sanctuary:mangrove_boat_3x3",
|
||||
"sanctuary:cherry_boat_2x2",
|
||||
"sanctuary:cherry_boat_3x3",
|
||||
"sanctuary:pale_oak_boat_2x2",
|
||||
"sanctuary:pale_oak_boat_3x3",
|
||||
"sanctuary:poplar_boat_2x2",
|
||||
"sanctuary:poplar_boat_3x3",
|
||||
"sanctuary:bamboo_raft_2x2",
|
||||
"sanctuary:bamboo_raft_3x3",
|
||||
"sanctuary:oak_boat_3x1",
|
||||
"sanctuary:oak_boat_1x3",
|
||||
"sanctuary:oak_boat_2x3",
|
||||
"sanctuary:oak_boat_3x2",
|
||||
"sanctuary:spruce_boat_3x1",
|
||||
"sanctuary:spruce_boat_1x3",
|
||||
"sanctuary:spruce_boat_2x3",
|
||||
"sanctuary:spruce_boat_3x2",
|
||||
"sanctuary:birch_boat_3x1",
|
||||
"sanctuary:birch_boat_1x3",
|
||||
"sanctuary:birch_boat_2x3",
|
||||
"sanctuary:birch_boat_3x2",
|
||||
"sanctuary:jungle_boat_3x1",
|
||||
"sanctuary:jungle_boat_1x3",
|
||||
"sanctuary:jungle_boat_2x3",
|
||||
"sanctuary:jungle_boat_3x2",
|
||||
"sanctuary:acacia_boat_3x1",
|
||||
"sanctuary:acacia_boat_1x3",
|
||||
"sanctuary:acacia_boat_2x3",
|
||||
"sanctuary:acacia_boat_3x2",
|
||||
"sanctuary:dark_oak_boat_3x1",
|
||||
"sanctuary:dark_oak_boat_1x3",
|
||||
"sanctuary:dark_oak_boat_2x3",
|
||||
"sanctuary:dark_oak_boat_3x2",
|
||||
"sanctuary:mangrove_boat_3x1",
|
||||
"sanctuary:mangrove_boat_1x3",
|
||||
"sanctuary:mangrove_boat_2x3",
|
||||
"sanctuary:mangrove_boat_3x2",
|
||||
"sanctuary:cherry_boat_3x1",
|
||||
"sanctuary:cherry_boat_1x3",
|
||||
"sanctuary:cherry_boat_2x3",
|
||||
"sanctuary:cherry_boat_3x2",
|
||||
"sanctuary:pale_oak_boat_3x1",
|
||||
"sanctuary:pale_oak_boat_1x3",
|
||||
"sanctuary:pale_oak_boat_2x3",
|
||||
"sanctuary:pale_oak_boat_3x2",
|
||||
"sanctuary:poplar_boat_3x1",
|
||||
"sanctuary:poplar_boat_1x3",
|
||||
"sanctuary:poplar_boat_2x3",
|
||||
"sanctuary:poplar_boat_3x2",
|
||||
"sanctuary:bamboo_raft_3x1",
|
||||
"sanctuary:bamboo_raft_1x3",
|
||||
"sanctuary:bamboo_raft_2x3",
|
||||
"sanctuary:bamboo_raft_3x2",
|
||||
"sanctuary:oak_boat_1x2",
|
||||
"sanctuary:oak_boat_2x1",
|
||||
"sanctuary:spruce_boat_1x2",
|
||||
"sanctuary:spruce_boat_2x1",
|
||||
"sanctuary:birch_boat_1x2",
|
||||
"sanctuary:birch_boat_2x1",
|
||||
"sanctuary:jungle_boat_1x2",
|
||||
"sanctuary:jungle_boat_2x1",
|
||||
"sanctuary:acacia_boat_1x2",
|
||||
"sanctuary:acacia_boat_2x1",
|
||||
"sanctuary:dark_oak_boat_1x2",
|
||||
"sanctuary:dark_oak_boat_2x1",
|
||||
"sanctuary:mangrove_boat_1x2",
|
||||
"sanctuary:mangrove_boat_2x1",
|
||||
"sanctuary:cherry_boat_1x2",
|
||||
"sanctuary:cherry_boat_2x1",
|
||||
"sanctuary:pale_oak_boat_1x2",
|
||||
"sanctuary:pale_oak_boat_2x1",
|
||||
"sanctuary:poplar_boat_1x2",
|
||||
"sanctuary:poplar_boat_2x1",
|
||||
"sanctuary:bamboo_raft_1x2",
|
||||
"sanctuary:bamboo_raft_2x1"
|
||||
]
|
||||
}
|
||||
}
|
||||
+36
-2
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate the source pack or stage an installable packwiz directory (stdlib only)."""
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -80,6 +81,9 @@ def check_index(base):
|
||||
|
||||
def check():
|
||||
values = properties()
|
||||
require(re.fullmatch(r'beta\.(?!000$)[0-9]{3}', values['mod_version']),
|
||||
'Expected Sanctuary version beta.001 through beta.999')
|
||||
require(values['mod_version'] == values['pack_version'], 'Sanctuary mod/pack version drift')
|
||||
pack, entries = check_index(SOURCE)
|
||||
require(pack['versions']['minecraft'] == values['minecraft_version'], 'Minecraft version drift')
|
||||
require(pack['versions']['fabric'] == values['loader_version'], 'Fabric Loader version drift')
|
||||
@@ -89,7 +93,8 @@ def check():
|
||||
'Fabric API version drift')
|
||||
require(any(e['file'] == 'mods/fabric-api.pw.toml' and e.get('metafile') for e in entries),
|
||||
'Fabric API is missing from the index')
|
||||
print('Pack versions and index hashes verified.')
|
||||
require((SOURCE / 'icon.png').read_bytes() == (ROOT / 'mods/sanctuary/src/main/resources/assets/sanctuary/icon.png').read_bytes(), 'Sanctuary mod and instance icons differ')
|
||||
print('Pack versions, icon and index hashes verified.')
|
||||
return values, entries
|
||||
|
||||
|
||||
@@ -105,6 +110,34 @@ def assemble(jar_url=None):
|
||||
values, entries = check()
|
||||
jar = ROOT / 'mods/sanctuary/build/libs' / ('sanctuary-' + values['mod_version'] + '.jar')
|
||||
require(jar.is_file(), 'Build Sanctuary before assembling the pack: ' + str(jar))
|
||||
# Demeure is autonomous but distributed as a Fabric nested mod in Sanctuary.
|
||||
with zipfile.ZipFile(jar) as archive:
|
||||
metadata = json.loads(archive.read('fabric.mod.json'))
|
||||
require(metadata['version'] == values['mod_version'], 'Stale Sanctuary JAR')
|
||||
require(archive.read(metadata['icon']) == (SOURCE / 'icon.png').read_bytes(), 'Stale Sanctuary icon in JAR')
|
||||
nested = 'META-INF/jars/demeure-' + values['mod_version'] + '.jar'
|
||||
require({'file': nested} in metadata.get('jars', []), 'Demeure nested mod is not declared')
|
||||
data = archive.read(nested)
|
||||
built_demeure = ROOT / 'mods/demeure/build/libs' / ('demeure-' + values['mod_version'] + '.jar')
|
||||
require(built_demeure.is_file() and data == built_demeure.read_bytes(), 'Stale nested Demeure JAR')
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as demeure:
|
||||
info = json.loads(demeure.read('fabric.mod.json'))
|
||||
require(info['id'] == 'demeure' and info['version'] == values['mod_version'], 'Demeure metadata drift')
|
||||
require('LICENSE_demeure' in demeure.namelist() and 'PROVENANCE.md' in demeure.namelist(), 'Demeure notices missing')
|
||||
jei_path = 'META-INF/jars/jei-sanctuary-26.3-pre-2-30.32.0-sanctuary.2.jar'
|
||||
require({'file': jei_path} in metadata.get('jars', []), 'JEI source fork is not declared')
|
||||
jei_data = archive.read(jei_path)
|
||||
built_jei = ROOT / 'mods/jei/build/libs' / Path(jei_path).name
|
||||
require(built_jei.is_file() and jei_data == built_jei.read_bytes(), 'Stale JEI source fork')
|
||||
with zipfile.ZipFile(io.BytesIO(jei_data)) as jei:
|
||||
info = json.loads(jei.read('fabric.mod.json'))
|
||||
require(info['id'] == 'jei' and info['version'] == '30.32.0-sanctuary.2', 'JEI metadata drift')
|
||||
require(info['depends']['minecraft'] == values['minecraft_version'].replace('-pre-', '-pre.'), 'JEI Minecraft version drift')
|
||||
require(all(path in jei.namelist() for path in ('LICENSE.txt', 'PROVENANCE.md',
|
||||
'licenses/baked-substring-index.txt', 'licenses/suffixtree.txt')), 'JEI notices missing')
|
||||
require(len(info.get('jars', [])) == 2, 'JEI runtime libraries missing')
|
||||
for library in info['jars']:
|
||||
require(jei.read(library['file']).startswith(b'PK'), 'Invalid nested JEI library')
|
||||
destination = ROOT / ('build/packwiz-release' if jar_url else 'build/packwiz')
|
||||
# Only this generated staging directory is replaced; never touch a game instance.
|
||||
if destination.exists():
|
||||
@@ -164,7 +197,7 @@ def prism(pack_url):
|
||||
# Prism instance.cfg uses Qt INI escaping for quotation marks.
|
||||
config = '\n'.join([
|
||||
'[General]', 'ConfigVersion=1.3', 'InstanceType=OneSix',
|
||||
'name=Sanctuary Beta', 'iconKey=default', 'AutomaticJava=true',
|
||||
'name=Sanctuary - ' + values['pack_version'], 'iconKey=sanctuary-beta', 'AutomaticJava=true',
|
||||
'OverrideJavaLocation=false', 'IgnoreJavaCompatibility=false',
|
||||
'OverrideMemory=true', 'MinMemAlloc=512', 'MaxMemAlloc=4096',
|
||||
'ManagedPack=false', 'OverrideCommands=true', 'LogPrePostOutput=true',
|
||||
@@ -182,6 +215,7 @@ def prism(pack_url):
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(destination, 'w', zipfile.ZIP_DEFLATED) as archive:
|
||||
archive.writestr('instance.cfg', config)
|
||||
archive.write(SOURCE / 'icon.png', 'sanctuary-beta.png')
|
||||
archive.writestr('mmc-pack.json', json.dumps(components, indent=2) + '\n')
|
||||
archive.writestr('minecraft/packwiz-installer-bootstrap.jar', bootstrap)
|
||||
print('Prism auto-update template: ' + str(destination))
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reconstruct the MIT JEI source fork from an immutable upstream commit and patch."""
|
||||
from pathlib import Path
|
||||
import hashlib, io, shutil, subprocess, tarfile
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
COMMIT='aae2dfcfb82e6b5bce787ba72bb2eda7339b274f'
|
||||
PATCH=ROOT/'mods/jei/patches/26.3.patch'
|
||||
DEST=ROOT/'build/jei-fork'
|
||||
STAMP=COMMIT+':'+hashlib.sha256(PATCH.read_bytes()+Path(__file__).read_bytes()).hexdigest()
|
||||
if (DEST/'.sanctuary-source').exists() and (DEST/'.sanctuary-source').read_text()==STAMP:
|
||||
raise SystemExit(0)
|
||||
mirror=ROOT/'build/jei-26.2-upstream'
|
||||
if not (mirror/'.git').exists():
|
||||
subprocess.run(['git','clone','--no-checkout','https://github.com/mezz/JustEnoughItems.git',str(mirror)],check=True)
|
||||
subprocess.run(['git','-C',str(mirror),'cat-file','-e',COMMIT+'^{commit}'],check=True)
|
||||
archive=subprocess.check_output(['git','-C',str(mirror),'archive',COMMIT])
|
||||
tmp=ROOT/'build/jei-fork-preparing'
|
||||
if tmp.exists(): shutil.rmtree(tmp)
|
||||
tmp.mkdir(parents=True)
|
||||
with tarfile.open(fileobj=io.BytesIO(archive)) as tar:
|
||||
for entry in tar:
|
||||
target=(tmp/entry.name).resolve()
|
||||
if tmp.resolve() not in target.parents or not (entry.isfile() or entry.isdir()):
|
||||
raise RuntimeError('Unexpected upstream archive entry: '+entry.name)
|
||||
if entry.isdir(): target.mkdir(parents=True,exist_ok=True)
|
||||
else:
|
||||
target.parent.mkdir(parents=True,exist_ok=True)
|
||||
target.write_bytes(tar.extractfile(entry).read())
|
||||
subprocess.run(['patch','-E','-p1','--batch','--forward','-i',str(PATCH)],cwd=tmp,check=True,stdout=subprocess.DEVNULL)
|
||||
(tmp/'.sanctuary-source').write_text(STAMP)
|
||||
if DEST.exists(): shutil.rmtree(DEST)
|
||||
tmp.rename(DEST)
|
||||
print('JEI source verified and patched: '+COMMIT)
|
||||
@@ -182,7 +182,7 @@ def main():
|
||||
f'Fabric {values["loader_version"]}.\n\nSource: {head}.\n')
|
||||
origin = git('remote', 'get-url', 'origin')
|
||||
gitea = Gitea(origin)
|
||||
tag = 'v' + version
|
||||
tag = version
|
||||
existing_commit = remote_tag_commit(tag)
|
||||
pack.require(existing_commit is None or existing_commit == head,
|
||||
'Release tag already belongs to another source commit; increment the version')
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stage the optional flat-world test profile, without altering the regular pack."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
from pack import ROOT, properties, read_toml, check_index, sha256, require
|
||||
|
||||
version = properties()['mod_version']
|
||||
source = ROOT / 'build/packwiz'
|
||||
destination = ROOT / 'build/packwiz-test'
|
||||
jar = ROOT / 'mods/sanctuary-test/build/libs' / f'sanctuary-test-{version}.jar'
|
||||
check_index(source)
|
||||
require(read_toml(source / 'pack.toml')['version'] == version, 'Regular staged pack is stale')
|
||||
with zipfile.ZipFile(jar) as archive:
|
||||
metadata = json.loads(archive.read('fabric.mod.json'))
|
||||
require(metadata['id'] == 'sanctuary_test' and metadata['version'] == version, 'Test module version drift')
|
||||
require(metadata['depends']['sanctuary'] == version, 'Test module needs the matching Sanctuary build')
|
||||
require('LICENSE_sanctuary_test' in archive.namelist(), 'Missing test module license')
|
||||
if destination.exists():
|
||||
shutil.rmtree(destination)
|
||||
shutil.copytree(source, destination)
|
||||
shutil.copy2(jar, destination / 'mods' / jar.name)
|
||||
entries = read_toml(destination / 'index.toml')['files']
|
||||
entries.append({'file': 'mods/' + jar.name, 'hash': sha256(jar)})
|
||||
lines = ['hash-format = "sha256"', '']
|
||||
for entry in sorted(entries, key=lambda e: e['file']):
|
||||
lines.append('[[files]]')
|
||||
lines.extend(key + ' = ' + json.dumps(value) for key, value in entry.items())
|
||||
lines.append('')
|
||||
index = destination / 'index.toml'
|
||||
index.write_text('\n'.join(lines), encoding='utf-8')
|
||||
manifest = (source / 'pack.toml').read_text(encoding='utf-8').replace('name = "Sanctuary"', 'name = "Sanctuary Test"', 1)
|
||||
manifest, count = re.subn(r'(?m)^hash = "[a-f0-9]+"$', 'hash = "' + sha256(index) + '"', manifest)
|
||||
require(count == 1, 'Expected a single test pack index hash')
|
||||
(destination / 'pack.toml').write_text(manifest, encoding='utf-8')
|
||||
check_index(destination)
|
||||
print('Optional test pack staged at ' + str(destination))
|
||||
Reference in New Issue
Block a user