working model cat backup

This commit is contained in:
JakeBreath
2026-08-04 21:47:37 -05:00
parent 50a70bfe31
commit be8c1a9ea8
6 changed files with 156 additions and 47 deletions
+68 -31
View File
@@ -12,6 +12,9 @@ import zipfile
INJECTOR_NAMES = {'V': 'Vaginal', 'M': 'Mouth', 'A': 'Anal'}
PROBLEM_TAGS = {'broken', 'bugged', 'borked'}
# Blockbench-compatible model formats accepted by the explicit Model category.
MODEL_FILE_EXTENSIONS = ('.bbmodel', '.geo.json', '.jem', '.gltf', '.glb')
def _decode(data):
"""Decode JSON text, tolerating a UTF-8 BOM (common in hand-edited packs)."""
@@ -239,53 +242,61 @@ def _resource_to_member(resource):
return f'assets/{resource}'
def read_models_manifest(path):
"""Build the Models/Textures manifest from GeckoLib geo models.
def read_models_manifest(path, category='non_pack'):
"""Build the Models/Textures manifest.
Returns None when the pack has no geo models. Each model records its geo
filename, identifier, bone/cube counts, and the afw_bone_textures map
(bone → texture zip member) so the viewer can texture it."""
For the explicit ``model`` category this scans the archive for any
Blockbench-compatible model file (``MODEL_FILE_EXTENSIONS``) anywhere in
the zip. For other categories it keeps the NoN GeckoLib geo-model
detection (``geckolib/models/*.geo.json``). Returns None when no models
are found. Each model records its filename, zip member, format, bone/cube
counts, and the afw_bone_textures map (bone → texture zip member).
"""
zf = _open_zip(path)
if zf is None:
return None
try:
names = set(zf.namelist())
geo_names = sorted(
n for n in names if '/geckolib/models/' in n and n.endswith('.geo.json')
)
if not geo_names:
if category == 'model':
model_names = sorted(
n for n in names
if not n.endswith('/') and n.lower().endswith(MODEL_FILE_EXTENSIONS)
)
else:
model_names = sorted(
n for n in names if '/geckolib/models/' in n and n.endswith('.geo.json')
)
if not model_names:
return None
models = []
for name in geo_names:
for name in model_names:
ext = _model_ext(name)
data = None
try:
data = _decode(zf.read(name))
except (KeyError, json.JSONDecodeError):
continue
except (KeyError, json.JSONDecodeError, UnicodeDecodeError):
data = None
bone_textures = {}
for bone, tex in (data.get('afw_bone_textures') or {}).items():
member = _resource_to_member(str(tex))
if member in names:
bone_textures[str(bone)] = member
bone_count = 0
cube_count = 0
identifier = ''
for geom in data.get('minecraft:geometry') or []:
identifier = identifier or (geom.get('description') or {}).get('identifier', '')
for bone in geom.get('bones') or []:
bone_count += 1
cube_count += len(bone.get('cubes') or [])
if data and isinstance(data, dict):
for bone, tex in (data.get('afw_bone_textures') or {}).items():
member = _resource_to_member(str(tex))
if member in names:
bone_textures[str(bone)] = member
if ext == '.geo.json':
for geom in data.get('minecraft:geometry') or []:
identifier = identifier or (geom.get('description') or {}).get('identifier', '')
for bone in geom.get('bones') or []:
bone_count += 1
cube_count += len(bone.get('cubes') or [])
elif ext == '.jem':
bone_count = len(data.get('models') 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.
entity = _entity_name(name)
default_texture = ''
if not bone_textures and entity:
for n in names:
@@ -295,11 +306,14 @@ def read_models_manifest(path):
models.append({
'name': name.rsplit('/', 1)[-1],
'member': name,
'ext': ext,
'mode': 'pack',
'identifier': identifier,
'entity': entity,
'default_texture': default_texture,
'bones': bone_count,
'cubes': cube_count,
'has_embedded_texture': ext in ('.bbmodel', '.gltf', '.glb'),
'bone_textures': bone_textures,
'textures': sorted(set(bone_textures.values())),
})
@@ -310,6 +324,29 @@ def read_models_manifest(path):
zf.close()
def _model_ext(name):
"""The matching MODEL_FILE_EXTENSIONS suffix for a member, or ''."""
lower = name.lower()
for ext in MODEL_FILE_EXTENSIONS:
if lower.endswith(ext):
return ext
return ''
def _entity_name(name):
"""Derive the entity name from a model filename (strip ext + gender suffix)."""
base = name.rsplit('/', 1)[-1]
for ext in MODEL_FILE_EXTENSIONS:
if base.lower().endswith(ext):
base = base[: -len(ext)]
break
for suffix in ('.mf', '.fm', '.m', '.f', '.g'):
if base.endswith(suffix):
base = base[:-len(suffix)]
break
return base
def _item_name_from_member(member):
parts = member.split('/')
try: