151 lines
5.1 KiB
Python
151 lines
5.1 KiB
Python
"""版本解析:从实例文件夹读出「该用哪个协议」。
|
||
|
||
设计依据(2026-09-25 实测):
|
||
* 实例文件夹 = 一个完整的 MC 客户端安装(与官方启动器一致)
|
||
* 版本信息在 versions/<verId>/<verId>.json
|
||
* 加载器(forge/fabric)的 verId **不是**协议版本,必须经 inheritsFrom 归一化:
|
||
minecraft-data('1.20.1') -> OK
|
||
minecraft-data('1.20.1-forge-47.4.0') -> FAIL
|
||
minecraft-data('fabric-loader-0.19.5-1.20.1') -> FAIL
|
||
* mod 加载器会改写 mainClass,这决定「谁来渲染」(见 loaders 判定)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
from dataclasses import dataclass, field
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
# 已知加载器的 mainClass 特征(用于判定实例该按哪种加载器解析 mods/)
|
||
LOADER_MAINCLASS_HINTS: tuple[tuple[str, str], ...] = (
|
||
("forgewrapper", "forge"),
|
||
("forge", "forge"),
|
||
("neoforge", "neoforge"),
|
||
("fabric", "fabric"),
|
||
("quilt", "quilt"),
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class VersionInfo:
|
||
"""一个已安装版本的解析结果。"""
|
||
|
||
version_id: str # versions/ 下的目录名,如 1.20.1-forge-47.4.0
|
||
mc_version: str # 归一化后的真实 MC 版本,如 1.20.1
|
||
inherits_from: str | None # version.json 里的 inheritsFrom
|
||
main_class: str # 启动主类(加载器会改写它)
|
||
loader: str # vanilla / forge / neoforge / fabric / quilt
|
||
json_path: Path # version.json 路径
|
||
jar_path: Path | None # 客户端 jar(继承时可能在自己的目录里没有)
|
||
assets: str | None = None
|
||
libs_count: int = 0
|
||
raw: dict[str, Any] = field(default_factory=dict)
|
||
|
||
@property
|
||
def is_modded(self) -> bool:
|
||
return self.loader != "vanilla"
|
||
|
||
|
||
def _detect_loader(main_class: str, version_id: str, inherits_from: str | None) -> str:
|
||
"""从 mainClass / 版本 id 判定加载器类型。
|
||
|
||
注意:vanilla 的 version.json 里没有 inheritsFrom;加载器则一定有
|
||
(forge: inheritsFrom=1.20.1, id=1.20.1-forge-47.4.0)。
|
||
"""
|
||
lc = (main_class or "").lower()
|
||
for hint, name in LOADER_MAINCLASS_HINTS:
|
||
if hint in lc:
|
||
return name
|
||
# mainClass 不可辨识时退回看 version id(如 fabric-loader-0.19.5-1.20.1)
|
||
vid = version_id.lower()
|
||
if inherits_from:
|
||
for name in ("neoforge", "forge", "fabric", "quilt"):
|
||
if name in vid:
|
||
return name
|
||
return "unknown-modded"
|
||
return "vanilla"
|
||
|
||
|
||
def parse_version_json(path: Path) -> VersionInfo:
|
||
"""解析单个 versions/<id>/<id>.json。"""
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
|
||
version_id = data.get("id") or path.stem
|
||
inherits_from = data.get("inheritsFrom")
|
||
main_class = data.get("mainClass", "")
|
||
loader = _detect_loader(main_class, version_id, inherits_from)
|
||
|
||
# mc_version 归一化:有 inheritsFrom 就取它(这才是协议版本)
|
||
mc_version = inherits_from or version_id
|
||
|
||
jar = path.parent / f"{version_id}.jar"
|
||
return VersionInfo(
|
||
version_id=version_id,
|
||
mc_version=mc_version,
|
||
inherits_from=inherits_from,
|
||
main_class=main_class,
|
||
loader=loader,
|
||
json_path=path,
|
||
jar_path=jar if jar.is_file() else None,
|
||
assets=data.get("assets"),
|
||
libs_count=len(data.get("libraries", [])),
|
||
raw=data,
|
||
)
|
||
|
||
|
||
def list_versions(instance_dir: str | os.PathLike[str]) -> list[VersionInfo]:
|
||
"""列出实例文件夹里所有已安装版本(instances/<name>/versions/*/)。
|
||
|
||
跳过以 '.' 开头的目录,以及没有对应 json 的目录(半途安装的残留)。
|
||
"""
|
||
vroot = Path(instance_dir) / "versions"
|
||
if not vroot.is_dir():
|
||
return []
|
||
|
||
out: list[VersionInfo] = []
|
||
for d in sorted(vroot.iterdir()):
|
||
if not d.is_dir() or d.name.startswith("."):
|
||
continue
|
||
vj = d / f"{d.name}.json"
|
||
if vj.is_file():
|
||
out.append(parse_version_json(vj))
|
||
return out
|
||
|
||
|
||
def resolve_version(
|
||
instance_dir: str | os.PathLike[str],
|
||
prefer: str | None = None,
|
||
) -> VersionInfo | None:
|
||
"""从实例文件夹选出「该用哪个版本」。
|
||
|
||
prefer 指定优先的 version id(用户显式选择时用)。
|
||
否则:优先 vanilla(少一层加载器,最稳),再按目录名排序取第一个。
|
||
"""
|
||
versions = list_versions(instance_dir)
|
||
if not versions:
|
||
return None
|
||
|
||
if prefer:
|
||
for v in versions:
|
||
if v.version_id == prefer:
|
||
return v
|
||
return None
|
||
|
||
vanilla = [v for v in versions if v.loader == "vanilla"]
|
||
if vanilla:
|
||
# 有多个原版时取版本号最高的那个
|
||
return max(vanilla, key=lambda v: _verkey(v.mc_version))
|
||
return versions[0]
|
||
|
||
|
||
def _verkey(s: str) -> tuple[int, ...]:
|
||
"""把 '1.20.1' 变成可比较的元组;非数字段回退为 0。"""
|
||
parts: list[int] = []
|
||
for seg in s.split("."):
|
||
num = "".join(ch for ch in seg if ch.isdigit())
|
||
parts.append(int(num) if num else 0)
|
||
return tuple(parts)
|