full suport for models and hotloading Plugins
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.3 on 2026-08-05 02:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0008_version_logical_manifest_version_models_manifest'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='version',
|
||||
name='plugins_manifest',
|
||||
field=models.JSONField(blank=True, default=dict),
|
||||
),
|
||||
]
|
||||
@@ -209,6 +209,7 @@ class Version(models.Model):
|
||||
pack_format = models.IntegerField(null=True, blank=True)
|
||||
pack_description = models.TextField(blank=True, default='')
|
||||
mods_manifest = models.JSONField(default=dict, blank=True)
|
||||
plugins_manifest = models.JSONField(default=dict, blank=True)
|
||||
animation_id = models.CharField(max_length=128, blank=True, default='', db_index=True)
|
||||
animation_manifest = models.JSONField(default=dict, blank=True)
|
||||
models_manifest = models.JSONField(default=dict, blank=True)
|
||||
|
||||
@@ -1030,8 +1030,8 @@ class UGCZipParsingTests(TestCase):
|
||||
from library.zips import parse_mods_manifest
|
||||
path = self._write_zip(_zip_bytes({
|
||||
'modrinth.index.json': json.dumps({
|
||||
'dependencies': [
|
||||
{'project_id': 'abc', 'file_name': 'mod-a.jar', 'dependency_type': 'required'},
|
||||
'files': [
|
||||
{'path': 'mods/mod-a.jar', 'env': {'client': 'required', 'server': 'required'}},
|
||||
],
|
||||
}),
|
||||
}), suffix='.mrpack')
|
||||
|
||||
@@ -41,6 +41,7 @@ from .zips import (
|
||||
read_models_manifest,
|
||||
read_pack_meta,
|
||||
read_pack_mcmeta,
|
||||
read_plugins_manifest,
|
||||
)
|
||||
|
||||
|
||||
@@ -331,6 +332,7 @@ def project_detail(request, slug):
|
||||
models_manifest = None
|
||||
logical_manifest = None
|
||||
mods_manifest = None
|
||||
plugins_manifest = None
|
||||
anim_stats = None
|
||||
latest = versions.first()
|
||||
if project.category == 'guide' and latest is not None:
|
||||
@@ -340,6 +342,7 @@ def project_detail(request, slug):
|
||||
models_manifest = latest.models_manifest or None
|
||||
logical_manifest = latest.logical_manifest or None
|
||||
mods_manifest = latest.mods_manifest or None
|
||||
plugins_manifest = latest.plugins_manifest or None
|
||||
if animation_manifest:
|
||||
anims = animation_manifest.get('animations') or []
|
||||
entity_count = sum(1 for a in anims if a.get('type') == 'Entity x Player')
|
||||
@@ -353,6 +356,14 @@ def project_detail(request, slug):
|
||||
'tags': sorted({t for a in anims for t in (a.get('content_tags') or [])}),
|
||||
}
|
||||
|
||||
autoload_plugins_json = ''
|
||||
if plugins_manifest:
|
||||
members = [
|
||||
p['member'] for p in plugins_manifest.get('plugins', [])
|
||||
if p.get('autoload') and p.get('member')
|
||||
]
|
||||
autoload_plugins_json = json.dumps(members)
|
||||
|
||||
context = {
|
||||
'project': project,
|
||||
'versions': versions,
|
||||
@@ -364,6 +375,8 @@ def project_detail(request, slug):
|
||||
'models_manifest': models_manifest,
|
||||
'logical_manifest': logical_manifest,
|
||||
'mods_manifest': mods_manifest,
|
||||
'plugins_manifest': plugins_manifest,
|
||||
'autoload_plugins_json': autoload_plugins_json,
|
||||
'anim_stats': anim_stats,
|
||||
}
|
||||
return render(request, 'library/project_detail.html', context)
|
||||
@@ -638,6 +651,7 @@ def _reparse_version(version):
|
||||
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 {}
|
||||
manifest = parse_mods_manifest(path, release.file.original_filename)
|
||||
version.mods_manifest = manifest if manifest.get('files') else {}
|
||||
fmt, desc = read_pack_mcmeta(path)
|
||||
@@ -659,6 +673,7 @@ def _finalize_version(project, version_name, changelog, temp_uploads, actor):
|
||||
pack_format = None
|
||||
pack_description = ''
|
||||
mods_manifest = {}
|
||||
plugins_manifest = None
|
||||
animation_id = ''
|
||||
animation_manifest = None
|
||||
models_manifest = None
|
||||
@@ -701,6 +716,8 @@ def _finalize_version(project, version_name, changelog, temp_uploads, actor):
|
||||
manifest = parse_mods_manifest(path, index.original_filename)
|
||||
if manifest.get('files'):
|
||||
mods_manifest = manifest
|
||||
if plugins_manifest is None and project.category == 'model':
|
||||
plugins_manifest = read_plugins_manifest(path)
|
||||
|
||||
update_fields = []
|
||||
if pack_format is not None:
|
||||
@@ -710,6 +727,9 @@ def _finalize_version(project, version_name, changelog, temp_uploads, actor):
|
||||
if mods_manifest:
|
||||
version.mods_manifest = mods_manifest
|
||||
update_fields.append('mods_manifest')
|
||||
if plugins_manifest is not None:
|
||||
version.plugins_manifest = plugins_manifest
|
||||
update_fields.append('plugins_manifest')
|
||||
if animation_id:
|
||||
version.animation_id = animation_id
|
||||
update_fields.append('animation_id')
|
||||
|
||||
@@ -16,6 +16,71 @@ PROBLEM_TAGS = {'broken', 'bugged', 'borked'}
|
||||
MODEL_FILE_EXTENSIONS = ('.bbmodel', '.geo.json', '.jem', '.gltf', '.glb')
|
||||
|
||||
|
||||
def _in_plugins_dir(name):
|
||||
"""True when a member lives inside a plugins/ folder (any nesting)."""
|
||||
parts = name.split('/')
|
||||
return 'plugins' in parts[:-1]
|
||||
|
||||
|
||||
def read_plugins_manifest(path):
|
||||
"""Scan the archive's plugins/ folder for Blockbench plugins.
|
||||
|
||||
A ``.js`` file under a ``plugins/`` directory that registers a Blockbench
|
||||
plugin (contains ``Plugin.register(`` or ``BBPlugin.register(``) is listed.
|
||||
``plugins/autoload.txt`` holds one plugin name per line; matching plugins
|
||||
are marked ``autoload`` so the viewer can hotload them when a model opens.
|
||||
Returns None when the pack has no plugins.
|
||||
"""
|
||||
zf = _open_zip(path)
|
||||
if zf is None:
|
||||
return None
|
||||
try:
|
||||
names = set(zf.namelist())
|
||||
autoload = set()
|
||||
for n in names:
|
||||
if _in_plugins_dir(n) and n.endswith('autoload.txt'):
|
||||
try:
|
||||
raw = zf.read(n).decode('utf-8', 'replace')
|
||||
except KeyError:
|
||||
continue
|
||||
for line in raw.splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
autoload.add(line)
|
||||
|
||||
plugins = []
|
||||
for n in sorted(names):
|
||||
if not _in_plugins_dir(n) or not n.endswith('.js'):
|
||||
continue
|
||||
try:
|
||||
content = zf.read(n)
|
||||
except KeyError:
|
||||
continue
|
||||
if len(content) > 8 * 1024 * 1024:
|
||||
continue
|
||||
text = content.decode('utf-8', 'replace')
|
||||
if 'Plugin.register(' not in text and 'BBPlugin.register(' not in text:
|
||||
continue
|
||||
base = n.rsplit('/', 1)[-1]
|
||||
stem = base[:-3] if base.lower().endswith('.js') else base
|
||||
plugins.append({
|
||||
'name': base,
|
||||
'member': n,
|
||||
'size': len(content),
|
||||
'autoload': base in autoload or stem in autoload,
|
||||
})
|
||||
|
||||
if not plugins:
|
||||
return None
|
||||
return {
|
||||
'plugins': plugins,
|
||||
'has_plugins': True,
|
||||
'autoload_count': sum(1 for p in plugins if p['autoload']),
|
||||
}
|
||||
finally:
|
||||
zf.close()
|
||||
|
||||
|
||||
def _decode(data):
|
||||
"""Decode JSON text, tolerating a UTF-8 BOM (common in hand-edited packs)."""
|
||||
return json.loads(data.decode('utf-8-sig', 'replace'))
|
||||
@@ -668,7 +733,9 @@ def parse_mods_manifest(path, filename=''):
|
||||
parts = name.split('/')
|
||||
if 'mods' not in parts or parts.index('mods') >= len(parts) - 1:
|
||||
continue
|
||||
if 'minecraft' in parts or '.minecraft' in parts:
|
||||
# Full instance = .minecraft/mods or a nested <instance>/minecraft/mods;
|
||||
# a root-level minecraft/mods folder is the classic folder layout.
|
||||
if '.minecraft' in parts or (('minecraft' in parts) and parts.index('minecraft') > 0):
|
||||
is_instance = True
|
||||
fname = parts[-1]
|
||||
if fname and fname not in seen:
|
||||
|
||||
Reference in New Issue
Block a user