Files
LCE-Qt-Launcher/src/lce_qt_launcher/managers/instance_manager.py
xgui4 1a8d41a412 Refactor application :
Refactor application  structure by removing BuildInfo class and integrating its attributes directly into the application context. Update features and views to utilize new string constants for application metadata. Replace UserPref class with a new implementation in pref.py, streamlining user preference management. Enhance instance manager and launcher views to accommodate these changes, ensuring consistent access to application information. Remove deprecated preferences module and adjust related functionality across the codebase.
2026-05-18 00:34:25 -04:00

389 lines
14 KiB
Python

# TODO : Make Instance mode indpendant
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from lce_qt_launcher.app_context import AppContext
from lce_qt_launcher import instance_extension_str
import lce_qt_launcher.views.term_service as term_service
from PySide6.QtNetwork import QNetworkReply
from PySide6.QtCore import QObject
from PySide6.QtWidgets import QMessageBox
from enum import Enum
from subprocess import TimeoutExpired
import subprocess
import json
import os
SCHEME_VERSION = "https://raw.githubusercontent.com/xgui4/LCE-Qt-Launcher/refs/heads/beta/schemas/x-application-lce_inst.json"
class InstanceSource(Enum):
"""_summary_ The 4 Type of Instances (2 functional, the othes coming soon)"""
GITHUB_RELEASE = 0
FORGEJO_RELEASE = 1
REMOTE_GIT_SOURCE = 2
LOCAL_INSTALLATION = 3
LOCAL_SOURCE_CODE = 4
def from_int_to_InstanceSource(value: int) -> InstanceSource:
"""_summary_ Convert Into to Instance Source
Args:
value (int): _description_ the int to convert
Raises:
RuntimeError: _description_ Raise if a incorrect int is specified
Returns:
InstanceSource: _description_ The InstanceSource from the int
"""
match value:
case 0:
return InstanceSource.GITHUB_RELEASE
case 1:
return InstanceSource.FORGEJO_RELEASE
case 2:
return InstanceSource.REMOTE_GIT_SOURCE
case 3:
return InstanceSource.LOCAL_INSTALLATION
case 4:
return InstanceSource.LOCAL_SOURCE_CODE
case _:
raise RuntimeError(f"{value} is an Incorrect InstanceSource Type")
def from_str_to_InstanceSource(string: str) -> InstanceSource:
"""_summary_ Convert str to Instance Source
Args:
string (str): _description_ the string to convert
Raises:
RuntimeError: _description_ Raise if a incorrect str is specified
Returns:
InstanceSource: _description_ The InstanceSource from the str
"""
match string:
case "InstanceSource.GITHUB_RELEASE" | "0":
return InstanceSource.GITHUB_RELEASE
case "InstanceSource.FORGEJO_RELEASE" | "1":
return InstanceSource.FORGEJO_RELEASE
case "InstanceSource.REMOTE_GIT_SOURCE" | "2":
return InstanceSource.REMOTE_GIT_SOURCE
case "InstanceSource.LOCAL_INSTALLATION" | "3":
return InstanceSource.LOCAL_INSTALLATION
case "InstanceSource.LOCAL_SOURCE_CODE" | "4":
return InstanceSource.LOCAL_SOURCE_CODE
case _:
raise RuntimeError(f"{string} is an Incorrect InstanceSource Type")
class InstanceType(Enum):
"""_summary_ The instance Type (no function yet)"""
CLIENT_VANILLA = 0
CLENT_MODDED = 1
SERVER_VANILLA = 2
SERVER_MODDED = 3
def from_int_to_InstanceType(value: int) -> InstanceType:
"""_summary_ Convert Into to Instance Type
Args:
value (int): _description_ the int to convert
Raises:
RuntimeError: _description_ Raise if a incorrect int is specified
Returns:
InstanceType: _description_ The InstanceType from the int
"""
match value:
case 0:
return InstanceType.CLIENT_VANILLA
case 1:
return InstanceType.CLENT_MODDED
case 2:
return InstanceType.SERVER_VANILLA
case 3:
return InstanceType.SERVER_MODDED
case _:
raise RuntimeError(f"{value} is an Incorrect InstanceType Type")
def from_str_to_InstanceType(string: str) -> InstanceType:
"""_summary_ Convert str to Instance Type
Args:
string (str): _description_ the string to convert
Raises:
RuntimeError: _description_ Raise if a incorrect str is specified
Returns:
InstanceSource: _description_ The InstanceType from the str
"""
match string:
case "InstanceType.CLIENT_VANILLA":
return InstanceType.CLIENT_VANILLA
case "InstanceType.CLENT_MODDED":
return InstanceType.CLENT_MODDED
case "InstanceType.SERVER_VANILLA":
return InstanceType.SERVER_VANILLA
case "InstanceType.SERVER_MODDED":
return InstanceType.SERVER_MODDED
case _:
raise RuntimeError(f"{string} is an Incorrect Instance Type")
_DEFAULT_INST_NAME = "New Default (pieeebot neoLegacy)"
_DEFAULT_INSTALLATION_PATH = ".new-default"
_DEFAULT_USERNAME = "Steve"
_DEFAULT_ARCHIVE_FILE = "neoLegacyWindows64.zip"
_DEFAULT_EXE_NAME = "neoLegacyWindows64/Minecraft.Client.exe"
_DEFAULT_REPO_URL = "https://github.com/neoStudiosLCE/neoLegacy"
_DEFAULT_INST_SOURCE = InstanceSource.GITHUB_RELEASE
_DEFAULT_INST_TYPE = InstanceType.CLIENT_VANILLA
_DEFAULT_INST_SOURCE_STRING = "InstanceSource.GITHUB_RELEASE"
_DEFAULT_INST_TYPE_STRING = "InstanceType.CLIENT_VANILLA"
_DEFAULT_IMAGE = ":/assets/lce-hub-ico.png"
_DEFAULT_NEWS_FEED = ("https://github.com/neoStudiosLCE/neoLegacy/commits/main/")
_DEFAULT_VERSION = "v1.0.3b"
class Instance(QObject):
"""_summary_ An config and inform an instance of Minecraft LCE Installed or to install"""
def __init__(
self,
name: str = _DEFAULT_INST_NAME,
installation_path: str = _DEFAULT_INSTALLATION_PATH,
username: str = _DEFAULT_USERNAME,
exe_name: str = _DEFAULT_EXE_NAME,
archive_file: str = _DEFAULT_ARCHIVE_FILE,
repo_url: str = _DEFAULT_REPO_URL,
image: str = _DEFAULT_IMAGE,
instance_source: InstanceSource = _DEFAULT_INST_SOURCE,
instance_type: InstanceType = _DEFAULT_INST_TYPE,
news_feed: str = _DEFAULT_NEWS_FEED,
version: str = _DEFAULT_VERSION,
steam_link: str = "N/A",
) -> None:
super().__init__(parent=None, objectName="Instance")
self.name: str = name
self.installation_path: str = installation_path
self.username: str = username
self.archive_file: str = archive_file
self.exe_name: str = exe_name
self.repo_url: str = repo_url
self.instance_source: InstanceSource = instance_source
self.instance_type: InstanceType = instance_type
self.image: str = image
self.news_feed: str = news_feed
self.version: str = version
self.steam_link: str = steam_link
def load_inst_from_dict(self, inst_dict: dict[str, str]) -> None:
"""_summary_ Load A JSON to a empty Instance Object to import it
Args:
inst_dict (dict[str, str]): _description_
"""
self.name = inst_dict.get("name", _DEFAULT_INST_NAME)
self.installation_path = inst_dict.get(
"installation_path", _DEFAULT_INSTALLATION_PATH
)
self.username = inst_dict.get("username", _DEFAULT_USERNAME)
self.exe_name = inst_dict.get("exe_name", _DEFAULT_EXE_NAME)
self.archive_file = inst_dict.get("archive_file", _DEFAULT_ARCHIVE_FILE)
self.repo_url = inst_dict.get("repo_url", _DEFAULT_REPO_URL)
self.instance_source = from_str_to_InstanceSource(
inst_dict.get("instances_source", _DEFAULT_INST_SOURCE_STRING)
)
self.instance_type = from_str_to_InstanceType(
inst_dict.get("instance_type", _DEFAULT_INST_TYPE_STRING)
)
self.image = inst_dict.get("image", _DEFAULT_IMAGE)
self.news_feed = inst_dict.get("news_feed", _DEFAULT_NEWS_FEED)
self.version = inst_dict.get("version", _DEFAULT_VERSION)
self.steam_link = inst_dict.get("steam_link", "N/A")
def get_download_url(self) -> str:
"""_summary_ Get the download URL of a Instance
Raises:
RuntimeError: _description_ Error ! Local Installation does not have a download URL when trying to obtain a URL from a local installation
RuntimeError: _description_ Not implemented yet! if the state is not implemented yey.
Returns:
str: _description_
"""
download_release_url = "/releases/download/"
if (
self.instance_source == InstanceSource.GITHUB_RELEASE
or self.instance_source == InstanceSource.FORGEJO_RELEASE
):
return (
self.repo_url
+ download_release_url
+ self.version
+ "/"
+ self.archive_file
)
if self.instance_source == InstanceSource.REMOTE_GIT_SOURCE:
return f"{self.repo_url}.git"
if self.instance_source == InstanceSource.LOCAL_INSTALLATION:
raise RuntimeError(
"Error ! Local Installation does not have a download URL"
)
else:
raise RuntimeError("Not implemented yet!")
def to_dict(self) -> dict[str, str]:
dict_to_return: dict[str, str] = {
"$schema": SCHEME_VERSION,
"name": self.name,
"installation_path": self.installation_path,
"username": self.username,
"exe_name": self.exe_name,
"archive_file": self.archive_file,
"repo_url": self.repo_url,
"instance_source": self.instance_source.name,
"instance_type": self.instance_type.name,
"image": self.image,
"news_feed": self.news_feed,
"version": self.version,
"steam_link": self.version,
}
return dict_to_return
def display(self) -> None:
print(f"Name : {self.name}")
print(f"=============================")
print(f"installation path : {self.installation_path}")
print(f"username : {self.username}")
print(f"executable name : {self.exe_name}")
print(f"archive file : {self.archive_file}")
print(f"repo url : {self.repo_url}")
print(f"image : {self.image}")
print(f"instance source : {self.instance_source.value}")
print(f"instance type : {self.instance_source.value}")
print(f"news_feed : {self.news_feed}")
print(f"version : {self.version}")
print(f"steam link : {self.steam_link}")
# print(f"servers : {self.servers}")
class InstanceManager:
"""_summary_ The Manager for Instances objects"""
def __init__(self, instance: Instance, appContext: AppContext):
self.instance: Instance = instance
from lce_qt_launcher.managers.downloader import Downloader
self._downloader: Downloader = Downloader(appContext)
def play(self) -> str:
"""_summary_ Launch an Instance
Returns:
str: _description_ error message or status and exit codes
"""
return_code: int = 0
try:
client_path: str = os.path.join(self.instance.installation_path, self.instance.exe_name)
try:
game_process_temp = subprocess.run([client_path, "-name", self.instance.username])
return_code = game_process_temp.returncode
except subprocess.SubprocessError as e:
if os.name == "posix":
game_process_temp = subprocess.run(["wine", client_path, "-name", self.instance.username])
return_code = game_process_temp.returncode
else:
QMessageBox.critical(None, "Instance Error", str(e.args))
except TimeoutExpired as err:
term_service.print_error(f"process of lauching instance {self.instance.name} Failed. Reason : Timeout Expired.\n traceback : {err.with_traceback}")
return f"process of lauching instance {self.instance.name} Failed. Reason : Timeout Expired.\n traceback : {err.with_traceback}"
except PermissionError as err:
term_service.print_error(f"Cannot launch {self.instance.name}. Reason : Permission Denied.\n traceback : {err.with_traceback}")
return f"Cannot launch {self.instance.name}. Reason : Permission Denied.\n traceback : {err.with_traceback}"
else:
return f"Client closed with code {return_code}"
def install_instance(self) -> QNetworkReply | str:
"""_summary_ Install the selected Instance
Raises:
RuntimeWarning: _description_ #TODO : TO REMOVE, no good Reason to be there
e: _description_ #TODO : TO REMOVE, no good Reason to be there
Returns:
QNetworkReply: _description_ : the QtNetwork Reply Object of the download process
"""
try:
if self.instance.instance_source in [
InstanceSource.GITHUB_RELEASE,
InstanceSource.FORGEJO_RELEASE,
]:
return self._downloader.download_inst_async(self.instance)
if self.instance.instance_source == InstanceSource.REMOTE_GIT_SOURCE:
subprocess.run(["git", "clone", self.instance.repo_url, self.instance.installation_path])
return "git_operation_finished"
else:
raise RuntimeWarning(
"Not implemented YET"
) # TODO : implementing other type
except RuntimeError as e:
raise e
def save_instance(self, save_file: str) -> None:
"""_summary_ Save the install with a specified save file and location
Args:
save_file (str): _description_ specified save file and location
"""
full_save_file = save_file
json_string: str = ""
try:
json_string = json.dumps(
vars(self.instance),
indent=4,
)
except:
json_string = json.dumps(obj=vars(self.instance), indent=4, default=str)
if not save_file.endswith(instance_extension_str):
full_save_file: str = save_file + instance_extension_str
with open(file=full_save_file, mode="w") as f:
_ = f.write(json_string)
def load_instance(self, save_file: str) -> None:
"""_summary_ Load an instance with a specified save file and location
#TODO : Check for the any report
Args:
save_file (str): _description_ specified save file and location
"""
with open(file=save_file, mode="r") as json_file:
json_data = json.load(json_file)
inst_dict: dict[str, str] = json_data
self.instance.load_inst_from_dict(inst_dict)
def is_installable(self) -> bool:
"""_summary_ #TODO"""
# Note : Right now the remote git location is not installable via this launcher, it will added in the next version
if self.instance.instance_source in [
InstanceSource.FORGEJO_RELEASE,
InstanceSource.GITHUB_RELEASE
]: return True
else:
return False