diff --git a/nonpacks/library/tests.py b/nonpacks/library/tests.py index 5d4f75e..e39779c 100644 --- a/nonpacks/library/tests.py +++ b/nonpacks/library/tests.py @@ -309,6 +309,30 @@ class UGCProjectDraftTests(UGCMediaTestCase, UGCGatedTestCase): self.assertFalse(ProjectDraft.objects.filter(uuid=uuid).exists()) self.assertFalse(TempUpload.objects.filter(draft_uuid=uuid).exists()) + def test_skin_individual_png_builds_manifest(self): + uuid, _ = self._new_draft() + self._auth(self.alice) + self._upload_temp('version', 'skin.png', make_png_bytes(size=(64, 64)), 'image/png', draft_uuid=uuid) + self.client.post(reverse('library:project_create', args=[uuid]), { + 'title': 'My Skin', 'category': 'skin', 'version_name': '1.0.0', + }) + version = Project.objects.get(slug='my-skin').versions.first() + manifest = version.skins_manifest + self.assertIn('skins', manifest) + self.assertEqual(manifest['skins'][0]['width'], 64) + self.assertEqual(manifest['skins'][0]['height'], 64) + self.assertTrue(manifest['skins'][0]['url'].startswith('/api/files/')) + + def test_skin_rejects_non_skin_sized_png(self): + uuid, _ = self._new_draft() + self._auth(self.alice) + self._upload_temp('version', 'small.png', make_png_bytes(size=(8, 8)), 'image/png', draft_uuid=uuid) + self.client.post(reverse('library:project_create', args=[uuid]), { + 'title': 'Bad Skin', 'category': 'skin', 'version_name': '1.0.0', + }) + version = Project.objects.get(slug='bad-skin').versions.first() + self.assertEqual(version.skins_manifest, {}) + class UGCVersionTests(UGCMediaTestCase, UGCGatedTestCase): def setUp(self): diff --git a/nonpacks/library/views.py b/nonpacks/library/views.py index 3169feb..e1723d6 100644 --- a/nonpacks/library/views.py +++ b/nonpacks/library/views.py @@ -50,6 +50,8 @@ from .zips import ( read_plugins_manifest, read_skins_manifest, _mc_from_filename, + _png_size, + SKIN_SIZES, ) @@ -391,6 +393,14 @@ def project_detail(request, slug): mods_manifest = latest.mods_manifest or None plugins_manifest = latest.plugins_manifest or None skins_manifest = latest.skins_manifest or None + if skins_manifest and latest.pk: + # Older manifests lack a per-skin url; fall back to the zip member. + for skin in skins_manifest.get('skins', []): + if not skin.get('url') and skin.get('member'): + skin['url'] = reverse( + 'library:pack_asset', + args=[project.slug, latest.pk, skin['member']], + ) mod_manifest = latest.mod_manifest or None if animation_manifest: anims = animation_manifest.get('animations') or [] @@ -981,22 +991,55 @@ def _animation_meta_for_upload(request, project): } +def _build_skins_manifest(version, release_path=None): + """Skins for a version: members of the release zip plus individually + uploaded PNG files. Each entry carries a ready-to-use ``url``.""" + skins = [] + if release_path is not None: + manifest = read_skins_manifest(release_path) + if manifest: + for skin in manifest.get('skins', []): + skin['url'] = reverse( + 'library:pack_asset', + args=[version.project.slug, version.pk, skin['member']], + ) + skins.append(skin) + for vf in version.files.filter(kind='png').select_related('file'): + try: + with open(_stored_path(vf.file), 'rb') as fh: + head = fh.read(24) + except OSError: + continue + size = _png_size(head) + if size and size in SKIN_SIZES: + skins.append({ + 'name': vf.filename, + 'member': str(vf.file.uuid), + 'width': size[0], + 'height': size[1], + 'url': reverse('library:file_request', args=[vf.file.uuid]), + }) + if not skins: + return {} + return {'skins': skins} + + def _reparse_version(version): """Re-derive a version's metadata (id + manifests) from its release zip.""" release = version.files.filter(kind='release').first() - if release is None: + path = _stored_path(release.file) if release is not None else None + if path is None and version.project.category != 'skin': return - path = _stored_path(release.file) - meta = read_pack_meta(path) + meta = read_pack_meta(path) if path else None if meta and meta.get('animation_id'): version.animation_id = meta['animation_id'] else: version.animation_id = '' - version.animation_manifest = read_animation_manifest(path) or {} - version.models_manifest = read_models_manifest(path, version.project.category) or {} - version.logical_manifest = read_logical_manifest(path) or {} - version.plugins_manifest = read_plugins_manifest(path) or {} if version.project.category == 'model' else {} - version.skins_manifest = read_skins_manifest(path) or {} if version.project.category == 'skin' else {} + version.animation_manifest = read_animation_manifest(path) or {} if path else {} + version.models_manifest = read_models_manifest(path, version.project.category) or {} if path else {} + version.logical_manifest = read_logical_manifest(path) or {} if path else {} + version.plugins_manifest = read_plugins_manifest(path) or {} if path and version.project.category == 'model' else {} + version.skins_manifest = _build_skins_manifest(version, path) if version.project.category == 'skin' else {} if version.project.category == 'mod': entries = [ (vf.filename, _stored_path(vf.file)) @@ -1006,12 +1049,15 @@ def _reparse_version(version): version.mod_manifest = read_mod_version_manifest(entries) else: version.mod_manifest = {} - manifest = parse_mods_manifest(path, release.file.original_filename) - version.mods_manifest = manifest if manifest.get('files') else {} - fmt, desc = read_pack_mcmeta(path) - if fmt is not None: - version.pack_format = fmt - version.pack_description = desc + if release is not None: + manifest = parse_mods_manifest(path, release.file.original_filename) + version.mods_manifest = manifest if manifest.get('files') else {} + fmt, desc = read_pack_mcmeta(path) + if fmt is not None: + version.pack_format = fmt + version.pack_description = desc + else: + version.mods_manifest = {} version.save() _sync_auto_tags(version.project, version.project.owner, version.project.versions.first()) @@ -1080,8 +1126,14 @@ def _finalize_version(project, version_name, changelog, temp_uploads, actor): mods_manifest = manifest if plugins_manifest is None and project.category == 'model': plugins_manifest = read_plugins_manifest(path) - if skins_manifest is None and project.category == 'skin': - skins_manifest = read_skins_manifest(path) + + # Skins come from the release zip members and/or individually uploaded PNGs. + if project.category == 'skin': + release = version.files.filter(kind='release').first() + release_path = None + if release is not None and (release.filename or '').lower().endswith('.zip'): + release_path = _stored_path(release.file) + skins_manifest = _build_skins_manifest(version, release_path) update_fields = [] if pack_format is not None: @@ -1094,7 +1146,7 @@ def _finalize_version(project, version_name, changelog, temp_uploads, actor): if plugins_manifest is not None: version.plugins_manifest = plugins_manifest update_fields.append('plugins_manifest') - if skins_manifest is not None: + if skins_manifest: version.skins_manifest = skins_manifest update_fields.append('skins_manifest') if project.category == 'mod' and mod_file_entries: diff --git a/nonpacks/static/js/packs_preview.js b/nonpacks/static/js/packs_preview.js index 3958ed6..20499a4 100644 --- a/nonpacks/static/js/packs_preview.js +++ b/nonpacks/static/js/packs_preview.js @@ -258,10 +258,9 @@ // Load a Minecraft skin into Blockbench's built-in "Minecraft Skin" project. async function openSkin(btn) { - const member = btn.dataset.member; - const name = btn.dataset.name || member; - const opts = { baseUrl: btn.dataset.baseUrl }; - if (!member || !opts.baseUrl || busy) return; + const url = btn.dataset.url; + const name = btn.dataset.name || 'skin'; + if (!url || busy) return; busy = true; if (!(await prepareOverlay(name))) { busy = false; return; } @@ -270,7 +269,7 @@ try { const select = btn.closest('.skin-card')?.querySelector('.skin-model-select'); const model = select ? select.value : 'steve'; - const img = await loadImage(assetUrl(opts.baseUrl, member)); + const img = await loadImage(url); iframe.contentWindow.postMessage({ type: 'packs-open-skin', name: name, diff --git a/nonpacks/templates/library/project_detail.html b/nonpacks/templates/library/project_detail.html index f74fbdc..83a48c7 100644 --- a/nonpacks/templates/library/project_detail.html +++ b/nonpacks/templates/library/project_detail.html @@ -436,7 +436,7 @@