full suport for models and hotloading Plugins

This commit is contained in:
JakeBreath
2026-08-04 22:28:39 -05:00
parent be8c1a9ea8
commit 552f570efb
9 changed files with 236 additions and 7 deletions
+68 -1
View File
@@ -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: