# -*- coding: utf-8 -*- """ Created on Thu Aug 21 21:55:21 2025 Modified to include GUI on Fri Sep 12 19:00:00 2025 @author: ljz @modifier: Qwen (Alibaba Cloud) """ import minecraft_launcher_lib import subprocess import os import uuid import shutil import requests import zipfile import tkinter as tk from tkinter import messagebox, scrolledtext, simpledialog import threading class LauncherGUI: def __init__(self, root): self.root = root self.root.title("DizzyCraftLauncher2 GUI") self.root.geometry("600x500") # --- 配置路径 --- self.launcher_directory = "C:\\DizzyCraftLauncher2" self.minecraft_directory = os.path.join(self.launcher_directory, ".minecraft") self.java_dir = os.path.join(self.launcher_directory, "jdk-21.0.8") self.java_path = os.path.join(self.java_dir, "bin", "java.exe") self.java_zip_path = os.path.join(self.launcher_directory, "jdk-21_windows-x64_bin.zip") self.java_download_url = "https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.zip" # --- 创建 GUI 元素 --- self.create_widgets() # --- 初始化 --- self.username = "" self.refresh_versions() def create_widgets(self): # 欢迎标签 self.label_welcome = tk.Label(self.root, text="欢迎使用 DizzyCraftLauncher2 GUI", font=("Arial", 14)) self.label_welcome.pack(pady=10) # 用户名输入 frame_username = tk.Frame(self.root) frame_username.pack(pady=5) tk.Label(frame_username, text="用户名:").pack(side=tk.LEFT) self.entry_username = tk.Entry(frame_username) self.entry_username.pack(side=tk.LEFT, padx=(5, 0)) # 默认用户名可以从系统获取,这里简化处理 self.entry_username.insert(0, os.getlogin()) # 版本列表 frame_versions = tk.Frame(self.root) frame_versions.pack(pady=5, fill=tk.BOTH, expand=True) tk.Label(frame_versions, text="已下载版本:").pack(anchor=tk.W) self.listbox_versions = tk.Listbox(frame_versions) self.listbox_versions.pack(fill=tk.BOTH, expand=True, pady=(0, 5)) self.listbox_versions.bind('<>', self.on_version_select) # 版本输入和按钮 frame_version_input = tk.Frame(self.root) frame_version_input.pack(pady=5) tk.Label(frame_version_input, text="版本:").pack(side=tk.LEFT) self.entry_version = tk.Entry(frame_version_input, width=20) self.entry_version.pack(side=tk.LEFT, padx=(5, 10)) self.button_launch = tk.Button(frame_version_input, text="启动游戏", command=self.launch_game) self.button_launch.pack(side=tk.LEFT, padx=(0, 5)) self.button_download_version = tk.Button(frame_version_input, text="下载版本", command=self.download_version_gui) self.button_download_version.pack(side=tk.LEFT) # 下载 Java 按钮 self.button_download_java = tk.Button(self.root, text="下载 Java (JDK 21)", command=self.start_download_java) self.button_download_java.pack(pady=5) # 日志输出 frame_log = tk.Frame(self.root) frame_log.pack(pady=5, fill=tk.BOTH, expand=True) tk.Label(frame_log, text="日志:").pack(anchor=tk.W) self.text_log = scrolledtext.ScrolledText(frame_log, state='disabled', height=8) self.text_log.pack(fill=tk.BOTH, expand=True) def log(self, message): """在日志区域添加信息""" self.text_log.config(state='normal') self.text_log.insert(tk.END, message + "\n") self.text_log.config(state='disabled') self.text_log.see(tk.END) # 自动滚动到底部 def refresh_versions(self): """刷新已下载版本列表""" self.listbox_versions.delete(0, tk.END) versions_path = os.path.join(self.minecraft_directory, "versions") if os.path.exists(versions_path): try: for item in os.scandir(versions_path): if item.is_dir(): self.listbox_versions.insert(tk.END, item.name) except Exception as e: self.log(f"刷新版本列表时出错: {e}") def on_version_select(self, event): """当在列表框中选择版本时,填充到输入框""" selection = self.listbox_versions.curselection() if selection: version = self.listbox_versions.get(selection[0]) self.entry_version.delete(0, tk.END) self.entry_version.insert(0, version) def get_username(self): """获取用户名""" username = self.entry_username.get().strip() if not username: messagebox.showerror("错误", "请输入用户名!") return None if ' ' in username: messagebox.showwarning("警告", "用户名包含空格,Minecraft 可能不支持。已移除空格。") username = username.replace(' ', '_') self.entry_username.delete(0, tk.END) self.entry_username.insert(0, username) return username def get_version(self): """获取版本""" version = self.entry_version.get().strip() if not version: messagebox.showerror("错误", "请输入或选择版本!") return None return version def is_version_installed(self, version): """检查版本是否已安装""" version_json_path = os.path.join(self.minecraft_directory, "versions", version, f"{version}.json") return os.path.exists(version_json_path) def download_minecraft(self, version_name): """下载 Minecraft 版本 (后台线程执行)""" try: self.log(f"开始下载版本 {version_name}...") # 确保 .minecraft 目录存在 os.makedirs(self.minecraft_directory, exist_ok=True) if self.is_version_installed(version_name): self.log(f"版本 {version_name} 已经存在!") return True # 使用新版本 API if hasattr(minecraft_launcher_lib, "install"): minecraft_launcher_lib.install.install_minecraft_version(version_name, self.minecraft_directory) else: # 旧版本 API (不太可能在新库中) minecraft_launcher_lib.utils.download_minecraft(version_name, self.minecraft_directory) if self.is_version_installed(version_name): self.log(f"版本 {version_name} 下载并验证成功!") # 在主线程刷新 UI self.root.after(0, self.refresh_versions) return True else: self.log(f"版本 {version_name} 下载失败或验证失败!") return False except Exception as e: self.log(f"下载版本 {version_name} 时出错: {e}") return False def download_version_gui(self): """下载版本按钮的回调""" version = self.get_version() if version: # 在新线程中执行下载,避免阻塞 GUI thread = threading.Thread(target=self.download_minecraft, args=(version,)) thread.daemon = True # 主程序关闭时,线程也关闭 thread.start() def download_java(self): """下载并解压 Java (后台线程执行)""" try: self.log("开始下载 Java (JDK 21)...") if os.path.exists(self.java_path): self.log("Java 已存在,无需下载。") return True # 确保 launcher 目录存在 os.makedirs(self.launcher_directory, exist_ok=True) # 下载 JDK self.log(f"正在从 {self.java_download_url} 下载 Java (约 180MB)...") with requests.get(self.java_download_url, stream=True) as r: r.raise_for_status() with open(self.java_zip_path, "wb") as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) self.log("Java 下载完成。") # 解压 ZIP 文件 self.log("正在解压 Java... (可能需要几分钟)") with zipfile.ZipFile(self.java_zip_path, "r") as zip_ref: zip_ref.extractall(self.launcher_directory) self.log("Java 解压完成。") # 清理 ZIP 文件 os.remove(self.java_zip_path) self.log("已删除 Java ZIP 压缩包。") # 验证 Java 是否可用 if os.path.exists(self.java_path): self.log("Java 安装成功!") # 在主线程更新按钮状态或提示 (如果需要) # self.root.after(0, lambda: self.button_download_java.config(state='disabled', text="Java 已下载")) return True else: self.log("Java 安装失败:解压后未找到 java.exe") return False except Exception as e: self.log(f"下载/安装 Java 时出错: {e}") # 清理失败的下载 try: if os.path.exists(self.java_zip_path): os.remove(self.java_zip_path) # 注意:解压失败的目录清理可能比较复杂,这里简化处理 # if os.path.exists(self.java_dir) and not os.listdir(self.java_dir): # os.rmdir(self.java_dir) except: pass return False def start_download_java(self): """下载 Java 按钮的回调""" # 简单确认 if messagebox.askyesno("确认", "即将下载约 180MB 的 Java (JDK 21) 文件。确定继续吗?"): self.log("--- 开始 Java 下载任务 ---") # 在新线程中执行下载,避免阻塞 GUI thread = threading.Thread(target=self.download_java) thread.daemon = True thread.start() def launch_game(self): """启动游戏按钮的回调""" username = self.get_username() version = self.get_version() if not username or not version: return if not self.is_version_installed(version): messagebox.showerror("错误", f"版本 {version} 未安装!请先下载。") return # 检查 Java if not os.path.exists(self.java_path): messagebox.showwarning("警告", "未检测到 Java。请先点击 '下载 Java' 按钮。") return self.log(f"准备启动游戏: {version} 作为用户: {username}") try: user_uuid = str(uuid.uuid3(uuid.NAMESPACE_DNS, username)) options = { "username": username, "uuid": user_uuid, "token": "", # 离线模式,token 为空 "launcherName": "DizzyCraftLauncher2_GUI", "versionName": version, "gameDirectory": self.minecraft_directory, } command = minecraft_launcher_lib.command.get_minecraft_command( version=version, minecraft_directory=self.minecraft_directory, options=options ) # 替换命令中的 Java 路径 command[0] = self.java_path self.log("启动命令: " + " ".join(command)) self.log("--- 游戏启动中,请稍候... ---\n请耐心等待,由于不会写多线程互通,游戏启动时启动器可能会出现未响应,等等就好了") # 启动游戏进程 (阻塞当前线程) # 注意:subprocess.run 会阻塞调用它的线程。 # 如果希望 GUI 在游戏运行时仍然响应,可以考虑使用 subprocess.Popen, # 但这会使得管理游戏进程生命周期变得复杂。 # 这里为了简化,直接在调用线程运行。如果在主线程调用会导致 GUI 冻结。 # 更好的做法是在新线程中启动,但需要处理进程间通信等问题。 # 作为示例,我们暂时在调用线程(即当前按钮点击事件的线程,它本身就在主线程之外) # 执行 run,但这意味着 GUI 会在此期间无响应。 # --- 改进:在新线程中启动游戏 --- def run_game(): try: # 重新获取用户名和版本,以防用户在点击后修改 # 但为了简化,我们使用点击时的值 result = subprocess.run(command) self.root.after(0, lambda: self.log(f"--- 游戏已退出 (返回码: {result.returncode}) ---")) except Exception as e: self.root.after(0, lambda: self.log(f"启动游戏进程时出错: {e}")) self.root.after(0, lambda: messagebox.showerror("启动错误", f"无法启动游戏: {e}")) game_thread = threading.Thread(target=run_game) game_thread.daemon = True # 主程序关闭时,游戏进程也可能被终止 (取决于系统) game_thread.start() self.log("游戏启动命令已发送到新线程。") except Exception as e: self.log(f"构建启动命令时出错: {e}") messagebox.showerror("启动错误", f"无法构建启动命令: {e}") if __name__ == "__main__": # --- 确保必要的目录存在 --- # (虽然类内部也会创建,但提前确保更安全) launcher_dir = "C:\\DizzyCraftLauncher2" os.makedirs(launcher_dir, exist_ok=True) os.makedirs(os.path.join(launcher_dir, ".minecraft"), exist_ok=True) root = tk.Tk() app = LauncherGUI(root) # 显示初始欢迎信息 app.log("DizzyCraftLauncher2 GUI 已启动。") root.mainloop()