"""版本解析:从实例文件夹读出「该用哪个协议」。 设计依据(2026-09-25 实测): * 实例文件夹 = 一个完整的 MC 客户端安装(与官方启动器一致) * 版本信息在 versions//.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//.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//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)