From be8c1a9ea86d680dc854e1c423d131e097943821 Mon Sep 17 00:00:00 2001 From: JakeBreath Date: Tue, 4 Aug 2026 21:47:37 -0500 Subject: [PATCH] working model cat backup --- nonpacks/library/views.py | 4 +- nonpacks/library/zips.py | 99 +++++++++++++------ nonpacks/static/css/style.css | 31 ++++++ nonpacks/static/js/packs_preview.js | 45 ++++++--- .../blockbench/plugins/packs_bootstrap.js | 13 +++ .../templates/library/project_detail.html | 11 ++- 6 files changed, 156 insertions(+), 47 deletions(-) diff --git a/nonpacks/library/views.py b/nonpacks/library/views.py index cb47643..94260ba 100644 --- a/nonpacks/library/views.py +++ b/nonpacks/library/views.py @@ -636,7 +636,7 @@ def _reparse_version(version): else: version.animation_id = '' version.animation_manifest = read_animation_manifest(path) or {} - version.models_manifest = read_models_manifest(path) or {} + version.models_manifest = read_models_manifest(path, version.project.category) or {} version.logical_manifest = read_logical_manifest(path) or {} manifest = parse_mods_manifest(path, release.file.original_filename) version.mods_manifest = manifest if manifest.get('files') else {} @@ -694,7 +694,7 @@ def _finalize_version(project, version_name, changelog, temp_uploads, actor): if animation_manifest is None: animation_manifest = read_animation_manifest(path) if models_manifest is None: - models_manifest = read_models_manifest(path) + models_manifest = read_models_manifest(path, project.category) if logical_manifest is None: logical_manifest = read_logical_manifest(path) if not mods_manifest: diff --git a/nonpacks/library/zips.py b/nonpacks/library/zips.py index 8239e57..0c6c6c5 100644 --- a/nonpacks/library/zips.py +++ b/nonpacks/library/zips.py @@ -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: diff --git a/nonpacks/static/css/style.css b/nonpacks/static/css/style.css index c682624..c04dea9 100644 --- a/nonpacks/static/css/style.css +++ b/nonpacks/static/css/style.css @@ -3401,3 +3401,34 @@ a.deletelink { .mods-badge.server { background: #e6e0ff; color: #3a2d8f; } .mods-link { color: inherit; text-decoration: none; opacity: 0.6; } .mods-link:hover { opacity: 1; } + +/* Model category format badges */ +.model-format-badge { + display: inline-block; + border-radius: 999px; + background: #eef2ff; + color: #3730a3; + padding: 0.1rem 0.5rem; + font-size: 0.7rem; + font-weight: 600; + margin-right: 0.35rem; +} +.model-tex-badge.embedded { + display: inline-block; + border-radius: 999px; + background: #d1fae5; + color: #065f46; + padding: 0.1rem 0.5rem; + font-size: 0.7rem; + font-weight: 600; + margin-right: 0.35rem; +} +.model-incompatible { + display: inline-block; + border-radius: 999px; + background: #fee2e2; + color: #991b1b; + padding: 0.15rem 0.6rem; + font-size: 0.72rem; + font-weight: 600; +} diff --git a/nonpacks/static/js/packs_preview.js b/nonpacks/static/js/packs_preview.js index 3cae9ce..4743ee4 100644 --- a/nonpacks/static/js/packs_preview.js +++ b/nonpacks/static/js/packs_preview.js @@ -83,6 +83,17 @@ return r.json(); } + async function fetchModelText(member, baseUrl) { + const r = await fetch(assetUrl(baseUrl, member), { headers: { 'Accept': 'application/json' } }); + if (!r.ok) throw new Error('Could not load the model file'); + return r.text(); + } + + // Formats whose textures are baked into the file (nothing to resolve). + // glTF/GLB are NOT here — import needs the desktop-only glTF Importer + // plugin, so those models are marked "Incompatible" instead. + const EMBEDDED_FORMATS = ['.bbmodel']; + async function fetchVanillaIndex(vanillaBaseUrl) { try { const r = await fetch(assetUrl(vanillaBaseUrl, 'entity_index.json')); @@ -165,6 +176,7 @@ async function open(btn) { const member = btn.dataset.member; const name = btn.dataset.name || member; + const modelFormat = btn.dataset.modelFormat || '.geo.json'; const opts = { baseUrl: btn.dataset.baseUrl, vanillaBaseUrl: btn.dataset.vanillaBaseUrl, defaultTexture: btn.dataset.defaultTexture || null }; if (!member || !opts.baseUrl || busy) return; busy = true; @@ -205,19 +217,26 @@ setStatus('Loading model…'); try { - const [geo, index] = await Promise.all([ - fetchGeo(member, opts.baseUrl), - fetchVanillaIndex(opts.vanillaBaseUrl), - ]); - const entity = entityNameFromMember(member); - const textures = await resolveTextures(geo, entity, opts, index); - iframe.contentWindow.postMessage({ - type: 'packs-open-model', - geo: geo, - name: name, - textures: textures, - }, window.location.origin); - setStatus('Applying textures…'); + const msg = { type: 'packs-open-model', name: name }; + if (modelFormat === '.gltf' || modelFormat === '.glb') { + setStatus('Incompatible — glTF/GLB preview needs the desktop glTF Importer plugin.'); + busy = false; + return; + } + if (EMBEDDED_FORMATS.indexOf(modelFormat) !== -1) { + // Textures baked into the file — hand it to Blockbench as-is. + msg.modelFile = { format: modelFormat, content: await fetchModelText(member, opts.baseUrl) }; + } else { + const [geo, index] = await Promise.all([ + fetchGeo(member, opts.baseUrl), + fetchVanillaIndex(opts.vanillaBaseUrl), + ]); + const entity = entityNameFromMember(member); + msg.geo = geo; + msg.textures = await resolveTextures(geo, entity, opts, index); + } + iframe.contentWindow.postMessage(msg, window.location.origin); + setStatus('Applying…'); } catch (e) { setStatus('Could not load model: ' + e.message); } diff --git a/nonpacks/static/vendor/blockbench/plugins/packs_bootstrap.js b/nonpacks/static/vendor/blockbench/plugins/packs_bootstrap.js index 88b5d4c..0c21bdc 100644 --- a/nonpacks/static/vendor/blockbench/plugins/packs_bootstrap.js +++ b/nonpacks/static/vendor/blockbench/plugins/packs_bootstrap.js @@ -62,6 +62,19 @@ async function openModel(msg) { await waitFor(() => typeof loadModelFile === 'function', 15000); const name = msg.name || 'model'; + + if (msg.modelFile) { + // Standalone Blockbench model (bbmodel) with baked textures. + // glTF/GLB is not sent here — those are marked "Incompatible" by the + // parent page (import needs the desktop-only glTF Importer plugin). + if (msg.modelFile.content) { + await loadModelFile({ content: msg.modelFile.content, name: name, path: name }); + } + await new Promise((r) => setTimeout(r, 400)); + notify('model-open', { name: name }); + return; + } + await loadModelFile({ content: JSON.stringify(msg.geo), name: name, path: name }); // Give the format importer a beat, then set the feature-bone overrides // (before adding textures, so the plugin's add_texture listener applies diff --git a/nonpacks/templates/library/project_detail.html b/nonpacks/templates/library/project_detail.html index 3d5bde1..5a77396 100644 --- a/nonpacks/templates/library/project_detail.html +++ b/nonpacks/templates/library/project_detail.html @@ -245,13 +245,22 @@
{{ model.name }} + {% if model.ext == '.bbmodel' or model.ext == '.geo.json' or model.ext == '.jem' %} + {% else %} + Incompatible + {% endif %}
-

{{ model.bones }} bones · {{ model.cubes }} cubes

+

+ {% if model.ext %}{{ model.ext }}{% endif %} + {% if model.has_embedded_texture %}textures embedded{% endif %} + {{ model.bones }} bones · {{ model.cubes }} cubes +

{% for member in model.textures %} {{ member }}