added ModPacks support
This commit is contained in:
+114
-22
@@ -506,17 +506,69 @@ def read_animation_manifest(path):
|
||||
zf.close()
|
||||
|
||||
|
||||
def _normalize_modloader(loader_id):
|
||||
"""'fabricloader-0.16.9' → 'fabric', 'neoforge-21.1.66' → 'neoforge'."""
|
||||
low = str(loader_id or '').lower().strip()
|
||||
if not low:
|
||||
return ''
|
||||
for prefix, name in (
|
||||
('fabricloader', 'fabric'),
|
||||
('neoforge', 'neoforge'),
|
||||
('quilt', 'quilt'),
|
||||
('liteloader', 'liteloader'),
|
||||
('forge', 'forge'),
|
||||
('fml', 'forge'),
|
||||
('rift', 'rift'),
|
||||
('vanilla', 'vanilla'),
|
||||
):
|
||||
if low.startswith(prefix):
|
||||
return name
|
||||
return low.split('-')[0]
|
||||
|
||||
|
||||
def _mod_env(env):
|
||||
"""Normalize an mrpack env object ({client, server}) to a label."""
|
||||
client = str((env or {}).get('client') or '')
|
||||
server = str((env or {}).get('server') or '')
|
||||
if 'optional' in (client, server):
|
||||
return 'optional'
|
||||
if client == 'required' and server == 'unsupported':
|
||||
return 'client'
|
||||
if server == 'required' and client == 'unsupported':
|
||||
return 'server'
|
||||
return ''
|
||||
|
||||
|
||||
def _modpack_totals(files):
|
||||
total = len(files)
|
||||
if any('required' in f for f in files):
|
||||
required = sum(1 for f in files if f.get('required'))
|
||||
optional = total - required
|
||||
elif any('env' in f for f in files):
|
||||
optional = sum(1 for f in files if f.get('env') == 'optional')
|
||||
required = total - optional
|
||||
else:
|
||||
required = optional = None
|
||||
return {
|
||||
'total': total,
|
||||
'required': required,
|
||||
'optional': optional,
|
||||
'client': sum(1 for f in files if f.get('env') == 'client'),
|
||||
'server': sum(1 for f in files if f.get('env') == 'server'),
|
||||
}
|
||||
|
||||
|
||||
def parse_mods_manifest(path, filename=''):
|
||||
"""Inspect a modpack archive and build a manifest for the Mods tab.
|
||||
|
||||
- Modrinth .mrpack: modrinth.index.json → dependencies
|
||||
- Curseforge modpack: manifest.json → files array
|
||||
- Classic folder layout: minecraft/mods/*.jar
|
||||
Returns a dict: {source, files: [...]}.
|
||||
- Modrinth .mrpack: modrinth.index.json → per-file paths + env
|
||||
- Curseforge modpack: manifest.json → MC version, loaders, file list
|
||||
- Full instance / folder: mods under mods/, minecraft/mods/, .minecraft/mods/
|
||||
Returns a dict: {source, minecraft, modloaders, files, totals}.
|
||||
"""
|
||||
zf = _open_zip(path)
|
||||
if zf is None:
|
||||
return {'source': 'unknown', 'files': []}
|
||||
return _modpack_result('unknown', '', [], [])
|
||||
|
||||
try:
|
||||
names = set(zf.namelist())
|
||||
@@ -526,38 +578,78 @@ def parse_mods_manifest(path, filename=''):
|
||||
index = json.loads(zf.read('modrinth.index.json').decode('utf-8', 'replace'))
|
||||
except (KeyError, json.JSONDecodeError):
|
||||
index = {}
|
||||
deps = []
|
||||
for dep in index.get('dependencies') or []:
|
||||
deps.append({
|
||||
'name': dep.get('file_name') or dep.get('project_id', 'unknown'),
|
||||
'version_id': dep.get('version_id'),
|
||||
'type': dep.get('dependency_type', ''),
|
||||
files = []
|
||||
for entry in index.get('files') or []:
|
||||
path_name = str(entry.get('path') or '').replace('\\', '/')
|
||||
if not path_name:
|
||||
continue
|
||||
files.append({
|
||||
'name': path_name.rsplit('/', 1)[-1],
|
||||
'file_name': path_name,
|
||||
'env': _mod_env(entry.get('env')),
|
||||
})
|
||||
return {'source': 'modrinth', 'files': deps}
|
||||
return _modpack_result('modrinth', '', [], files)
|
||||
|
||||
if 'manifest.json' in names:
|
||||
try:
|
||||
manifest = json.loads(zf.read('manifest.json').decode('utf-8', 'replace'))
|
||||
except (KeyError, json.JSONDecodeError):
|
||||
manifest = {}
|
||||
mc = manifest.get('minecraft') or {}
|
||||
minecraft = str(mc.get('version') or '')
|
||||
modloaders = []
|
||||
for loader in mc.get('modLoaders') or []:
|
||||
norm = _normalize_modloader(loader.get('id'))
|
||||
if norm and norm not in modloaders:
|
||||
modloaders.append(norm)
|
||||
files = []
|
||||
for entry in manifest.get('files') or []:
|
||||
file_name = str(entry.get('fileName') or '')
|
||||
slug = str(entry.get('projectSlug') or '')
|
||||
project_id = entry.get('projectID')
|
||||
pid = str(project_id) if project_id else ''
|
||||
name = slug or file_name or (f'Mod #{pid}' if pid else 'unknown')
|
||||
files.append({
|
||||
'name': entry.get('fileName', entry.get('projectID', 'unknown')),
|
||||
'project_id': entry.get('projectID'),
|
||||
'file_id': entry.get('fileID'),
|
||||
'name': name,
|
||||
'file_name': file_name,
|
||||
'required': bool(entry.get('required', True)),
|
||||
'slug': slug,
|
||||
'project_id': pid,
|
||||
'url': f'https://www.curseforge.com/projects/{pid}' if pid else '',
|
||||
})
|
||||
if files:
|
||||
return {'source': 'curseforge', 'files': files}
|
||||
if files or minecraft or modloaders:
|
||||
return _modpack_result('curseforge', minecraft, modloaders, files)
|
||||
|
||||
# Full instance / folder: mod jars under a mods/ dir at any nesting
|
||||
# (minecraft/mods, .minecraft/mods, <instance>/minecraft/mods, mods).
|
||||
mods = []
|
||||
prefix = 'minecraft/mods/'
|
||||
seen = set()
|
||||
is_instance = False
|
||||
for name in sorted(names):
|
||||
if name.startswith(prefix) and not name.endswith('/'):
|
||||
mods.append({'name': name[len(prefix):]})
|
||||
if name.endswith('/') or not name.lower().endswith('.jar'):
|
||||
continue
|
||||
parts = name.split('/')
|
||||
if 'mods' not in parts or parts.index('mods') >= len(parts) - 1:
|
||||
continue
|
||||
if 'minecraft' in parts or '.minecraft' in parts:
|
||||
is_instance = True
|
||||
fname = parts[-1]
|
||||
if fname and fname not in seen:
|
||||
seen.add(fname)
|
||||
mods.append({'name': fname, 'file_name': name})
|
||||
if mods:
|
||||
return {'source': 'folder', 'files': mods}
|
||||
return _modpack_result('instance' if is_instance else 'folder', '', [], mods)
|
||||
|
||||
return {'source': 'unknown', 'files': []}
|
||||
return _modpack_result('unknown', '', [], [])
|
||||
finally:
|
||||
zf.close()
|
||||
|
||||
|
||||
def _modpack_result(source, minecraft, modloaders, files):
|
||||
return {
|
||||
'source': source,
|
||||
'minecraft': minecraft,
|
||||
'modloaders': modloaders,
|
||||
'files': files,
|
||||
'totals': _modpack_totals(files),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user