Integrate verified Sanctuary beta.110 with clay workshop, eggs and seasonal snow
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-17 03:10:39 +02:00
parent 49feb5a376
commit 468743bfe7
1111 changed files with 41665 additions and 534 deletions
+212
View File
@@ -0,0 +1,212 @@
{
"white": {
"brick": [
160,
155,
163
],
"clay": [
198,
196,
200
],
"source_sha256": "67ede486baa239fcb86f8f01dd9fa07d60e7ef9e64999dcf098359585223fda3"
},
"orange": {
"brick": [
187,
96,
33
],
"clay": [
211,
170,
141
],
"source_sha256": "8bfa041d13a01c042131e66c074eb79191dc034fc99a3a58ee6b17773cb17441"
},
"magenta": {
"brick": [
187,
72,
182
],
"clay": [
211,
159,
208
],
"source_sha256": "7e3de34f443ac6ffe22ad18abc92ed65250109eec140e43106c88911ab57d4c5"
},
"light_blue": {
"brick": [
42,
111,
168
],
"clay": [
145,
176,
202
],
"source_sha256": "a617f1226fc81b162aa52a5b490017220e51cb01d475b656bb078cfe427ac6d2"
},
"yellow": {
"brick": [
163,
143,
56
],
"clay": [
200,
191,
152
],
"source_sha256": "d5212ed932c1cf5430dec2ff2e324ea8c9e0a3d4940c4600255ca406c7af7492"
},
"lime": {
"brick": [
78,
159,
61
],
"clay": [
162,
198,
154
],
"source_sha256": "0aa80fd773294deb6edf6756b34fd4aec9aad65f24933d59ec595d26198ee4e8"
},
"pink": {
"brick": [
204,
154,
209
],
"clay": [
218,
196,
221
],
"source_sha256": "2d197f8c27150c59b4cafdb568a77e1e9a6d2377790ec39626506b24bbe3af58"
},
"gray": {
"brick": [
88,
90,
99
],
"clay": [
116,
118,
125
],
"source_sha256": "146248d1c67977e9cd667c592ca7b1905e5fd4c08b719cc83df3b70dac724e2b",
"clay_contrast": 1.35
},
"light_gray": {
"brick": [
94,
92,
100
],
"clay": [
169,
168,
172
],
"source_sha256": "22ed4652ff235d16a7fcdebc870516280480fef0fd15a11866dd2bde6a2cff8d"
},
"cyan": {
"brick": [
92,
156,
148
],
"clay": [
168,
197,
193
],
"source_sha256": "65b9d6aea4c75597d33f85640f36dc904004ad82556a227393de587deaece29d"
},
"purple": {
"brick": [
142,
42,
223
],
"clay": [
190,
145,
227
],
"source_sha256": "6e0f71dbdbcb1dc960799030d3faaefa59d266d979e94ffe739163c589326420"
},
"blue": {
"brick": [
74,
89,
136
],
"clay": [
160,
167,
188
],
"source_sha256": "4ddfe1960668269014c1be6c40208a6d65f3c053d4a29a870751f1843db43fb7"
},
"brown": {
"brick": [
83,
60,
41
],
"clay": [
164,
154,
145
],
"source_sha256": "e7b8c560cd24031219d753eaa5abc658edc6a4485977efaf986097f872abab0c"
},
"green": {
"brick": [
75,
118,
59
],
"clay": [
160,
180,
153
],
"source_sha256": "6af83f7e1a8c42e32abbf381471ace1bc7c315a2eedbf9361b70002ce79a30d7"
},
"red": {
"brick": [
118,
61,
59
],
"clay": [
180,
154,
153
],
"source_sha256": "a0feb57b088fa8d9fc94adfcdbe5bbe9c00bcb4bd8225e781c06c28d9db61934"
},
"black": {
"brick": [
50,
51,
47
],
"clay": [
68,
69,
65
],
"source_sha256": "6600aa4c16a5faf63188e8da6c5c39b1aa28c309171fd5bccc29ed2283dee0f5",
"clay_contrast": 1.35
}
}
+1
View File
@@ -0,0 +1 @@
[[9, 19, 23], [9, 25, 26], [10, 19, 23], [10, 25, 26], [11, 16, 17], [11, 18, 31], [12, 13, 32], [12, 34, 35], [13, 13, 35], [14, 12, 36], [15, 12, 36], [16, 11, 37], [17, 10, 37], [18, 10, 38], [19, 10, 38], [30, 19, 20], [30, 23, 24], [30, 26, 29], [31, 16, 30], [31, 31, 32], [32, 14, 34], [32, 35, 36], [33, 14, 34], [33, 35, 36], [34, 13, 36], [35, 11, 37], [36, 11, 37], [37, 10, 38], [38, 10, 38], [39, 10, 38], [40, 10, 38], [41, 10, 19], [41, 20, 24], [41, 26, 29], [41, 30, 38]]
+103
View File
@@ -0,0 +1,103 @@
"""Native 26.3 resources + author-approved deterministic vanilla pixel recoloring.
Requires Pillow. Source brick textures already installed in assets/sanctuary/textures/block.
Usage: python generate-colored-bricks.py --minecraft-jar <26.3 minecraft-client.jar>
"""
import argparse, hashlib, json, statistics, zipfile
from pathlib import Path
from PIL import Image, ImageDraw
ap=argparse.ArgumentParser();ap.add_argument('--minecraft-jar',required=True);args=ap.parse_args()
root=Path(__file__).resolve().parents[1];res=root/'mods/sanctuary/src/main/resources';assets=res/'assets/sanctuary';data=res/'data'
colors=['white','orange','magenta','light_blue','yellow','lime','pink','gray','light_gray','cyan','purple','blue','brown','green','red','black']
fr=[('blanche','blanches'),('orange','orange'),('magenta','magenta'),('bleu clair','bleu clair'),('jaune','jaunes'),('vert clair','vert clair'),('rose','roses'),('grise','grises'),('gris clair','gris clair'),('cyan','cyan'),('violette','violettes'),('bleue','bleues'),('marron','marron'),('verte','vertes'),('rouge','rouges'),('noire','noires')]
z=zipfile.ZipFile(args.minecraft_jar)
def write(path,obj):
path.parent.mkdir(parents=True,exist_ok=True);path.write_text(json.dumps(obj,ensure_ascii=False,indent=2)+'\n')
def native(path):return json.loads(z.read(path))
def copy_native(path,dest,mapping):
text=z.read(path).decode()
for a,b in sorted(mapping.items(),key=lambda x:-len(x[0])):text=text.replace(a,b)
write(dest,json.loads(text))
def id(name):return 'sanctuary:'+name
def luminance(p):return .2126*p[0]+.7152*p[1]+.0722*p[2]
def sample(image):
pixels=[p[:3] for p in image.convert('RGBA').getdata() if p[3]]
chroma=[max(p)-min(p) for p in pixels]
if max(chroma)>20:
threshold=sorted(chroma)[len(chroma)//2];pixels=[p for p in pixels if max(p)-min(p)>=threshold]
else:
threshold=sorted(map(luminance,pixels))[int(len(pixels)*.7)];pixels=[p for p in pixels if luminance(p)<=threshold]
return tuple(round(statistics.mean(p[i] for p in pixels)) for i in range(3))
def recolor(src,target,contrast=.85):
image=Image.open(__import__('io').BytesIO(z.read('assets/minecraft/textures/'+src+'.png'))).convert('RGBA')
pixels=list(image.getdata());mean=statistics.mean(luminance(p) for p in pixels if p[3])
out=[]
for p in pixels:
if not p[3]:out.append(p);continue
# Retain every source shading step and alpha, never resize or redraw a pixel.
delta=luminance(p)-mean
out.append(tuple(max(0,min(255,round(c+delta*contrast))) for c in target)+(p[3],))
image.putdata(out);return image
recipes=[];block_ids=[];item_ids=[];palette={}
def tag(namespace,kind,name,values):
path=data/namespace/'tags'/kind/(name+'.json');old=json.loads(path.read_text()) if path.exists() else {'replace':False,'values':[]}
old['values']=list(dict.fromkeys(old['values']+values));write(path,old)
def recipe(name,obj,ingredient):
write(data/'sanctuary/recipe'/(name+'.json'),obj);recipes.append(id(name))
write(data/'sanctuary/advancement/recipes/colored_bricks'/(name+'.json'),{'parent':'minecraft:recipes/root','criteria':{'has_material':{'trigger':'minecraft:inventory_changed','conditions':{'items':[{'items':ingredient}]}},'has_the_recipe':{'trigger':'minecraft:recipe_unlocked','conditions':{'recipes':id(name)}}},'requirements':[['has_material','has_the_recipe']],'rewards':{'recipes':[id(name)]}})
def shaped(name,pattern,key,result,count=1):recipe(name,{'type':'minecraft:crafting_shaped','category':'building','pattern':pattern,'key':key,'result':{'id':result,'count':count}},list(key.values())[0])
labels={lang:json.loads((assets/'lang'/f'{lang}.json').read_text()) for lang in ['fr_fr','en_us']}
version=dict(line.split('=',1) for line in (root/'gradle.properties').read_text().splitlines() if '=' in line and not line.startswith('#'))['pack_version']
preview=Image.new('RGB',(840,16*68+50),(30,31,34));draw=ImageDraw.Draw(preview);draw.text((12,12),version.upper()+' bricks / clay block / clay ball / brick item - native 16 x 16',fill='white')
for index,color in enumerate(colors):
names=[color+'_'+suffix for suffix in ['bricks','brick_slab','brick_stairs','clay','clay_ball','brick']];bricks,slab,stairs,clay,ball,brick=names
source=assets/'textures/block'/f'{bricks}.png';image=Image.open(source);assert image.size==(16,16),source
target=sample(image)
# Dark neutrals need less white to stay distinct from light gray; retain the source hue.
weight={'gray':.80,'black':.90}.get(color,.45)
pastel=tuple(round(c*weight+230*(1-weight)) for c in target)
clay_contrast=1.35 if color in ('gray','black') else .85
palette[color]={'brick':target,'clay':pastel,'source_sha256':hashlib.sha256(source.read_bytes()).hexdigest()}
if color in ('gray','black'):palette[color]['clay_contrast']=clay_contrast
images=[image.convert('RGBA')]
for src,name,tint in [('block/clay',clay,pastel),('item/clay_ball',ball,pastel),('item/brick',brick,target)]:
out=recolor(src,tint,clay_contrast if src in ('block/clay','item/clay_ball') else .85);path=assets/'textures'/src.split('/')[0]/(name+'.png');path.parent.mkdir(parents=True,exist_ok=True);out.save(path);images.append(out)
draw.text((12,50+index*68+22),color,fill='white')
for col,img in enumerate(images):
preview.paste(img.resize((64,64),Image.Resampling.NEAREST),(170+col*160,50+index*68),img.resize((64,64),Image.Resampling.NEAREST))
mapping={f'minecraft:{s}':id(n) for s,n in zip(['bricks','brick_slab','brick_stairs','clay','clay_ball','brick'],names)}
mapping.update({f'minecraft:block/{s}':f'sanctuary:block/{n}' for s,n in zip(['bricks','brick_slab','brick_stairs','clay'],names)})
mapping.update({f'minecraft:blocks/{s}':f'sanctuary:blocks/{n}' for s,n in zip(['bricks','brick_slab','brick_stairs','clay'],names)})
mapping.update({f'minecraft:item/{s}':f'sanctuary:item/{n}' for s,n in [('clay_ball',ball),('brick',brick)]})
for vanilla,name in zip(['bricks','brick_slab','brick_stairs','clay'],names):
for category in ['blockstates','models/block','items']:
copy_native('assets/minecraft/'+category+'/'+vanilla+'.json',assets/category/(name+'.json'),mapping)
copy_native('data/minecraft/loot_table/blocks/'+vanilla+'.json',data/'sanctuary/loot_table/blocks'/(name+'.json'),mapping)
for vanilla,name in [('brick_slab_top',slab+'_top'),('brick_stairs_inner',stairs+'_inner'),('brick_stairs_outer',stairs+'_outer')]:
copy_native('assets/minecraft/models/block/'+vanilla+'.json',assets/'models/block'/(name+'.json'),mapping)
for vanilla,name in [('clay_ball',ball),('brick',brick)]:
for category in ['models/item','items']:copy_native('assets/minecraft/'+category+'/'+vanilla+'.json',assets/category/(name+'.json'),mapping)
block_ids.extend(map(id,names[:4]));item_ids.extend(map(id,names));
singular,plural=fr[index];en=color.replace('_',' ').title()
for key,eng,french,isblock in zip(names,[f'{en} Bricks',f'{en} Brick Slab',f'{en} Brick Stairs',f'{en} Clay',f'{en} Clay Ball',f'{en} Brick'],[f'Briques {plural}',f'Dalle de briques {plural}',f'Escalier de briques {plural}',f'Argile {singular}',f"Boule dargile {singular}",f'Brique {singular}'],[True]*4+[False]*2):
translation=('block.' if isblock else 'item.')+'sanctuary.'+key;labels['en_us'][translation]=eng;labels['fr_fr'][translation]=french
dye='minecraft:'+color+'_dye'
for output,ingredient in [(ball,'#sanctuary:clay_balls'),(clay,'#sanctuary:clay_blocks'),(brick,'#sanctuary:brick_items'),(bricks,'#sanctuary:brick_blocks')]:
shaped(output+'_from_dye',['###','#X#','###'],{'#':ingredient,'X':dye},id(output),8)
shaped(clay,['##','##'],{'#':id(ball)},id(clay));shaped(bricks,['##','##'],{'#':id(brick)},id(bricks))
shaped(slab,['###'],{'#':id(bricks)},id(slab),6);shaped(stairs,['# ','## ','###'],{'#':id(bricks)},id(stairs),4)
for output,amount in [(slab,2),(stairs,1)]:recipe(output+'_from_stonecutting',{'type':'minecraft:stonecutting','ingredient':id(bricks),'result':{'id':id(output),'count':amount}},id(bricks))
for output,ingredient,exp in [(id(brick),ball,.3),('minecraft:'+color+'_terracotta',clay,.35)]:
recipe(ingredient+'_smelting',{'type':'minecraft:smelting','cookingtime':200,'experience':exp,'ingredient':id(ingredient),'result':{'id':output}},id(ingredient))
tag('minecraft','block','mineable/pickaxe',list(map(id,names[:3])));tag('minecraft','block','mineable/shovel',[id(clay)])
for kind in ['block','item']:
tag('minecraft',kind,'slabs',[id(slab)]);tag('minecraft',kind,'stairs',[id(stairs)])
for name,values in [('clay_balls',['minecraft:clay_ball',id(ball)]),('clay_blocks',['minecraft:clay',id(clay)]),('brick_items',['minecraft:brick',id(brick)]),('brick_blocks',['minecraft:bricks',id(bricks)])]:tag('sanctuary','item',name,values)
for lang,obj in labels.items():write(assets/'lang'/f'{lang}.json',obj)
p=root/'scripts/data/collections-sanctuary.json';obj=json.loads(p.read_text());obj['clay_and_bricks']={'additional_recipes':recipes,'additional_items':item_ids,'members':{'blocks':block_ids,'items':item_ids}};write(p,obj)
# The reviewed collection compiler remains the sole writer of runtime taxonomy.
import subprocess,sys
subprocess.run([sys.executable,str(root/'scripts/compile_collections.py'),'--write'],check=True)
write(root/'tools/colored-bricks-palette.json',palette)
(root/'build').mkdir(exist_ok=True)
preview.save(root/'build'/f'colored-bricks-{version}-preview.png')
print(f'{len(colors)} colors, {len(block_ids)} blocks, {len(item_ids)} items including blocks, {len(recipes)} recipes')
+23
View File
@@ -2,7 +2,9 @@
No image resampling: JSON UVs select thirds of the supplied source PNGs.
"""
import json
import copy
from pathlib import Path
embers = json.loads(Path(__file__).with_name('fourneau-embers.json').read_text())
root = Path(__file__).resolve().parents[1] / 'mods/sanctuary/src/main/resources/assets/sanctuary/models/block/fourneau'
root.mkdir(parents=True, exist_ok=True)
for facing in ('north', 'east', 'south', 'west'):
@@ -16,3 +18,24 @@ for facing in ('north', 'east', 'south', 'west'):
model = {'textures':{n:'sanctuary:block/fourneau/'+n for n in ('front','side','top')} | {'particle':'sanctuary:block/fourneau/side'},
'elements':[{'from':[0,0,0],'to':[16,16,16],'faces':faces}]}
(root / f'{facing}_{x}_{y}_{z}.json').write_text(json.dumps(model,indent=2)+'\n')
# Preserve every original masonry pixel. Only the dark aperture strips
# receive the generated fire artwork, slightly ahead of the native face.
lit = copy.deepcopy(model)
lit['textures']['embers'] = 'sanctuary:block/fourneau/front_on'
front = {'north': z == 0, 'south': z == 2, 'east': x == 2, 'west': x == 0}[facing]
u0 = {'north': 2-x, 'south': x, 'east': 2-z, 'west': z}[facing]*16
v0 = (2-y)*16
if front:
for row, start, end in embers:
lo, hi = max(start,u0), min(end,u0+16)
if not v0 <= row < v0+16 or lo >= hi:
continue
a,b = lo-u0,hi-u0
bottom,top = 15-(row-v0),16-(row-v0)
if facing == 'north': begin,finish = [16-b,bottom,-.002],[16-a,top,-.002]
elif facing == 'south': begin,finish = [a,bottom,16.002],[b,top,16.002]
elif facing == 'east': begin,finish = [16.002,bottom,16-b],[16.002,top,16-a]
else: begin,finish = [-.002,bottom,a],[-.002,top,b]
lit['elements'].append({'from':begin,'to':finish,'faces':{facing:{
'texture':'#embers','cullface':facing,'uv':[lo/3,row/3,hi/3,(row+1)/3]}}})
(root / f'{facing}_{x}_{y}_{z}_lit.json').write_text(json.dumps(lit,indent=2)+'\n')
+17
View File
@@ -0,0 +1,17 @@
"""Tile the creator's 48x48 top and 48x16 side, without resampling either PNG."""
import json
from pathlib import Path
root=Path(__file__).resolve().parents[1]/'mods/sanctuary/src/main/resources/assets/sanctuary'
models=root/'models/block/metatable';models.mkdir(parents=True,exist_ok=True)
variants={}
for z in range(3):
for x in range(3):
part=x+3*z;faces={}
for d,u in [('north',2-x),('south',x),('east',2-z),('west',z)]:
faces[d]={'texture':'#side','cullface':d,'uv':[u*16/3,0,(u+1)*16/3,16]}
faces['up']={'texture':'#top','cullface':'up','uv':[x*16/3,z*16/3,(x+1)*16/3,(z+1)*16/3]}
faces['down']={'texture':'#bottom','cullface':'down','uv':[0,0,16,16]}
model={'textures':{'top':'sanctuary:block/metatable/top','side':'sanctuary:block/metatable/side','bottom':'minecraft:block/oak_planks','particle':'sanctuary:block/metatable/side'},'elements':[{'from':[0,0,0],'to':[16,16,16],'faces':faces}]}
(models/f'{part}.json').write_text(json.dumps(model,indent=2)+'\n')
variants[f'part={part}']={'model':f'sanctuary:block/metatable/{part}'}
(root/'blockstates/metatable.json').write_text(json.dumps({'variants':variants},indent=2)+'\n')
+11 -3
View File
@@ -18,7 +18,15 @@ def sides(x,y,thin=False,extended=False):
for d,u,r in [('up',x,0),('down',2-x,180),('west',y,270),('east',2-y,90)]:
result[d]=face('side',[u*16/3,4 if extended else 0,(u+1)*16/3,4 if thin else 16],r,None if thin else d)
return result
def shaft():return {'from':[2,2,0],'to':[14,14,16],'faces':{d:face('top',[6,0,10,16]) for d in rotations}}
def shaft(start=0,end=16):
# Use only the central 16px wood tile of the 48px plate, at native pixel
# density. Short base/head segments sample only their actual length.
faces={}
for d in rotations:
pixels=([2,2,14,14] if d in ('north','south') else
[2,start,14,end] if d in ('up','down') else [start,2,end,14])
faces[d]=face('top',[(16+p)/3 for p in pixels])
return {'from':[2,2,start],'to':[14,14,end],'faces':faces}
for part in range(9):
x,y=part%3,part//3
for front in ['top','top_sticky','inner']:
@@ -26,13 +34,13 @@ for part in range(9):
faces=sides(x,y,extended=extended);faces['north']=face(front,uv(2-x,2-y),cull=None if extended else 'north');faces['south']=face('bottom',uv(x,2-y),cull='south')
pieces=[{'from':[0,0,4 if extended else 0],'to':[16,16,16],'faces':faces}]
if extended and part==4:
rod=shaft();rod['to'][2]=4;pieces.append(rod)
pieces.append(shaft(0,4))
write(f'base_{front}_{part}',pieces)
for sticky in [False,True]:
faces=sides(x,y,True);faces['north']=face('top_sticky' if sticky else 'top',uv(2-x,2-y));faces['south']=face('top',uv(x,2-y))
pieces=[{'from':[0,0,0],'to':[16,16,4],'faces':faces}]
if part==4:
rod=shaft();rod['from'][2]=4;pieces.append(rod)
pieces.append(shaft(4,16))
write(f'head_{str(sticky).lower()}_{part}',pieces)
write('rod',[shaft()])
(root/'blockstates').mkdir(parents=True,exist_ok=True)