From a93641b26ddbf0f245a4f92af124b8faf3393559 Mon Sep 17 00:00:00 2001 From: Wei Hong Date: Fri, 10 Jul 2026 23:27:21 +0800 Subject: [PATCH] Update many files. Add some description about this project, and now it has a simple UI (made with Qt) If you don't specify the mode of (interface?) the UI. Launcher will show a crude window that lets you select a mode to start the (real) launcher. Test version data fetching functions. Looks great. --- README.md | 1 + __init__.py | 18 ++++++ app.py | 79 +++++++++++++++++++++++- core_lib/__init__.py | 1 + core_lib/core_init.py | 3 + core_lib/log/default.py | 23 +++++++ core_lib/qt/hack.py | 10 +++ core_lib/unclassified.py | 127 ++++++++++++++++++++++++++++++++++++--- main.py | 31 +++++++--- 9 files changed, 274 insertions(+), 19 deletions(-) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 core_lib/__init__.py create mode 100644 core_lib/core_init.py create mode 100644 core_lib/log/default.py create mode 100644 core_lib/qt/hack.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e9ce2e --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# TestLauncher \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..2820700 --- /dev/null +++ b/__init__.py @@ -0,0 +1,18 @@ +__version__ = "0.0.1" +__author__ = "wei" +__email__ = "weidnite1222@gmail.com" +__description__ = """ +A *Minecraft* launcher that I'm still thinking about its name. + +I made this due to the official Minecraft Launcher is too huge and laggy. I need to wait it over 1 min to launch +game, and I also think that other launchers are too complicated for most people to use. + +This launcher should adheres to the following rules: + +1: Be simple +2: Be complex at some point (Yeah this rule looks opposite to the first rule) +3: Be cool and fun +4: No AI slop (Nobody wants to read spaghetti code that made by AI) +5: Freedom just like the bird +""" +__website__ = "https://repo.weispace.net/wei/Launcher" \ No newline at end of file diff --git a/app.py b/app.py index 92f5356..d4ad845 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,12 @@ -from PySide6.QtWidgets import QApplication, QWidget, QMainWindow +import logging +import textwrap -from core_lib.unclassified import move_window_to_center +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QDialog, QPlainTextEdit, \ + QHBoxLayout +from core_lib.qt.hack import move_window_to_center +from core_lib.unclassified import get_latest_version, get_version_manifest, get_specific_version_manifest_url, \ + fetch_specific_version_manifest, find_useful_part_in_specific_version_manifest class Launcher(QApplication): @@ -12,6 +18,73 @@ class Launcher(QApplication): move_window_to_center(self.main_window) + self.logger = logging.getLogger("Launcher.MainWindow") + + self.test_dialog = self.TestDialog(self.main_window) + self.test_dialog.show() + + class TestDialog(QDialog): + def __init__(self, parent): + super().__init__(parent) + self.parent = parent + self.resize(300, 200) + self.setWindowFlags(self.windowFlags() | Qt.WindowMaximizeButtonHint) + + self.button = QPushButton("Fetch Version Data") + self.button.setObjectName("ActionButton") + self.button.clicked.connect(self.run_test) + + self.output = QPlainTextEdit() + self.output.setReadOnly(True) + + self.layout = QHBoxLayout() + self.layout.addWidget(self.output) + + self.right_layout = QVBoxLayout() + self.right_layout.setObjectName("RightLayout") + self.right_layout.setAlignment(Qt.AlignmentFlag.AlignRight) + self.right_layout.addWidget(self.button) + self.right_layout.addStretch() + self.right_layout.setContentsMargins(0, 10, 0, 0) + + self.layout.addLayout(self.right_layout) + + self.setLayout(self.layout) + + self.setStyleSheet( + textwrap.dedent(""" + QPushButton#ActionButton { + padding: 6px 12px; + } + """)) + + @property + def app(self): + return self.parent.parent + + def run_test(self): + data = get_version_manifest() + ver = get_latest_version(data) + ver_data = fetch_specific_version_manifest(data, ver) + arguments, asset_index, downloads, java_version, libraries, main_class = ( + find_useful_part_in_specific_version_manifest(ver_data)) + + content = textwrap.dedent(f"""\ + Arguments: {arguments}\n + + Asset: {asset_index}\n + + Downloads: {downloads}\n + + Java Version: {java_version}\n + + Libraries: {libraries}\n + + Main Class: {main_class}\n + """) + + self.output.setPlainText(content) + def exec(self): self.main_window.show() - self.exec_() \ No newline at end of file + self.exec_() diff --git a/core_lib/__init__.py b/core_lib/__init__.py new file mode 100644 index 0000000..541f859 --- /dev/null +++ b/core_lib/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1.0' \ No newline at end of file diff --git a/core_lib/core_init.py b/core_lib/core_init.py new file mode 100644 index 0000000..cc28335 --- /dev/null +++ b/core_lib/core_init.py @@ -0,0 +1,3 @@ +import logging + +logger = logging.getLogger("Launcher.CoreLib") \ No newline at end of file diff --git a/core_lib/log/default.py b/core_lib/log/default.py new file mode 100644 index 0000000..788e20a --- /dev/null +++ b/core_lib/log/default.py @@ -0,0 +1,23 @@ +import logging +import sys + +from colorlog import ColoredFormatter + +DEFAULT_FORMAT = '%(asctime)s:%(name)s:%(levelname)s: %(message)s' +DEFAULT_DATE_FORMAT = '%H:%M:%S' + +def get_colorful_handler(): + handler = logging.StreamHandler(sys.stdout) + formatter = ColoredFormatter( + '%(light_white)s%(asctime)s:%(light_green)s%(name)s:%(log_color)s%(levelname)s%(reset)s: %(message)s', + log_colors={ + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'light_red', + 'CRITICAL': 'red', + }, + datefmt=DEFAULT_DATE_FORMAT + ) + handler.setFormatter(formatter) + return handler \ No newline at end of file diff --git a/core_lib/qt/hack.py b/core_lib/qt/hack.py new file mode 100644 index 0000000..0afd46a --- /dev/null +++ b/core_lib/qt/hack.py @@ -0,0 +1,10 @@ +from PySide6.QtGui import QScreen +from PySide6.QtWidgets import QMainWindow, QApplication + + +def move_window_to_center(window: QMainWindow): + main_screen = QApplication.primaryScreen() + center = QScreen.availableGeometry(main_screen).center() + geometry = window.frameGeometry() + geometry.moveCenter(center) + window.move(geometry.topLeft()) diff --git a/core_lib/unclassified.py b/core_lib/unclassified.py index 05ae8dd..e1b5eba 100644 --- a/core_lib/unclassified.py +++ b/core_lib/unclassified.py @@ -1,10 +1,121 @@ -from PySide6.QtGui import QScreen -from PySide6.QtWidgets import QMainWindow, QApplication +import json + +import requests +from .core_init import logger + +MOJANG_VERSION_MANIFEST_V2 = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json" + +def get_version_manifest(manifest_url=MOJANG_VERSION_MANIFEST_V2, timeout=10) -> dict | None: + logger.debug("Getting version manifest from {}".format(manifest_url)) + + try: + r = requests.get(manifest_url, timeout=timeout) + except requests.exceptions.RequestException as e: + logger.error("Error getting version manifest from {}".format(manifest_url)) + logger.error("Exception: {}".format(e)) + return None + + try: + data = r.json() + logger.debug("VersionManifest: {}".format(json.dumps(data, indent=4))) + return data + except ValueError as e: + logger.error("Unable to deserialize response data into dictionary.") + logger.error("URL: {}".format(manifest_url)) + logger.error("Exception: {}".format(e)) + return None + +def get_latest_version(version_manifest: dict, is_snapshot=False) -> None | str: + latest = version_manifest.get("latest", {}) + + if len(latest) == 0: + logger.debug("No latest version section available in version manifest.") + logger.debug("Data: {}".format(json.dumps(version_manifest, indent=4))) + return None + + latest_ver = latest.get("release" if not is_snapshot else "snapshot", None) + + if latest_ver is None: + logger.debug("No latest version available.") + else: + logger.debug("Latest version: {}".format(latest_ver)) + + return latest_ver + +def get_specific_version_manifest_url(version_manifest: dict, + version_id: str, is_snapshot=False) -> str | None: + versions = version_manifest.get("versions", []) + + if len(versions) == 0: + logger.debug("No version section available in version manifest.") + return None + + ver_type = "release" if not is_snapshot else "snapshot" + versions = [version for version in versions if version.get("type") == ver_type] + + for version in versions: + if version.get("id") == version_id: + specific_ver_manifest_url = version.get("url", None) + + if specific_ver_manifest_url is None: + logger.debug("Specific version manifest URL not available in version manifest.") + return None + + return specific_ver_manifest_url + + logger.debug("No version section available in version manifest.") + + return None + +def fetch_specific_version_manifest(version_manifest: dict, spec_version: str) -> dict | None: + ver_url = get_specific_version_manifest_url(version_manifest, spec_version) + + if ver_url is None: + logger.debug("Unable to get specific version manifest url from version manifest.\n" + "Version: {}".format(spec_version)) + return None + + try: + r = requests.get(ver_url, timeout=10) + except requests.exceptions.RequestException as e: + logger.error("Error getting version manifest from {}".format(ver_url)) + logger.error("Exception: {}".format(e)) + return None + + try: + data = r.json() + logger.debug("SpecificVersionManifest: {}".format(json.dumps(data, indent=4))) + return data + except ValueError as e: + logger.error("Unable to deserialize response data into dictionary.") -def move_window_to_center(window: QMainWindow): - main_screen = QApplication.primaryScreen() - center = QScreen.availableGeometry(main_screen).center() - geometry = window.frameGeometry() - geometry.moveCenter(center) - window.move(geometry.topLeft()) \ No newline at end of file +def find_useful_part_in_specific_version_manifest(spec_version_manifest: dict)\ + -> tuple[dict | None, dict | None, dict | None, dict | None, list | None, str | None]: + arguments = spec_version_manifest.get("arguments", {}) + asset_index = spec_version_manifest.get("assetIndex", {}) + downloads = spec_version_manifest.get("downloads", {}) + java_version = spec_version_manifest.get("java_version", {}) + libraries = spec_version_manifest.get("libraries", []) + main_class = spec_version_manifest.get("mainClass", None) + + if len(arguments) == 0: + logger.debug("No arguments section available in version manifest.") + + if len(asset_index) == 0: + logger.debug("No asset index section available in version manifest.") + + if len(downloads) == 0: + logger.debug("No downloads section available in version manifest.") + + if len(java_version) == 0: + logger.debug("No java version section available in version manifest.") + + if len(libraries) == 0: + logger.debug("No libraries section available in version manifest.") + + if main_class is None: + logger.debug("No main class section available in version manifest.") + + return arguments, asset_index, downloads, java_version, libraries, main_class + diff --git a/main.py b/main.py index 55680ce..7e98e0f 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,12 @@ -import os import sys import argparse import logging + from app import Launcher import tkinter as tk -# Logger configure -logging.basicConfig(format='%(asctime)s:%(name)s:%(levelname)s: %(message)s', - level=logging.INFO, - datefmt='%H:%M:%S') +from core_lib.log.default import DEFAULT_FORMAT, DEFAULT_DATE_FORMAT, get_colorful_handler + def select_mode() -> None | str: value = None @@ -49,26 +47,44 @@ def tui_entry(): logger.info("Wow tui is unfinished") def main(): + root_logger = logging.getLogger() logger = logging.getLogger("Launcher.Main") + # parse arguments parser = argparse.ArgumentParser() parser.add_argument("-m", "--mode", choices=['lightweight', 'full', 'choice'], default='choice', help="Select mode (lightweight for tui, full is gui," "choice for a mode select window)", required=False) + parser.add_argument("-d", "--debug", action="store_true", + help="Enable debug mode", default=False) args, unknown = parser.parse_known_args() for arg in unknown: - logging.warning("Unknown argument '%s'" % arg) + logger.warning("Unknown argument '%s'" % arg) + # args value set if not hasattr(args, "mode") or args.mode == "choice": mode = select_mode() if mode is None: - logging.warning("No mode selected. Exiting...") + logger.warning("No mode selected. Exiting...") sys.exit(0) else: mode = args.mode + debug = args.debug + + # Init lib + level = logging.DEBUG if debug else logging.INFO + + # Set root logger level and configure handler for colorful log (not sure why I added this) + root_logger.setLevel(level) + root_logger.addHandler(get_colorful_handler()) + + # Logger configure + logging.basicConfig(format=DEFAULT_FORMAT, datefmt=DEFAULT_DATE_FORMAT, level=level) + + # Start (the real) launcher if mode == 'lightweight': sys.exit(tui_entry()) elif mode == 'full': @@ -78,6 +94,5 @@ def main(): logger.error("Unknown mode '%s'" % mode) sys.exit(1) - if __name__ == '__main__': main()