1001 lines
41 KiB
Python
Executable File
1001 lines
41 KiB
Python
Executable File
import sys
|
||
import os
|
||
import subprocess
|
||
import platform
|
||
import configparser
|
||
import requests
|
||
import hashlib
|
||
import logging
|
||
import json
|
||
import socket
|
||
import threading
|
||
import time
|
||
from PyQt5.QtWidgets import QApplication, QFileDialog, QSystemTrayIcon, QMenu, QAction
|
||
from PyQt5.QtCore import (
|
||
QThread, pyqtSignal, QObject, pyqtSlot,
|
||
pyqtProperty, QUrl, QTimer, QMetaObject,
|
||
QVariant, Q_ARG, Qt
|
||
)
|
||
from PyQt5.QtQml import QQmlApplicationEngine
|
||
from PyQt5.QtGui import QIcon
|
||
from typing import Optional
|
||
|
||
# Настройка логирования
|
||
logging.basicConfig(
|
||
filename='launcher.log',
|
||
level=logging.DEBUG,
|
||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||
)
|
||
|
||
class ConfigManager:
|
||
def __init__(self, config_file: str = 'config.ini') -> None:
|
||
self.logger = logging.getLogger(__name__)
|
||
self.config_file = config_file
|
||
self.config = configparser.ConfigParser()
|
||
self.game_path = self.load_game_path()
|
||
self.current_version = self.load_current_version()
|
||
|
||
def load_game_path(self) -> Optional[str]:
|
||
try:
|
||
if os.path.exists(self.config_file):
|
||
self.config.read(self.config_file)
|
||
if 'Settings' in self.config and 'GamePath' in self.config['Settings']:
|
||
return self.config['Settings']['GamePath']
|
||
return None
|
||
except Exception as e:
|
||
self.logger.error(f"Ошибка при загрузке пути к игре: {str(e)}")
|
||
return None
|
||
|
||
def save_game_path(self, path: str) -> bool:
|
||
try:
|
||
self.config['Settings'] = {
|
||
'GamePath': path,
|
||
'CurrentVersion': self.current_version
|
||
}
|
||
with open(self.config_file, 'w') as configfile:
|
||
self.config.write(configfile)
|
||
return True
|
||
except Exception as e:
|
||
self.logger.error(f"Ошибка при сохранении пути к игре: {str(e)}")
|
||
return False
|
||
|
||
def load_current_version(self) -> str:
|
||
try:
|
||
if os.path.exists(self.config_file):
|
||
self.config.read(self.config_file)
|
||
if 'Settings' in self.config and 'CurrentVersion' in self.config['Settings']:
|
||
return self.config['Settings']['CurrentVersion']
|
||
return "3.3.5"
|
||
except Exception as e:
|
||
self.logger.error(f"Ошибка при загрузке версии: {str(e)}")
|
||
return "3.3.5"
|
||
|
||
class DownloadManager(QThread):
|
||
update_status = pyqtSignal(str)
|
||
update_progress = pyqtSignal(float)
|
||
update_file_name = pyqtSignal(str)
|
||
update_speed = pyqtSignal(str)
|
||
update_size_info = pyqtSignal(str) # Новый сигнал для отображения информации о размере файлов
|
||
finished = pyqtSignal()
|
||
|
||
def __init__(self, manifest_url: str, game_path: str, files_to_download=None):
|
||
super().__init__()
|
||
self.manifest_url = manifest_url
|
||
self.game_path = game_path
|
||
self.specific_files = files_to_download # список конкретных файлов для загрузки
|
||
self.is_downloading = True
|
||
self.files_to_download = {}
|
||
self.files_to_process = {} # Файлы, которые нужно скачать
|
||
self.corrupted_files = []
|
||
self.logger = logging.getLogger(__name__)
|
||
self.chunk_size = 8192
|
||
self.current_downloaded = 0
|
||
self.last_downloaded = 0
|
||
self.resume_position = 0 # Позиция для возобновления загрузки
|
||
self.speed_timer = QTimer()
|
||
self.speed_timer.timeout.connect(self.calculate_speed)
|
||
self.speed_timer.start(1000) # Обновляем каждую секунду
|
||
self.segment_size = 1024 * 1024 * 10 # 10 МБ сегменты
|
||
self.max_retries = 3 # Максимальное количество попыток загрузки файла
|
||
self.total_size = 0 # Общий размер всех файлов
|
||
self.total_downloaded = 0 # Общий размер загруженных файлов
|
||
self.current_speed = 0 # Текущая скорость загрузки
|
||
|
||
def check_existing_files(self):
|
||
"""Проверяет существующие файлы и их целостность"""
|
||
try:
|
||
response = requests.get(self.manifest_url)
|
||
response.raise_for_status()
|
||
manifest = response.json()['files']
|
||
|
||
# Если указаны конкретные файлы, загружаем только их
|
||
if self.specific_files:
|
||
self.files_to_process = {
|
||
filename: manifest[filename]
|
||
for filename in self.specific_files
|
||
if filename in manifest
|
||
}
|
||
else:
|
||
# Стандартная проверка всех файлов
|
||
self.files_to_download = manifest
|
||
self.files_to_process = {}
|
||
|
||
for filename, file_info in self.files_to_download.items():
|
||
local_path = os.path.join(self.game_path, filename)
|
||
needs_download = True
|
||
|
||
if os.path.exists(local_path):
|
||
# Проверяем размер файла
|
||
actual_size = os.path.getsize(local_path)
|
||
if actual_size == file_info['size']:
|
||
# Проверяем хеш только если размер совпадает
|
||
if self.verify_checksum(local_path, file_info['hash']):
|
||
needs_download = False
|
||
self.logger.info(f'Файл {filename} проверен и корректен')
|
||
continue
|
||
|
||
self.logger.warning(f'Файл {filename} поврежден или неполон. Будет перезагружен')
|
||
if os.path.exists(local_path):
|
||
os.remove(local_path)
|
||
|
||
if needs_download:
|
||
self.files_to_process[filename] = file_info
|
||
self.logger.info(f'Файл {filename} добавлен в очередь загрузки')
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"Ошибка при проверке файлов: {str(e)}")
|
||
raise
|
||
|
||
def stop(self):
|
||
self.is_downloading = False
|
||
self.speed_timer.stop()
|
||
self.logger.info('Загрузка остановлена пользователем')
|
||
|
||
def verify_checksum(self, file_path: str, expected_checksum: str) -> bool:
|
||
try:
|
||
sha256_hash = hashlib.sha256()
|
||
with open(file_path, "rb") as f:
|
||
for byte_block in iter(lambda: f.read(4096), b""):
|
||
sha256_hash.update(byte_block)
|
||
return sha256_hash.hexdigest() == expected_checksum
|
||
except Exception as e:
|
||
self.logger.error(f"Ошибка при проверке контрольной суммы {file_path}: {e}")
|
||
return False
|
||
|
||
def calculate_speed(self):
|
||
if hasattr(self, 'current_downloaded'):
|
||
self.current_speed = self.current_downloaded - self.last_downloaded
|
||
self.last_downloaded = self.current_downloaded
|
||
if self.current_speed > 0: # Отправляем сигнал только если есть прогресс
|
||
speed_str = f"{self.current_speed / (1024 * 1024):.1f} МБ/с" if self.current_speed > 1024 * 1024 else f"{self.current_speed / 1024:.1f} КБ/с"
|
||
self.update_speed.emit(speed_str)
|
||
# Обновляем информацию о размере файлов
|
||
downloaded_gb = self.total_downloaded / (1024 * 1024 * 1024)
|
||
total_gb = self.total_size / (1024 * 1024 * 1024)
|
||
downloaded_str = f"{downloaded_gb:.2f}/{total_gb:.2f} ГБ"
|
||
self.update_size_info.emit(downloaded_str)
|
||
|
||
def download_file_segmented(self, url: str, local_path: str, file_size: int):
|
||
temp_path = local_path + '.temp'
|
||
downloaded_size = 0
|
||
|
||
if os.path.exists(temp_path):
|
||
downloaded_size = os.path.getsize(temp_path)
|
||
self.total_downloaded += downloaded_size # Учитываем уже загруженный размер
|
||
|
||
with open(temp_path, 'ab') as f:
|
||
while downloaded_size < file_size:
|
||
start = downloaded_size
|
||
end = min(start + self.segment_size - 1, file_size - 1)
|
||
|
||
for attempt in range(self.max_retries):
|
||
try:
|
||
headers = {'Range': f'bytes={start}-{end}'}
|
||
response = requests.get(url, headers=headers, stream=True)
|
||
|
||
if response.status_code in [206, 200]:
|
||
for chunk in response.iter_content(chunk_size=8192):
|
||
if chunk:
|
||
f.write(chunk)
|
||
downloaded_size += len(chunk)
|
||
self.total_downloaded += len(chunk)
|
||
self.current_downloaded = self.total_downloaded
|
||
# Обновляем общий прогресс
|
||
self.update_progress.emit(self.total_downloaded / self.total_size)
|
||
break
|
||
except Exception as e:
|
||
if attempt == self.max_retries - 1:
|
||
raise Exception(f"Не удалось загрузить сегмент файла {self.max_retries} попыток")
|
||
continue
|
||
|
||
return temp_path
|
||
|
||
def run(self):
|
||
try:
|
||
self.check_existing_files()
|
||
|
||
# Вычисляем общий размер файлов для загрузки
|
||
self.total_size = sum(file_info['size'] for file_info in self.files_to_process.values())
|
||
self.total_downloaded = 0
|
||
|
||
if len(self.files_to_process) == 0:
|
||
self.update_status.emit('Все файлы актуальны')
|
||
self.update_progress.emit(1.0) # Устанавливаем полный прогресс
|
||
return
|
||
|
||
for filename, file_info in self.files_to_process.items():
|
||
if not self.is_downloading:
|
||
break
|
||
|
||
self.update_file_name.emit(filename)
|
||
local_path = os.path.join(self.game_path, filename)
|
||
|
||
# Создаем директории если нужно
|
||
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
||
|
||
# Загружаем файлы сегментами
|
||
temp_path = self.download_file_segmented(
|
||
f'http://dl.neix.ru/{filename}',
|
||
local_path,
|
||
file_info['size']
|
||
)
|
||
|
||
# Проверяем целостность файла
|
||
if self.verify_checksum(temp_path, file_info['hash']):
|
||
if os.path.exists(local_path):
|
||
os.remove(local_path)
|
||
os.rename(temp_path, local_path)
|
||
else:
|
||
self.corrupted_files.append(filename)
|
||
if os.path.exists(temp_path):
|
||
os.remove(temp_path)
|
||
|
||
if self.is_downloading:
|
||
status = 'Загрузка успешно завершена!' if not self.corrupted_files else 'Загрузка завершена с ошибками'
|
||
self.update_status.emit(status)
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"Ошибка при загрузке: {str(e)}")
|
||
self.update_status.emit(f'Ошибка: {str(e)}')
|
||
finally:
|
||
self.finished.emit()
|
||
|
||
class ServerChecker(QThread):
|
||
status_changed = pyqtSignal(bool, str)
|
||
|
||
def __init__(self, auth_host='127.0.0.1', auth_port=3724, world_host='127.0.0.1', world_port=8085):
|
||
super().__init__()
|
||
self.auth_host = auth_host
|
||
self.auth_port = auth_port
|
||
self.world_host = world_host
|
||
self.world_port = world_port
|
||
self.is_running = True
|
||
self.logger = logging.getLogger(__name__)
|
||
|
||
def check_port(self, host: str, port: int) -> bool:
|
||
try:
|
||
with socket.create_connection((host, port), timeout=2):
|
||
return True
|
||
except (socket.timeout, socket.error):
|
||
return False
|
||
|
||
def run(self):
|
||
while self.is_running:
|
||
auth_status = self.check_port(self.auth_host, self.auth_port)
|
||
world_status = self.check_port(self.world_host, self.world_port)
|
||
|
||
if auth_status and world_status:
|
||
self.status_changed.emit(True, "Оба сервера")
|
||
elif auth_status:
|
||
self.status_changed.emit(True, "Auth сервер")
|
||
elif world_status:
|
||
self.status_changed.emit(True, "World сервер")
|
||
else:
|
||
self.status_changed.emit(False, "Offline")
|
||
|
||
time.sleep(5)
|
||
|
||
def stop(self):
|
||
self.is_running = False
|
||
|
||
class Settings(QObject):
|
||
settingsChanged = pyqtSignal()
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self._settings = {
|
||
'autostart': False,
|
||
'closeOnLaunch': True,
|
||
'speedLimit': 0,
|
||
'autoUpdate': True,
|
||
'slideInterval': 5,
|
||
'showNotifications': True,
|
||
'linuxEmulator': 'wine', # wine, lutris, proton, portproton, crossover
|
||
'close_to_tray': True
|
||
}
|
||
self.load_settings()
|
||
|
||
def load_settings(self):
|
||
try:
|
||
if os.path.exists('settings.json'):
|
||
with open('settings.json', 'r') as f:
|
||
self._settings.update(json.load(f))
|
||
except Exception as e:
|
||
logging.error(f"Ошибка загрузки настроек: {e}")
|
||
|
||
def save_settings(self):
|
||
try:
|
||
with open('settings.json', 'w') as f:
|
||
json.dump(self._settings, f)
|
||
except Exception as e:
|
||
logging.error(f"Ошибка сохранения настроек: {e}")
|
||
|
||
@pyqtProperty(bool, notify=settingsChanged)
|
||
def autostart(self): return self._settings['autostart']
|
||
@autostart.setter
|
||
def autostart(self, value):
|
||
if self._settings['autostart'] != value:
|
||
self._settings['autostart'] = value
|
||
self.settingsChanged.emit()
|
||
|
||
@pyqtProperty(bool, notify=settingsChanged)
|
||
def closeOnLaunch(self):
|
||
return self._settings['closeOnLaunch']
|
||
@closeOnLaunch.setter
|
||
def closeOnLaunch(self, value):
|
||
if self._settings['closeOnLaunch'] != value:
|
||
self._settings['closeOnLaunch'] = value
|
||
self.settingsChanged.emit()
|
||
|
||
@pyqtProperty(int, notify=settingsChanged)
|
||
def speedLimit(self): return self._settings['speedLimit']
|
||
@speedLimit.setter
|
||
def speedLimit(self, value):
|
||
if self._settings['speedLimit'] != value:
|
||
self._settings['speedLimit'] = value
|
||
self.settingsChanged.emit()
|
||
|
||
@pyqtProperty(bool, notify=settingsChanged)
|
||
def autoUpdate(self): return self._settings['autoUpdate']
|
||
@autoUpdate.setter
|
||
def autoUpdate(self, value):
|
||
if self._settings['autoUpdate'] != value:
|
||
self._settings['autoUpdate'] = value
|
||
self.settingsChanged.emit()
|
||
|
||
@pyqtProperty(int, notify=settingsChanged)
|
||
def slideInterval(self): return self._settings['slideInterval']
|
||
@slideInterval.setter
|
||
def slideInterval(self, value):
|
||
if self._settings['slideInterval'] != value:
|
||
self._settings['slideInterval'] = value
|
||
self.settingsChanged.emit()
|
||
|
||
@pyqtProperty(bool, notify=settingsChanged)
|
||
def showNotifications(self): return self._settings['showNotifications']
|
||
@showNotifications.setter
|
||
def showNotifications(self, value):
|
||
if self._settings['showNotifications'] != value:
|
||
self._settings['showNotifications'] = value
|
||
self.settingsChanged.emit()
|
||
|
||
@pyqtProperty(str, notify=settingsChanged)
|
||
def linuxEmulator(self): return self._settings['linuxEmulator']
|
||
@linuxEmulator.setter
|
||
def linuxEmulator(self, value):
|
||
if self._settings['linuxEmulator'] != value:
|
||
self._settings['linuxEmulator'] = value
|
||
self.settingsChanged.emit()
|
||
|
||
@pyqtProperty(bool, notify=settingsChanged)
|
||
def closeToTray(self):
|
||
return self._settings['close_to_tray']
|
||
|
||
@closeToTray.setter
|
||
def closeToTray(self, value):
|
||
if self._settings['close_to_tray'] != value:
|
||
self._settings['close_to_tray'] = value
|
||
self.settingsChanged.emit()
|
||
self.save_settings()
|
||
|
||
class LauncherBackend(QObject):
|
||
# Сигналы
|
||
statusTextChanged = pyqtSignal()
|
||
gamePathChanged = pyqtSignal()
|
||
isDownloadingChanged = pyqtSignal()
|
||
downloadProgressChanged = pyqtSignal()
|
||
downloadSpeedChanged = pyqtSignal()
|
||
currentFileNameChanged = pyqtSignal()
|
||
currentImageChanged = pyqtSignal()
|
||
canPlayChanged = pyqtSignal()
|
||
serverStatusChanged = pyqtSignal()
|
||
isServerOnlineChanged = pyqtSignal()
|
||
versionChanged = pyqtSignal()
|
||
notificationRequested = pyqtSignal(str, str) # message, type
|
||
downloadSizeInfoChanged = pyqtSignal() # новый сигнал
|
||
|
||
def __init__(self, config_manager):
|
||
super().__init__()
|
||
self.logger = logging.getLogger(__name__)
|
||
self._config_manager = config_manager
|
||
self._download_manager = None
|
||
self._server_checker = ServerChecker()
|
||
self._server_checker.status_changed.connect(self._handle_server_status)
|
||
self._server_checker.start()
|
||
|
||
# Сохраняем ссылку на engine
|
||
self.engine = None # Будет установлено позже
|
||
|
||
# Инициализация свойств
|
||
self._game_path = config_manager.game_path or ""
|
||
self._is_downloading = False
|
||
self._download_progress = 0.0
|
||
self._download_speed = ""
|
||
self._current_file_name = ""
|
||
self._current_image = "images/slide/1.jpg"
|
||
self._can_play = False
|
||
self._server_status = "⚫ Offline"
|
||
self._is_server_online = False
|
||
self._version = "3.3.5"
|
||
self._status_text = "Пожалуйста, выберите папку с игрой" # Инициализируем значение по умолчанию
|
||
|
||
# Инициализация слайд-шоу
|
||
self._slides = [
|
||
"qml/images/slide1.jpg",
|
||
"qml/images/slide2.jpg",
|
||
"qml/images/slide3.jpg",
|
||
"qml/images/slide4.jpg"
|
||
]
|
||
|
||
# Проверяем возможность игры при старте
|
||
self._check_can_play()
|
||
|
||
# Подключаем сигнал к QML
|
||
self.notificationRequested.connect(
|
||
lambda msg, type: QMetaObject.invokeMethod(
|
||
self.engine.rootObjects()[0],
|
||
"showNotification",
|
||
Q_ARG(QVariant, msg),
|
||
Q_ARG(QVariant, type)
|
||
)
|
||
)
|
||
|
||
self._settings = Settings()
|
||
self._file_verifier = None
|
||
self._download_size_info = ""
|
||
|
||
# Инициализация трея
|
||
self._tray_icon = QSystemTrayIcon()
|
||
self._tray_icon.setIcon(QIcon("qml/images/icons/tray.png")) # Иконка для трея
|
||
self._tray_icon.setToolTip("AzerothCraft Launcher")
|
||
|
||
# Создаем меню для трея
|
||
tray_menu = QMenu()
|
||
show_action = QAction('Показать', self)
|
||
quit_action = QAction('Выход', self)
|
||
|
||
show_action.triggered.connect(self.show_window)
|
||
quit_action.triggered.connect(QApplication.quit)
|
||
|
||
tray_menu.addAction(show_action)
|
||
tray_menu.addSeparator()
|
||
tray_menu.addAction(quit_action)
|
||
|
||
self._tray_icon.setContextMenu(tray_menu)
|
||
self._tray_icon.activated.connect(self._tray_icon_activated)
|
||
self._tray_icon.show()
|
||
|
||
# Свойства через декораторы
|
||
@pyqtProperty(str, notify=statusTextChanged)
|
||
def statusText(self): return self._status_text
|
||
@statusText.setter
|
||
def statusText(self, value):
|
||
if self._status_text != value:
|
||
self._status_text = value
|
||
self.statusTextChanged.emit()
|
||
|
||
@pyqtProperty(str, notify=gamePathChanged)
|
||
def gamePath(self): return self._game_path
|
||
@gamePath.setter
|
||
def gamePath(self, value):
|
||
if self._game_path != value:
|
||
self._game_path = value
|
||
self.gamePathChanged.emit()
|
||
|
||
@pyqtProperty(bool, notify=isDownloadingChanged)
|
||
def isDownloading(self): return self._is_downloading
|
||
@isDownloading.setter
|
||
def isDownloading(self, value):
|
||
if self._is_downloading != value:
|
||
self._is_downloading = value
|
||
self.isDownloadingChanged.emit()
|
||
|
||
@pyqtProperty(float, notify=downloadProgressChanged)
|
||
def downloadProgress(self): return self._download_progress
|
||
@downloadProgress.setter
|
||
def downloadProgress(self, value):
|
||
if self._download_progress != value:
|
||
self._download_progress = value
|
||
self.downloadProgressChanged.emit()
|
||
|
||
@pyqtProperty(str, notify=downloadSpeedChanged)
|
||
def downloadSpeed(self): return self._download_speed
|
||
@downloadSpeed.setter
|
||
def downloadSpeed(self, value):
|
||
if self._download_speed != value:
|
||
self._download_speed = value
|
||
self.downloadSpeedChanged.emit()
|
||
|
||
@pyqtProperty(str, notify=currentFileNameChanged)
|
||
def currentFileName(self): return self._current_file_name
|
||
@currentFileName.setter
|
||
def currentFileName(self, value):
|
||
if self._current_file_name != value:
|
||
self._current_file_name = value
|
||
self.currentFileNameChanged.emit()
|
||
|
||
@pyqtProperty(str, notify=currentImageChanged)
|
||
def currentImage(self): return self._current_image
|
||
@currentImage.setter
|
||
def currentImage(self, value):
|
||
if self._current_image != value:
|
||
self._current_image = value
|
||
self.currentImageChanged.emit()
|
||
|
||
@pyqtProperty(bool, notify=canPlayChanged)
|
||
def canPlay(self): return self._can_play
|
||
@canPlay.setter
|
||
def canPlay(self, value):
|
||
if self._can_play != value:
|
||
self._can_play = value
|
||
self.canPlayChanged.emit()
|
||
|
||
@pyqtProperty(str, notify=serverStatusChanged)
|
||
def serverStatus(self): return self._server_status
|
||
@serverStatus.setter
|
||
def serverStatus(self, value):
|
||
if self._server_status != value:
|
||
self._server_status = value
|
||
self.serverStatusChanged.emit()
|
||
|
||
@pyqtProperty(bool, notify=isServerOnlineChanged)
|
||
def isServerOnline(self): return self._is_server_online
|
||
@isServerOnline.setter
|
||
def isServerOnline(self, value):
|
||
if self._is_server_online != value:
|
||
self._is_server_online = value
|
||
self.isServerOnlineChanged.emit()
|
||
|
||
@pyqtProperty(str, notify=versionChanged)
|
||
def version(self): return self._version
|
||
|
||
@pyqtProperty(list)
|
||
def slides(self):
|
||
return self._slides
|
||
|
||
@pyqtProperty(QObject, constant=True)
|
||
def settings(self):
|
||
return self._settings
|
||
|
||
@pyqtProperty(str, notify=downloadSizeInfoChanged)
|
||
def downloadSizeInfo(self): return self._download_size_info
|
||
@downloadSizeInfo.setter
|
||
def downloadSizeInfo(self, value):
|
||
if self._download_size_info != value:
|
||
self._download_size_info = value
|
||
self.downloadSizeInfoChanged.emit()
|
||
|
||
@pyqtSlot()
|
||
def selectGamePath(self):
|
||
try:
|
||
folder = QFileDialog.getExistingDirectory(None, "Выберите папку с игрой")
|
||
if folder:
|
||
self.gamePath = folder
|
||
self._config_manager.save_game_path(folder)
|
||
self.statusText = f"Выбрана папка: {folder}"
|
||
self._check_can_play()
|
||
if not self.canPlay:
|
||
self.notificationRequested.emit(
|
||
"Игра не обнаружена. Нажмите 'Скачать клиент' для загрузки",
|
||
"info"
|
||
)
|
||
else:
|
||
self.notificationRequested.emit(
|
||
"Папка с игрой успешно выбрана",
|
||
"success"
|
||
)
|
||
except Exception as e:
|
||
self._handle_error(f"Ошибка при выборе папки: {str(e)}")
|
||
|
||
@pyqtSlot()
|
||
def startDownload(self):
|
||
if self.isDownloading:
|
||
if self._download_manager:
|
||
self._download_manager.stop()
|
||
self._download_manager = None
|
||
self.isDownloading = False
|
||
self.statusText = "Загрузка остановлена"
|
||
self.downloadProgress = 0.0
|
||
self.downloadSpeed = ""
|
||
self.currentFileName = ""
|
||
return
|
||
|
||
if not self.gamePath:
|
||
self.statusText = "Сначала выберите папку для установки"
|
||
return
|
||
|
||
self.isDownloading = True
|
||
self.statusText = "Начало загрузки..."
|
||
self._download_manager = DownloadManager("http://you.url.com/client.json", self.gamePath)
|
||
self._download_manager.update_progress.connect(self._handle_progress)
|
||
self._download_manager.update_status.connect(self._handle_status)
|
||
self._download_manager.update_file_name.connect(self._handle_filename)
|
||
self._download_manager.update_speed.connect(self._handle_speed)
|
||
self._download_manager.finished.connect(self._handle_download_finished)
|
||
self._download_manager.update_size_info.connect(self._handle_size_info)
|
||
self._download_manager.start()
|
||
|
||
@pyqtSlot()
|
||
def launchGame(self):
|
||
if not self.canPlay:
|
||
return
|
||
|
||
try:
|
||
game_exe = os.path.join(self.gamePath, "Wow.exe")
|
||
game_process = None
|
||
|
||
if platform.system() == 'Windows':
|
||
game_process = subprocess.Popen([game_exe])
|
||
else:
|
||
emulator = self._settings.linuxEmulator
|
||
if emulator == 'wine':
|
||
game_process = subprocess.Popen(['wine', game_exe])
|
||
elif emulator == 'lutris':
|
||
game_process = subprocess.Popen(['lutris', 'rungame', game_exe])
|
||
elif emulator == 'proton':
|
||
game_process = subprocess.Popen(['proton', 'run', game_exe])
|
||
elif emulator == 'portproton':
|
||
game_process = subprocess.Popen(['portproton', 'run', game_exe])
|
||
elif emulator == 'crossover':
|
||
game_process = subprocess.Popen(['crossover', game_exe])
|
||
|
||
self.statusText = "Игра запущена"
|
||
|
||
# Сворачиваем в трей если включена настройка
|
||
if self._settings.closeOnLaunch:
|
||
self.minimizeToTray()
|
||
|
||
# Запускаем мониторинг процесса игры в отдельном потоке
|
||
monitor_thread = threading.Thread(
|
||
target=self._monitor_game_process,
|
||
args=(game_process,),
|
||
daemon=True
|
||
)
|
||
monitor_thread.start()
|
||
|
||
except Exception as e:
|
||
self.statusText = f"Ошибка при запуске игры: {str(e)}"
|
||
|
||
def _monitor_game_process(self, process):
|
||
"""Мониторит процесс игры и разворачивает лаунчер после завершения"""
|
||
try:
|
||
process.wait() # Ждем завершения процесса игры
|
||
# разворачиваем лаунчер
|
||
QMetaObject.invokeMethod(
|
||
self,
|
||
"show_window",
|
||
Qt.QueuedConnection
|
||
)
|
||
if self._settings.showNotifications:
|
||
self._tray_icon.showMessage(
|
||
"WoW Launcher",
|
||
"Игра завершена",
|
||
QSystemTrayIcon.Information, # Используем информационное сообщение
|
||
2000
|
||
)
|
||
except Exception as e:
|
||
self.logger.error(f"Ошибка при мониторинге процесса игры: {str(e)}")
|
||
|
||
def _check_can_play(self):
|
||
if self.gamePath:
|
||
if os.path.exists(os.path.join(self.gamePath, "Wow.exe")):
|
||
self.canPlay = True
|
||
self.statusText = f"Выбрана папка: {self.gamePath}"
|
||
else:
|
||
self.canPlay = False
|
||
self.statusText = "Игра не обнаружена. Нажмите 'Скачать клиент' для загрузки"
|
||
else:
|
||
self.canPlay = False
|
||
self.statusText = "Пожалуйста, выберите папку с игрой"
|
||
|
||
def _handle_progress(self, progress):
|
||
self.downloadProgress = progress
|
||
|
||
def _handle_status(self, status):
|
||
self.statusText = status
|
||
|
||
def _handle_filename(self, filename):
|
||
self.currentFileName = filename
|
||
|
||
def _handle_speed(self, speed):
|
||
self.downloadSpeed = speed
|
||
|
||
def _handle_download_finished(self):
|
||
self.isDownloading = False
|
||
self._check_can_play()
|
||
|
||
# Сбрасываем прогресс и скорость при завершении
|
||
self.downloadProgress = 0.0
|
||
self.downloadSpeed = ""
|
||
self.currentFileName = ""
|
||
self.downloadSizeInfo = "" # Сбрасываем информацию о размере
|
||
|
||
if self._download_manager and self._download_manager.corrupted_files:
|
||
corrupted = len(self._download_manager.corrupted_files)
|
||
self.notificationRequested.emit(
|
||
f"Загрузка завершена с ошибками. Повреждено файлов: {corrupted}",
|
||
"error"
|
||
)
|
||
else:
|
||
self.notificationRequested.emit(
|
||
"Загрузка завершена успешно",
|
||
"success"
|
||
)
|
||
|
||
self._download_manager = None
|
||
|
||
def _handle_server_status(self, is_online, status):
|
||
self.isServerOnline = is_online
|
||
self.serverStatus = f"{'🟢' if is_online else '⚫'} {status}"
|
||
|
||
def _handle_error(self, error_msg):
|
||
self.logger.error(error_msg)
|
||
self.notificationRequested.emit(error_msg, "error")
|
||
|
||
@pyqtSlot()
|
||
def saveSettings(self):
|
||
self._settings.save_settings()
|
||
# Применяем настройки
|
||
if self._settings.autostart:
|
||
self._setup_autostart()
|
||
# ... другие действия при изменении настроек ...
|
||
|
||
@pyqtSlot()
|
||
def openGameFolder(self):
|
||
if self.gamePath:
|
||
if platform.system() == 'Windows':
|
||
os.startfile(self.gamePath)
|
||
else:
|
||
subprocess.Popen(['xdg-open', self.gamePath])
|
||
|
||
@pyqtSlot()
|
||
def verifyFiles(self):
|
||
if not self.isDownloading and self.gamePath:
|
||
self.statusText = "Проверка файлов..."
|
||
self._file_verifier = FileVerifier("http://you.url.com/client.json", self.gamePath)
|
||
self._file_verifier.progress_changed.connect(self._handle_verify_progress)
|
||
self._file_verifier.status_changed.connect(self._handle_status)
|
||
self._file_verifier.verification_complete.connect(self._handle_verify_complete)
|
||
self._file_verifier.start()
|
||
self.notificationRequested.emit("Проверка файлов начата", "info")
|
||
|
||
def _handle_verify_progress(self, progress):
|
||
self.downloadProgress = progress
|
||
|
||
def _handle_verify_complete(self, corrupted_files):
|
||
if corrupted_files:
|
||
self.statusText = f"Найдено {len(corrupted_files)} поврежденных файлов"
|
||
self.notificationRequested.emit(
|
||
f"Проверка завершена. Найдено {len(corrupted_files)} поврежденных файлов",
|
||
"error"
|
||
)
|
||
# Спрашиваем пользователя о восстановлении
|
||
self.repairClient(corrupted_files)
|
||
else:
|
||
self.statusText = "Проверка завершена. Все файлы в порядке"
|
||
self.notificationRequested.emit(
|
||
"Проверка завершена. Все файлы в порядке",
|
||
"success"
|
||
)
|
||
self.downloadProgress = 0.0
|
||
|
||
@pyqtSlot()
|
||
def repairClient(self, files_to_repair=None):
|
||
if not self.isDownloading and self.gamePath:
|
||
self.statusText = "Восстановление клиента..."
|
||
# Если список файлов не передан, проверяем все файлы
|
||
if files_to_repair is None:
|
||
self.verifyFiles()
|
||
return
|
||
|
||
# Запускаем загрузку только поврежденных файлов
|
||
self.isDownloading = True
|
||
self._download_manager = DownloadManager(
|
||
"http://you.url.com/client.json",
|
||
self.gamePath,
|
||
files_to_repair
|
||
)
|
||
self._download_manager.update_progress.connect(self._handle_progress)
|
||
self._download_manager.update_status.connect(self._handle_status)
|
||
self._download_manager.update_file_name.connect(self._handle_filename)
|
||
self._download_manager.update_speed.connect(self._handle_speed)
|
||
self._download_manager.finished.connect(self._handle_download_finished)
|
||
self._download_manager.start()
|
||
self.notificationRequested.emit(
|
||
f"Начато восстановление {len(files_to_repair)} файлов",
|
||
"info"
|
||
)
|
||
|
||
@pyqtSlot()
|
||
def checkEmulator(self):
|
||
try:
|
||
emulator = self._settings.linuxEmulator
|
||
if platform.system() == 'Windows':
|
||
self.notificationRequested.emit(
|
||
"Проверка эмулятора доступна только в Linux/Mac",
|
||
"info"
|
||
)
|
||
return
|
||
|
||
# Проверяем наличие эмулятора
|
||
if emulator == 'wine':
|
||
process = subprocess.Popen(['wine', '--version'],
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE)
|
||
elif emulator == 'lutris':
|
||
process = subprocess.Popen(['lutris', '--version'],
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE)
|
||
elif emulator == 'proton':
|
||
process = subprocess.Popen(['proton', '--version'],
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE)
|
||
elif emulator == 'portproton':
|
||
process = subprocess.Popen(['portproton', '--version'],
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE)
|
||
elif emulator == 'crossover':
|
||
process = subprocess.Popen(['crossover', '--version'],
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE)
|
||
|
||
out, err = process.communicate()
|
||
if process.returncode == 0:
|
||
self.notificationRequested.emit(
|
||
f"Эмулятор {emulator} установлен и работает корректно",
|
||
"success"
|
||
)
|
||
else:
|
||
self.notificationRequested.emit(
|
||
f"Эмулятор {emulator} не найден или не работает",
|
||
"error"
|
||
)
|
||
except Exception as e:
|
||
self.notificationRequested.emit(
|
||
f"Ошибка при проверке эмулятора: {str(e)}",
|
||
"error"
|
||
)
|
||
|
||
def _handle_size_info(self, size_info):
|
||
self.downloadSizeInfo = size_info
|
||
|
||
@pyqtSlot()
|
||
def show_window(self):
|
||
if self.engine and self.engine.rootObjects():
|
||
window = self.engine.rootObjects()[0]
|
||
window.show()
|
||
window.raise_()
|
||
window.requestActivate()
|
||
|
||
def _tray_icon_activated(self, reason):
|
||
if reason == QSystemTrayIcon.DoubleClick:
|
||
self.show_window()
|
||
|
||
@pyqtSlot()
|
||
def minimizeToTray(self):
|
||
if self.engine and self.engine.rootObjects():
|
||
window = self.engine.rootObjects()[0]
|
||
window.hide()
|
||
if self._settings.showNotifications:
|
||
self._tray_icon.showMessage(
|
||
"WoW Launcher",
|
||
"Лаунчер свернут в трей",
|
||
QSystemTrayIcon.Information, # Используем информационное сообщение
|
||
2000
|
||
)
|
||
|
||
class FileVerifier(QThread):
|
||
progress_changed = pyqtSignal(float)
|
||
status_changed = pyqtSignal(str)
|
||
verification_complete = pyqtSignal(list) # список поврежденных файлов
|
||
|
||
def __init__(self, manifest_url: str, game_path: str):
|
||
super().__init__()
|
||
self.manifest_url = manifest_url
|
||
self.game_path = game_path
|
||
self.is_running = True
|
||
self.logger = logging.getLogger(__name__)
|
||
|
||
def stop(self):
|
||
self.is_running = False
|
||
|
||
def run(self):
|
||
try:
|
||
self.status_changed.emit("Загрузка манифеста...")
|
||
response = requests.get(self.manifest_url)
|
||
response.raise_for_status()
|
||
manifest = response.json()['files']
|
||
|
||
corrupted_files = []
|
||
total_files = len(manifest)
|
||
checked_files = 0
|
||
|
||
for filename, file_info in manifest.items():
|
||
if not self.is_running:
|
||
break
|
||
|
||
local_path = os.path.join(self.game_path, filename)
|
||
self.status_changed.emit(f"Проверка: {filename}")
|
||
|
||
if not os.path.exists(local_path):
|
||
corrupted_files.append(filename)
|
||
else:
|
||
# Проверяем размер
|
||
if os.path.getsize(local_path) != file_info['size']:
|
||
corrupted_files.append(filename)
|
||
else:
|
||
# Проверяем хеш
|
||
sha256_hash = hashlib.sha256()
|
||
with open(local_path, "rb") as f:
|
||
for byte_block in iter(lambda: f.read(4096), b""):
|
||
if not self.is_running:
|
||
return
|
||
sha256_hash.update(byte_block)
|
||
|
||
if sha256_hash.hexdigest() != file_info['hash']:
|
||
corrupted_files.append(filename)
|
||
|
||
checked_files += 1
|
||
self.progress_changed.emit(checked_files / total_files)
|
||
|
||
if self.is_running:
|
||
self.verification_complete.emit(corrupted_files)
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"Ошибка при проверке файлов: {str(e)}")
|
||
self.status_changed.emit(f"Ошибка: {str(e)}")
|
||
|
||
if __name__ == '__main__':
|
||
app = QApplication(sys.argv)
|
||
|
||
# Добавляем поддержку QtGraphicalEffects
|
||
import os
|
||
os.environ['QT_QUICK_CONTROLS_STYLE'] = 'Material'
|
||
os.environ['QT_QUICK_CONTROLS_MATERIAL_VARIANT'] = 'Dense'
|
||
|
||
# Устанавливаем программный рендеринг для лучшей совместимости
|
||
from PyQt5.QtQuick import QQuickWindow
|
||
QQuickWindow.setSceneGraphBackend('software')
|
||
|
||
# Регистрируем Settings для QML
|
||
from PyQt5.QtQml import qmlRegisterType
|
||
qmlRegisterType(Settings, 'Settings', 1, 0, 'Settings')
|
||
|
||
engine = QQmlApplicationEngine()
|
||
|
||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||
engine.addImportPath(current_dir)
|
||
|
||
config_manager = ConfigManager()
|
||
backend = LauncherBackend(config_manager)
|
||
backend.engine = engine # Устанавливаем ссылку на engine
|
||
engine.rootContext().setContextProperty("launcher", backend)
|
||
|
||
qml_file = os.path.join(current_dir, 'main.qml')
|
||
engine.load(QUrl.fromLocalFile(qml_file))
|
||
|
||
if not engine.rootObjects():
|
||
sys.exit(-1)
|
||
|
||
sys.exit(app.exec_())
|