Files
DizzyCraftLauncher/yclgui.py
T
2026-04-26 20:42:30 +08:00

330 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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('<<ListboxSelect>>', 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()