Record Sanctuary sources through beta.060 and Git validation
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-15 09:29:11 +02:00
parent 1490fb2a80
commit e13f3cc1b8
1372 changed files with 92055 additions and 212 deletions
+213
View File
@@ -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 larchive 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 nest inclus.\n\n'
'Cette collection est privée et nest 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()
+82
View File
@@ -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()
+284
View File
@@ -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()
+29
View File
@@ -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"
]
}
}
+184
View File
@@ -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
View File
@@ -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))
+33
View File
@@ -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)
+1 -1
View File
@@ -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')
+39
View File
@@ -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))