docs: 为源码添加完整中文注释
- 文件头说明项目功能与用法 - 每个类前注释模块职责 - 每个方法添加docstring说明用途 - 方便开源社区开发者理解和使用
This commit is contained in:
1 parent
e4cc14cb9e
commit
b6e67db02f
2 files changed
+75
No files matched your search
+70
@@ -1,6 +1,20 @@
|
||||
|
||||
|
||||
|
||||
"""
|
||||
MeowLauncher - 跨平台 Minecraft 启动器 (图形界面版)
|
||||
|
||||
功能特性:
|
||||
- 图形界面 (Tkinter), Windows / Linux / macOS 三端界面一致
|
||||
- 离线账号与第三方 Yggdrasil 登录 (LittleSkin 等 authlib-injector 皮肤站)
|
||||
- 资源中心: 启动器内一键下载游戏版本 / Mod / 插件 (Modrinth 开放 API)
|
||||
- 自动下载依赖库 / 原生库 / 资源文件, SHA1 校验 + 断点续传
|
||||
- 保留命令行模式: python launcher.py --cli
|
||||
|
||||
用法:
|
||||
python launcher.py 启动图形界面
|
||||
python launcher.py --cli ... 命令行模式
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
@@ -138,10 +152,16 @@ def library_allowed(lib: dict) -> bool:
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Modrinth 资源站客户端
|
||||
# 用于在启动器内搜索和下载 Mod / 插件
|
||||
# 使用官方开放 API (https://api.modrinth.com), 无需注册密钥
|
||||
# ---------------------------------------------------------------------------
|
||||
class ModrinthClient:
|
||||
BASE = "https://api.modrinth.com/v2"
|
||||
|
||||
def search(self, query="", game_versions=None, loaders=None, project_type=None, limit=20):
|
||||
"""搜索 Mod/插件项目。query 为关键词, game_versions 过滤MC版本, loaders 过滤加载器, project_type 为 mod 或 plugin。"""
|
||||
facets = []
|
||||
if game_versions:
|
||||
facets.append(["versions:%s" % game_versions])
|
||||
@@ -156,6 +176,7 @@ class ModrinthClient:
|
||||
return data.get("hits", [])
|
||||
|
||||
def project_versions(self, project_id, game_version=None, loader=None):
|
||||
"""获取某个项目适配指定 MC 版本与加载器的版本列表。"""
|
||||
url = "%s/project/%s/version" % (self.BASE, project_id)
|
||||
params = []
|
||||
if game_version:
|
||||
@@ -167,6 +188,7 @@ class ModrinthClient:
|
||||
return json.loads(http_get(url).decode("utf-8"))
|
||||
|
||||
def first_file(self, project_id, game_version, loader):
|
||||
"""获取第一个可下载的 jar 文件 (含下载地址/文件名/大小)。"""
|
||||
versions = self.project_versions(project_id, game_version, loader)
|
||||
for v in versions:
|
||||
for f in v.get("files", []):
|
||||
@@ -174,6 +196,10 @@ class ModrinthClient:
|
||||
return f
|
||||
return None
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 第三方登录 (Yggdrasil / authlib-injector 协议)
|
||||
# 支持 LittleSkin、Blessing Skin 等皮肤站, 登录获取正版角色信息
|
||||
# ---------------------------------------------------------------------------
|
||||
class YggdrasilAuth:
|
||||
|
||||
PRESETS = {
|
||||
@@ -183,9 +209,11 @@ class YggdrasilAuth:
|
||||
}
|
||||
|
||||
def __init__(self, server_url: str):
|
||||
"""初始化窗口、配色与资源中心状态。"""
|
||||
self.server_url = server_url.rstrip("/")
|
||||
|
||||
def authenticate(self, username: str, password: str) -> dict:
|
||||
"""登录第三方皮肤站, 返回 {access_token, uuid, name} 账号信息。"""
|
||||
client_token = str(uuid_mod.uuid4())
|
||||
payload = {
|
||||
"agent": {"name": "Minecraft", "version": 1},
|
||||
@@ -206,6 +234,7 @@ class YggdrasilAuth:
|
||||
}
|
||||
|
||||
def download_injector(self, game_dir: Path) -> Path:
|
||||
"""下载最新版 authlib-injector.jar, 启动游戏时注入用。"""
|
||||
lib_dir = game_dir / "libraries" / "authlib-injector"
|
||||
lib_dir.mkdir(parents=True, exist_ok=True)
|
||||
jar = lib_dir / "authlib-injector.jar"
|
||||
@@ -225,6 +254,10 @@ class YggdrasilAuth:
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 核心启动器
|
||||
# 负责: 版本解析 -> 文件下载 -> 启动参数构建 -> 拉起 Java 进程
|
||||
# ---------------------------------------------------------------------------
|
||||
class MinecraftLauncher:
|
||||
def __init__(self, game_dir, version_id, username="Steve", java_path=None,
|
||||
min_mem=1024, max_mem=2048, auth=None, yggdrasil=None,
|
||||
@@ -251,9 +284,11 @@ class MinecraftLauncher:
|
||||
|
||||
|
||||
def fetch_version_manifest(self) -> dict:
|
||||
"""拉取 Mojang 官方版本清单。"""
|
||||
return json.loads(http_get(VERSION_MANIFEST_URL).decode("utf-8"))
|
||||
|
||||
def resolve_version(self) -> None:
|
||||
"""把版本号解析成版本元数据, 支持 latest-release / latest-snapshot。"""
|
||||
manifest = self.fetch_version_manifest()
|
||||
latest = manifest.get("latest", {})
|
||||
versions = manifest.get("versions", [])
|
||||
@@ -276,11 +311,13 @@ class MinecraftLauncher:
|
||||
|
||||
|
||||
def _emit(self, msg: str) -> None:
|
||||
"""输出日志: 同时打印到控制台和回调 GUI。"""
|
||||
log(msg)
|
||||
if self.progress_cb:
|
||||
self.progress_cb(msg)
|
||||
|
||||
def download_client(self) -> Path:
|
||||
"""下载客户端 jar 文件到 versions/<id>/。"""
|
||||
vdir = self.versions_dir / self.version_id
|
||||
vdir.mkdir(parents=True, exist_ok=True)
|
||||
jar = vdir / ("%s.jar" % self.version_id)
|
||||
@@ -293,6 +330,7 @@ class MinecraftLauncher:
|
||||
return jar
|
||||
|
||||
def download_libraries(self) -> list:
|
||||
"""下载全部依赖库与原生库, 返回 classpath 列表。"""
|
||||
cp = []
|
||||
total = sum(1 for lib in self.version_meta.get("libraries", []) if library_allowed(lib))
|
||||
done = 0
|
||||
@@ -322,12 +360,14 @@ class MinecraftLauncher:
|
||||
return cp
|
||||
|
||||
def _download_one(self, artifact, path, tag, done, total):
|
||||
"""下载单个库文件, 失败只警告不中断。"""
|
||||
try:
|
||||
download_file(artifact["url"], path, artifact.get("sha1"), artifact.get("size"))
|
||||
except Exception as e:
|
||||
self._emit(" [警告] %s 下载失败: %s" % (path.name, e))
|
||||
|
||||
def _extract_natives(self, native_jar: Path) -> None:
|
||||
"""把原生库 (natives) 解压到 versions/<id>/natives 目录。"""
|
||||
natives_dir = self.versions_dir / self.version_id / "natives"
|
||||
natives_dir.mkdir(parents=True, exist_ok=True)
|
||||
ext = {"linux": ".so", "osx": ".dylib", "windows": ".dll"}[OS_NAME]
|
||||
@@ -340,6 +380,7 @@ class MinecraftLauncher:
|
||||
shutil.copyfileobj(src, dst)
|
||||
|
||||
def download_assets(self) -> None:
|
||||
"""下载资源索引与全部资源对象到 assets/。"""
|
||||
index_info = self.version_meta.get("assetIndex")
|
||||
if not index_info:
|
||||
self._emit("[资源] 该版本无资源索引, 跳过")
|
||||
@@ -368,6 +409,7 @@ class MinecraftLauncher:
|
||||
|
||||
|
||||
def _resolve_argument(self, arg: str) -> str:
|
||||
"""替换启动参数里的 ${变量} 占位符。"""
|
||||
uuid = self.auth.get("uuid") or self._offline_uuid()
|
||||
replace_map = {
|
||||
"${auth_player_name}": self.auth.get("name") or self.username,
|
||||
@@ -394,6 +436,7 @@ class MinecraftLauncher:
|
||||
return arg
|
||||
|
||||
def _offline_uuid(self) -> str:
|
||||
"""按官方离线模式规则, 由玩家名生成稳定 UUID。"""
|
||||
name = self.auth.get("name") or self.username
|
||||
digest = bytearray(hashlib.md5(("OfflinePlayer:" + name).encode("utf-8")).digest())
|
||||
digest[6] = (digest[6] & 0x0F) | 0x30
|
||||
@@ -403,6 +446,7 @@ class MinecraftLauncher:
|
||||
hex_str[16:20], hex_str[20:32])
|
||||
|
||||
def _build_args(self) -> tuple:
|
||||
"""拆分 JVM 参数与游戏参数, 兼容 1.13+ 与旧版两种格式。"""
|
||||
jvm_args, game_args = [], []
|
||||
arguments = self.version_meta.get("arguments")
|
||||
if arguments:
|
||||
@@ -424,6 +468,7 @@ class MinecraftLauncher:
|
||||
return jvm_args, game_args
|
||||
|
||||
def _find_java(self) -> str:
|
||||
"""查找 Java: 优先用户指定 -> JAVA_HOME -> PATH。"""
|
||||
if self.java_path and os.path.isfile(self.java_path):
|
||||
return self.java_path
|
||||
java_home = os.environ.get("JAVA_HOME")
|
||||
@@ -438,6 +483,7 @@ class MinecraftLauncher:
|
||||
raise RuntimeError("未找到 Java, 请在设置中指定 java 可执行文件路径 (1.17+ 需要 Java 17+)")
|
||||
|
||||
def build_command(self, classpath: list) -> list:
|
||||
"""组装完整 java 启动命令, 含第三方登录时注入 authlib-injector。"""
|
||||
self._cp = classpath
|
||||
java = self._find_java()
|
||||
jvm_args, game_args = self._build_args()
|
||||
@@ -461,6 +507,7 @@ class MinecraftLauncher:
|
||||
|
||||
|
||||
def run(self) -> int:
|
||||
"""主流程: 解析版本 -> 下载文件 -> 构建命令 -> 启动游戏。"""
|
||||
self._emit("[启动] 游戏目录: %s" % self.game_dir)
|
||||
self._emit("[启动] 版本: %s 玩家: %s" % (self.version_id,
|
||||
self.auth.get("name") or self.username))
|
||||
@@ -479,6 +526,7 @@ class MinecraftLauncher:
|
||||
return subprocess.call(command, cwd=str(self.game_dir))
|
||||
|
||||
def list_versions(self) -> dict:
|
||||
"""获取版本清单 (供 GUI 和 CLI 使用)。"""
|
||||
manifest = self.fetch_version_manifest()
|
||||
return {
|
||||
"latest": manifest.get("latest", {}),
|
||||
@@ -498,6 +546,10 @@ except Exception:
|
||||
TK_OK = False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 图形界面 (Tkinter)
|
||||
# 两个标签页: [启动器] 登录/版本/设置/日志, [资源中心] 版本与Mod插件下载
|
||||
# ---------------------------------------------------------------------------
|
||||
class MeowLauncherApp:
|
||||
|
||||
|
||||
@@ -529,6 +581,7 @@ class MeowLauncherApp:
|
||||
|
||||
|
||||
def _setup_style(self):
|
||||
"""配置 ttk 深色主题, 保证三端外观一致。"""
|
||||
style = ttk.Style(self.root)
|
||||
try:
|
||||
style.theme_use("clam")
|
||||
@@ -560,6 +613,7 @@ class MeowLauncherApp:
|
||||
|
||||
|
||||
def _card(self, parent, title):
|
||||
"""创建一个圆角卡片容器, 用于分组界面元素。"""
|
||||
frame = ttk.Frame(parent, style="Card.TFrame", padding=14)
|
||||
frame.pack(fill="x", padx=14, pady=(8, 4))
|
||||
ttk.Label(frame, text=title, style="Card.TLabel",
|
||||
@@ -567,6 +621,7 @@ class MeowLauncherApp:
|
||||
return frame
|
||||
|
||||
def _build_ui(self):
|
||||
"""构建主窗口: 标题 + 标签页(启动器/资源中心)。"""
|
||||
ttk.Label(self.root, text="MeowLauncher", style="Title.TLabel").pack(pady=(14, 0))
|
||||
ttk.Label(self.root, text="跨平台 Minecraft 启动器 v%s" % VERSION, style="Dim.TLabel").pack()
|
||||
|
||||
@@ -581,6 +636,7 @@ class MeowLauncherApp:
|
||||
self._build_res_tab()
|
||||
|
||||
def _build_launch_tab(self):
|
||||
"""构建[启动器]页: 登录、版本、设置、日志、启动按钮。"""
|
||||
login = self._card(self.tab_launch, "账号登录")
|
||||
self.login_mode = tk.StringVar(value="offline")
|
||||
row = ttk.Frame(login, style="Card.TFrame")
|
||||
@@ -659,6 +715,7 @@ class MeowLauncherApp:
|
||||
self.launch_btn.pack(fill="x", padx=14, pady=14)
|
||||
|
||||
def _build_res_tab(self):
|
||||
"""构建[资源中心]页: 版本一键下载 + Mod/插件搜索下载。"""
|
||||
dver = self._card(self.tab_res, "游戏版本下载")
|
||||
row = ttk.Frame(dver, style="Card.TFrame")
|
||||
row.pack(fill="x")
|
||||
@@ -705,12 +762,14 @@ class MeowLauncherApp:
|
||||
self.res_progress.pack(fill="x", padx=14, pady=(0, 10))
|
||||
|
||||
def _res_append(self, msg):
|
||||
"""向资源中心日志框追加一行。"""
|
||||
self.res_log.config(state="normal")
|
||||
self.res_log.insert("end", msg + "\n")
|
||||
self.res_log.see("end")
|
||||
self.res_log.config(state="disabled")
|
||||
|
||||
def _search_res(self):
|
||||
"""后台线程搜索 Modrinth, 结果回填列表。"""
|
||||
def worker():
|
||||
try:
|
||||
q = self.res_query.get().strip()
|
||||
@@ -725,6 +784,7 @@ class MeowLauncherApp:
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _download_version_only(self):
|
||||
"""一键下载完整游戏版本 (不启动)。"""
|
||||
def worker():
|
||||
try:
|
||||
version = self.res_version_var.get()
|
||||
@@ -744,6 +804,7 @@ class MeowLauncherApp:
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _download_selected(self):
|
||||
"""下载列表中选中的 Mod/插件 jar 到 mods/ 或 plugins/ 目录。"""
|
||||
sel = self.res_tree.selection()
|
||||
if not sel:
|
||||
return
|
||||
@@ -774,6 +835,7 @@ class MeowLauncherApp:
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _toggle_login(self):
|
||||
"""切换离线/第三方登录表单的显示。"""
|
||||
if self.login_mode.get() == "offline":
|
||||
self.offline_frame.pack(fill="x")
|
||||
self.ygg_frame.pack_forget()
|
||||
@@ -782,16 +844,19 @@ class MeowLauncherApp:
|
||||
self.ygg_frame.pack(fill="x")
|
||||
|
||||
def _pick_java(self):
|
||||
"""弹出文件选择框, 选择 java 可执行文件。"""
|
||||
path = filedialog.askopenfilename(title="选择 java 可执行文件")
|
||||
if path:
|
||||
self.java_var.set(path)
|
||||
|
||||
def _pick_dir(self):
|
||||
"""弹出目录选择框, 选择游戏目录。"""
|
||||
path = filedialog.askdirectory(title="选择游戏目录")
|
||||
if path:
|
||||
self.dir_var.set(path)
|
||||
|
||||
def refresh_versions(self):
|
||||
"""后台线程拉取版本清单, 填充两个标签页的版本下拉框。"""
|
||||
def worker():
|
||||
try:
|
||||
data = MinecraftLauncher(
|
||||
@@ -803,6 +868,7 @@ class MeowLauncherApp:
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _poll_queue(self):
|
||||
"""定时轮询消息队列, 把后台线程的日志/结果刷到界面。"""
|
||||
try:
|
||||
while True:
|
||||
kind, payload = self.msg_queue.get_nowait()
|
||||
@@ -832,6 +898,7 @@ class MeowLauncherApp:
|
||||
self.root.after(100, self._poll_queue)
|
||||
|
||||
def _fill_versions(self, latest):
|
||||
"""根据快照开关过滤并填充版本下拉框。"""
|
||||
show_snap = self.show_snapshot.get()
|
||||
items = [v["id"] for v in self.versions_cache
|
||||
if show_snap or v["type"] == "release"]
|
||||
@@ -842,16 +909,19 @@ class MeowLauncherApp:
|
||||
self.res_version_var.set(latest.get("release") or (items[0] if items else ""))
|
||||
|
||||
def _append_log(self, msg):
|
||||
"""向[启动器]页日志框追加一行。"""
|
||||
self.log_box.config(state="normal")
|
||||
self.log_box.insert("end", msg + "\n")
|
||||
self.log_box.see("end")
|
||||
self.log_box.config(state="disabled")
|
||||
|
||||
def _on_launch(self):
|
||||
"""点击启动游戏: 禁用按钮并开后台线程。"""
|
||||
self.launch_btn.config(state="disabled")
|
||||
threading.Thread(target=self._launch_worker, daemon=True).start()
|
||||
|
||||
def _launch_worker(self):
|
||||
"""后台线程: 登录 -> 构建启动器 -> 下载 -> 启动游戏。"""
|
||||
try:
|
||||
if self.login_mode.get() == "yggdrasil":
|
||||
server_name = self.server_var.get()
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
|
||||
|
||||
|
||||
"""
|
||||
MeowLauncher 命令行版 (旧版备份)
|
||||
功能已被 launcher.py 的图形界面版完全覆盖, 保留此文件供命令行使用:
|
||||
python launcher_cli.py --username Steve --version 1.20.4
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
Reference in new issue
Block a user