backup befor new branch to test Blockbenh instead of regex renderer
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from library.vanilla_textures import refresh_vanilla_index
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Rebuild the vanilla entity-texture index (media/vanilla/entity_index.json).'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
index = refresh_vanilla_index()
|
||||
self.stdout.write(self.style.SUCCESS(f'Indexed {len(index)} entities.'))
|
||||
@@ -15,6 +15,7 @@ urlpatterns = [
|
||||
path('packs/<slug:slug>/versions/<int:version_id>/download/', views.version_download, name='version_download'),
|
||||
path('packs/<slug:slug>/versions/<int:version_id>/files/<uuid:file_uuid>/download/', views.version_file_download, name='version_file_download'),
|
||||
path('packs/<slug:slug>/versions/<int:version_id>/asset/<path:asset_path>', views.pack_asset, name='pack_asset'),
|
||||
path('vanilla/<path:asset_path>', views.vanilla_asset, name='vanilla_asset'),
|
||||
path('packs/<slug:slug>/guide/<int:version_id>/<uuid:file_uuid>/', views.guide_doc, name='guide_doc'),
|
||||
path('packs/<slug:slug>/gallery/upload/', views.asset_upload, name='asset_upload'),
|
||||
path('packs/<slug:slug>/gallery/<int:asset_id>/thumb/', views.asset_thumbnail, name='asset_thumbnail'),
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Build the vanilla entity-texture index for the model viewer.
|
||||
|
||||
Scans ``MEDIA_ROOT/vanilla/entity/`` (extracted from a Minecraft client jar)
|
||||
and writes ``MEDIA_ROOT/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'
|
||||
|
||||
# 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',
|
||||
)
|
||||
|
||||
|
||||
def _base_candidates(entity):
|
||||
"""Return (entity, [candidate relative paths]) in priority order."""
|
||||
if entity in ALIASES:
|
||||
return entity, [ALIASES[entity]]
|
||||
root = os.path.join(settings.MEDIA_ROOT, 'vanilla', 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 → base-skin map from the extracted vanilla set."""
|
||||
root = os.path.join(settings.MEDIA_ROOT, 'vanilla', 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]
|
||||
if not index.get(entity):
|
||||
index[entity] = entry
|
||||
continue
|
||||
if not os.path.isdir(rel):
|
||||
continue
|
||||
entity = entry
|
||||
_, candidates = _base_candidates(entity)
|
||||
if candidates and not index.get(entity):
|
||||
index[entity] = candidates[0]
|
||||
|
||||
for entity, rel in ALIASES.items():
|
||||
if not index.get(entity):
|
||||
index[entity] = rel
|
||||
|
||||
out = os.path.join(settings.MEDIA_ROOT, 'vanilla', INDEX_NAME)
|
||||
with open(out, 'w') as f:
|
||||
json.dump(index, f, indent=1, sort_keys=True)
|
||||
return index
|
||||
@@ -423,6 +423,19 @@ def api_packs_latest(request, namespace, pack_id):
|
||||
})
|
||||
|
||||
|
||||
def vanilla_asset(request, asset_path):
|
||||
"""Serve a bundled Minecraft vanilla resource (e.g. entity textures) — gated."""
|
||||
if not asset_path or asset_path.startswith('/') or '..' in asset_path.split('/'):
|
||||
raise Http404
|
||||
full = Path(settings.MEDIA_ROOT) / 'vanilla' / asset_path
|
||||
if not full.is_file():
|
||||
raise Http404
|
||||
content_type = mimetypes.guess_type(asset_path)[0] or 'application/octet-stream'
|
||||
response = HttpResponse(full.read_bytes(), content_type=content_type)
|
||||
response['Cache-Control'] = 'public, max-age=86400'
|
||||
return response
|
||||
|
||||
|
||||
def guide_doc(request, slug, version_id, file_uuid):
|
||||
"""Render one markdown guide document server-side (used by the switcher)."""
|
||||
version = get_object_or_404(
|
||||
|
||||
@@ -275,10 +275,29 @@ def read_models_manifest(path):
|
||||
for bone in geom.get('bones') or []:
|
||||
bone_count += 1
|
||||
cube_count += len(bone.get('cubes') or [])
|
||||
|
||||
entity = name.rsplit('/', 1)[-1]
|
||||
if entity.endswith('.geo.json'):
|
||||
entity = entity[:-len('.geo.json')]
|
||||
for suffix in ('.mf', '.fm', '.m', '.f', '.g'):
|
||||
if entity.endswith(suffix):
|
||||
entity = entity[:-len(suffix)]
|
||||
break
|
||||
|
||||
# A pack-provided full skin is the model's default texture when the
|
||||
# model has no per-bone textures of its own.
|
||||
default_texture = ''
|
||||
if not bone_textures and entity:
|
||||
for n in names:
|
||||
if n.endswith(f'/textures/entity/{entity}/{entity}.png') or n.endswith(f'/textures/entity/{entity}.png'):
|
||||
default_texture = n
|
||||
break
|
||||
models.append({
|
||||
'name': name.rsplit('/', 1)[-1],
|
||||
'member': name,
|
||||
'identifier': identifier,
|
||||
'entity': entity,
|
||||
'default_texture': default_texture,
|
||||
'bones': bone_count,
|
||||
'cubes': cube_count,
|
||||
'bone_textures': bone_textures,
|
||||
|
||||
Reference in New Issue
Block a user