Files
2026-08-04 19:05:38 -05:00

138 lines
5.0 KiB
Python

"""Build the vanilla entity-texture index for the model viewer.
Scans ``static/vanilla/entity/`` (extracted from a Minecraft client jar,
tracked in git) and writes ``static/vanilla/entity_index.json`` mapping each
entity name to the best "base skin" relative path, so the renderer can fall
back to real vanilla textures for models that have no pack textures of their
own.
"""
import json
import os
from django.conf import settings
ENTITY_ROOT = 'entity'
INDEX_NAME = 'entity_index.json'
def vanilla_dir():
"""The tracked static directory holding the vanilla texture set."""
return os.path.join(settings.BASE_DIR, 'static', 'vanilla')
# Entity names whose vanilla layout differs from the model-name convention.
ALIASES = {
'polar_bear': 'bear/polarbear.png',
'cave_spider': 'spider/cave_spider.png',
'husk': 'zombie/husk.png',
'drowned': 'zombie/drowned.png',
'stray': 'skeleton/stray.png',
'zoglin': 'hoglin/zoglin.png',
'piglin_brute': 'piglin/piglin_brute.png',
'zombified_piglin': 'piglin/zombified_piglin.png',
'horse': 'horse/horse_brown.png',
'donkey': 'horse/donkey.png',
'mule': 'horse/mule.png',
'skeleton_horse': 'horse/horse_skeleton.png',
'zombie_horse': 'horse/horse_zombie.png',
'evoker': 'illager/evoker.png',
'pillager': 'illager/pillager.png',
'vex': 'illager/vex.png',
'vindicator': 'illager/vindicator.png',
'ravager': 'illager/ravager.png',
'cat': 'cat/tabby.png',
'cow': 'cow/temperate_cow.png',
'pig': 'pig/temperate_pig.png',
'slime_size_0': 'slime/slime.png',
'slime_size_1': 'slime/slime.png',
'slime_size_3': 'slime/slime.png',
'player': 'player/wide/steve.png',
'player_slim': 'player/slim/steve.png',
}
# Texture names that are auxiliary layers, never a base skin.
_LAYER_KEYWORDS = (
'overlay', '_eyes', '_armor', '_collar', '_markings', '_tame', '_angry',
'_saddle', '_crack', '_outer', '_inner', '_layer', '_emissive', '_pelt',
'_fur', '_shell', '_crackiness',
)
# Entities whose base skin is one of several fur/colour variants. The preview
# picks one at random so the model matches the variety the pack shows in-game.
VARIANTS = {
'wolf': [
'wolf/wolf.png', 'wolf/wolf_ashen.png', 'wolf/wolf_black.png',
'wolf/wolf_chestnut.png', 'wolf/wolf_rusty.png', 'wolf/wolf_snowy.png',
'wolf/wolf_spotted.png', 'wolf/wolf_striped.png', 'wolf/wolf_woods.png',
],
'fox': ['fox/fox.png', 'fox/snow_fox.png'],
'cat': [
'cat/tabby.png', 'cat/black.png', 'cat/red.png', 'cat/siamese.png',
'cat/british_shorthair.png', 'cat/calico.png', 'cat/persian.png',
'cat/ragdoll.png', 'cat/white.png', 'cat/jellie.png', 'cat/all_black.png',
],
'cow': [
'cow/temperate_cow.png', 'cow/cold_cow.png', 'cow/warm_cow.png',
'cow/red_mooshroom.png', 'cow/brown_mooshroom.png',
],
}
def _base_candidates(entity):
"""Return (entity, [candidate relative paths]) in priority order."""
if entity in ALIASES:
return entity, [ALIASES[entity]]
root = os.path.join(vanilla_dir(), ENTITY_ROOT)
top = os.path.join(root, entity + '.png')
if os.path.isfile(top):
return entity, [entity + '.png']
folder = os.path.join(root, entity)
if not os.path.isdir(folder):
return entity, []
try:
names = sorted(os.listdir(folder))
except OSError:
return entity, []
pngs = [n for n in names if n.endswith('.png')]
exact = [f'{entity}/{n}' for n in pngs if n == f'{entity}.png']
default = [f'{entity}/{n}' for n in pngs if n in ('temperate_' + entity + '.png', 'default.png')]
plain = [
f'{entity}/{n}' for n in pngs
if not any(k in n for k in _LAYER_KEYWORDS)
]
return entity, exact + default + plain + [f'{entity}/{n}' for n in pngs]
def refresh_vanilla_index():
"""Rebuild the entity → {default, variants} skin map from the vanilla set."""
root = os.path.join(vanilla_dir(), ENTITY_ROOT)
if not os.path.isdir(root):
raise FileNotFoundError(f'{root} missing — extract vanilla entity textures first')
index = {}
for entry in sorted(os.listdir(root)):
rel = os.path.join(root, entry)
if entry.endswith('.png'):
entity = entry[:-4]
index.setdefault(entity, {'default': entry, 'variants': [entry]})
continue
if not os.path.isdir(rel):
continue
entity = entry
_, candidates = _base_candidates(entity)
if candidates and entity not in index:
index[entity] = {'default': candidates[0], 'variants': [candidates[0]]}
for entity, rel in ALIASES.items():
if entity not in index:
index[entity] = {'default': rel, 'variants': [rel]}
for entity, variants in VARIANTS.items():
if entity in index and variants:
index[entity]['default'] = variants[0]
index[entity]['variants'] = variants
out = os.path.join(vanilla_dir(), INDEX_NAME)
with open(out, 'w') as f:
json.dump(index, f, indent=1, sort_keys=True)
return index