From 8b6683475a1ccb2cd502d8e3172e1a0195ff88ac Mon Sep 17 00:00:00 2001 From: Nix <75538775+nixietab@users.noreply.github.com> Date: Wed, 10 Sep 2025 03:50:59 -0300 Subject: [PATCH] Initial commit of the replace --- healthcheck.py | 140 ++++++++++++++++++++++++++++++++++++++----- modulecli.py | 26 ++++++-- picodulce.py | 158 +++++++++++++++++++++++++++++++++++-------------- 3 files changed, 257 insertions(+), 67 deletions(-) diff --git a/healthcheck.py b/healthcheck.py index a0aeb6f..08e59c1 100644 --- a/healthcheck.py +++ b/healthcheck.py @@ -1,6 +1,54 @@ import os import json -import requests +import shutil +import modulecli +from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QLabel, QProgressBar +from PyQt5.QtCore import Qt, QThread, pyqtSignal +import sys + +class CopyThread(QThread): + progress_changed = pyqtSignal(int) + finished = pyqtSignal() + + def __init__(self, src_dir, dst_dir): + super().__init__() + self.src_dir = src_dir + self.dst_dir = dst_dir + + def run(self): + # Gather all files recursively + files_to_copy = [] + for root, dirs, files in os.walk(self.src_dir): + for f in files: + full_path = os.path.join(root, f) + relative_path = os.path.relpath(full_path, self.src_dir) + files_to_copy.append(relative_path) + + total_files = len(files_to_copy) + copied_files = 0 + + for relative_path in files_to_copy: + src_path = os.path.join(self.src_dir, relative_path) + dst_path = os.path.join(self.dst_dir, relative_path) + dst_folder = os.path.dirname(dst_path) + + if not os.path.exists(dst_folder): + try: + os.makedirs(dst_folder) + except PermissionError: + print(f"Skipping folder {dst_folder} (permission denied)") + continue + + try: + shutil.copy2(src_path, dst_path) + except PermissionError: + print(f"Skipping file {dst_path} (permission denied)") + + copied_files += 1 + progress_percent = int((copied_files / total_files) * 100) + self.progress_changed.emit(progress_percent) + + self.finished.emit() class HealthCheck: @@ -22,47 +70,112 @@ class HealthCheck: "ThemeRepository": "https://raw.githubusercontent.com/nixietab/picodulce-themes/main/repo.json", "Locale": "en", "ManageJava": False, - "MaxRAM": 2, - "JavaPath": "" + "MaxRAM": "2G", + "JavaPath": "", + "ZucaroCheck": False, } - # Step 1: Check if the file exists; if not, create it with default values if not os.path.exists(config_path): with open(config_path, "w") as config_file: json.dump(default_config, config_file, indent=4) self.config = default_config return - # Step 2: Try loading the config file, handle invalid JSON try: with open(config_path, "r") as config_file: self.config = json.load(config_file) except (json.JSONDecodeError, ValueError): - # File is corrupted, overwrite it with default configuration with open(config_path, "w") as config_file: json.dump(default_config, config_file, indent=4) self.config = default_config return - # Step 3: Check for missing keys and add defaults if necessary updated = False for key, value in default_config.items(): - if key not in self.config: # Field is missing + if key not in self.config: self.config[key] = value updated = True - # Step 4: Save the repaired config back to the file if updated: with open(config_path, "w") as config_file: json.dump(self.config, config_file, indent=4) + def get_folder_size(self, folder_path): + total_size = 0 + for dirpath, dirnames, filenames in os.walk(folder_path): + for f in filenames: + fp = os.path.join(dirpath, f) + if os.path.isfile(fp): + total_size += os.path.getsize(fp) + return total_size + + def zucaro_health_check(self): + if self.config.get("ZucaroCheck"): + return + + output = modulecli.run_command("instance dir").strip() + instance_dir = os.path.abspath(output) + base_dir = os.path.abspath(os.path.join(instance_dir, "..", "..")) + + possible_zucaro = [os.path.join(base_dir, "zucaro"), os.path.join(base_dir, ".zucaro")] + possible_picomc = [os.path.join(base_dir, "picomc"), os.path.join(base_dir, ".picomc")] + + zucaro_dir = next((d for d in possible_zucaro if os.path.exists(d)), None) + picomc_dir = next((d for d in possible_picomc if os.path.exists(d)), None) + + if picomc_dir is None or zucaro_dir is None: + print("Required directories not found. Skipping copy.") + # Mark the check as done so it wont run again + self.config["ZucaroCheck"] = True + with open("config.json", "w") as f: + json.dump(self.config, f, indent=4) + return + + picomc_size = self.get_folder_size(picomc_dir) + zucaro_size = self.get_folder_size(zucaro_dir) + + if picomc_size <= zucaro_size: + print("No action needed. Zucaro folder is not smaller than Picomc.") + # Update config so the check is considered done + self.config["ZucaroCheck"] = True + with open("config.json", "w") as f: + json.dump(self.config, f, indent=4) + return + + print(f"Copying Picomc ({picomc_size} bytes) to Zucaro ({zucaro_size} bytes)...") + + app = QApplication.instance() or QApplication(sys.argv) + dialog = QDialog() + dialog.setWindowTitle("Working...") + dialog.setWindowModality(Qt.ApplicationModal) + layout = QVBoxLayout() + label = QLabel("Working on stuff, please wait...") + progress = QProgressBar() + progress.setValue(0) + layout.addWidget(label) + layout.addWidget(progress) + dialog.setLayout(layout) + + # Setup copy thread + thread = CopyThread(picomc_dir, zucaro_dir) + thread.progress_changed.connect(progress.setValue) + thread.finished.connect(dialog.accept) + thread.start() + + dialog.exec_() # Runs the modal event loop + + # Mark as done + self.config["ZucaroCheck"] = True + with open("config.json", "w") as f: + json.dump(self.config, f, indent=4) + + print("Copy completed.") + def themes_integrity(self): - # Define folder and file paths themes_folder = "themes" dark_theme_file = os.path.join(themes_folder, "Dark.json") native_theme_file = os.path.join(themes_folder, "Native.json") - # Define the default content for Dark.json dark_theme_content = { "manifest": { "name": "Dark", @@ -88,7 +201,6 @@ class HealthCheck: "background_image_base64": "" } - # Define the default content for Native.json native_theme_content = { "manifest": { "name": "Native", @@ -99,25 +211,21 @@ class HealthCheck: "palette": {} } - # Step 1: Ensure the themes folder exists if not os.path.exists(themes_folder): print(f"Creating folder: {themes_folder}") os.makedirs(themes_folder) - # Step 2: Ensure Dark.json exists if not os.path.isfile(dark_theme_file): print(f"Creating file: {dark_theme_file}") with open(dark_theme_file, "w", encoding="utf-8") as file: json.dump(dark_theme_content, file, indent=2) print("Dark.json has been created successfully.") - # Step 3: Ensure Native.json exists if not os.path.isfile(native_theme_file): print(f"Creating file: {native_theme_file}") with open(native_theme_file, "w", encoding="utf-8") as file: json.dump(native_theme_content, file, indent=2) print("Native.json has been created successfully.") - # Check if both files exist and print OK message if os.path.isfile(dark_theme_file) and os.path.isfile(native_theme_file): print("Theme Integrity OK") diff --git a/modulecli.py b/modulecli.py index 8c54811..621be87 100644 --- a/modulecli.py +++ b/modulecli.py @@ -1,16 +1,27 @@ -import click -from picomc.cli.main import picomc_cli from io import StringIO import sys +import shlex +import gc + +def run_command(command="zucaro"): + # Remove all zucaro-related modules from sys.modules BEFORE import + modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')] + for mod in modules_to_remove: + del sys.modules[mod] + gc.collect() + + # Import zucaro_cli dynamically + from zucaro.cli.main import zucaro_cli -def run_command(command="picomc"): # Redirect stdout and stderr to capture the command output old_stdout, old_stderr = sys.stdout, sys.stderr sys.stdout = mystdout = StringIO() sys.stderr = mystderr = StringIO() try: - picomc_cli.main(args=command.split()) + # Use shlex.split to properly parse the command string + # This will call Click's CLI as if from command line, using args + zucaro_cli.main(args=shlex.split(command)) except SystemExit as e: if e.code != 0: print(f"Command exited with code {e.code}", file=sys.stderr) @@ -23,8 +34,13 @@ def run_command(command="picomc"): output = mystdout.getvalue().strip() error = mystderr.getvalue().strip() + + # Cleanup: remove zucaro-related modules from sys.modules and force garbage collection + modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')] + for mod in modules_to_remove: + del sys.modules[mod] + gc.collect() if not output: return f"Error: No output from command. Stderr: {error}" - return output \ No newline at end of file diff --git a/picodulce.py b/picodulce.py index 9fd98b5..e3b182a 100644 --- a/picodulce.py +++ b/picodulce.py @@ -15,7 +15,7 @@ from authser import MinecraftAuthenticator from healthcheck import HealthCheck import modulecli -from PyQt5.QtWidgets import QApplication, QComboBox, QWidget, QInputDialog, QVBoxLayout, QListWidget, QPushButton, QMessageBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QTabWidget, QFrame, QSpacerItem, QSizePolicy, QMainWindow, QGridLayout, QTextEdit, QListWidget, QListWidgetItem, QMenu +from PyQt5.QtWidgets import QApplication, QComboBox, QWidget, QInputDialog, QVBoxLayout, QListWidget, QSpinBox, QFileDialog, QPushButton, QMessageBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QTabWidget, QFrame, QSpacerItem, QSizePolicy, QMainWindow, QGridLayout, QTextEdit, QListWidget, QListWidgetItem, QMenu from PyQt5.QtGui import QFont, QIcon, QColor, QPalette, QMovie, QPixmap, QDesktopServices, QBrush from PyQt5.QtCore import Qt, QObject, pyqtSignal, QThread, QUrl, QMetaObject, Q_ARG, QByteArray, QSize from datetime import datetime @@ -31,6 +31,7 @@ class PicomcVersionSelector(QWidget): health_checker = HealthCheck() health_checker.themes_integrity() health_checker.check_config_file() + health_checker.zucaro_health_check() self.config = health_checker.config themes_folder = "themes" @@ -296,21 +297,17 @@ class PicomcVersionSelector(QWidget): def open_settings_dialog(self): dialog = QDialog(self) dialog.setWindowTitle('Settings') - - # Make the window resizable dialog.setMinimumSize(400, 300) - # Create a Tab Widget tab_widget = QTabWidget() - # Create the Settings Tab + # --- Settings Tab --- settings_tab = QWidget() settings_layout = QVBoxLayout() title_label = QLabel('Settings') title_label.setFont(QFont("Arial", 14)) - # Create checkboxes for settings tab discord_rcp_checkbox = QCheckBox('Discord Rich Presence') discord_rcp_checkbox.setChecked(self.config.get("IsRCPenabled", False)) @@ -326,7 +323,6 @@ class PicomcVersionSelector(QWidget): settings_layout.addWidget(check_updates_checkbox) settings_layout.addWidget(bleeding_edge_checkbox) - # Add buttons in the settings tab update_button = QPushButton('Check for updates') update_button.clicked.connect(self.check_for_update) @@ -342,53 +338,99 @@ class PicomcVersionSelector(QWidget): settings_tab.setLayout(settings_layout) - # Create the Customization Tab + # --- Customization Tab --- customization_tab = QWidget() customization_layout = QVBoxLayout() - # Create theme background checkbox for customization tab theme_background_checkbox = QCheckBox('Theme Background') theme_background_checkbox.setChecked(self.config.get("ThemeBackground", False)) - # Label to show currently selected theme theme_filename = self.config.get('Theme', 'Dark.json') current_theme_label = QLabel(f"Current Theme: {theme_filename}") - # QListWidget to display available themes json_files_label = QLabel('Installed Themes:') self.json_files_list_widget = QListWidget() - - # Track selected theme - self.selected_theme = theme_filename # Default to current theme - - # Build the list of themes + self.selected_theme = theme_filename themes_list = self.build_themes_list() - - # Populate themes initially self.populate_themes(self.json_files_list_widget, themes_list) - - # Update current theme label when a theme is selected self.json_files_list_widget.itemClicked.connect( lambda: self.on_theme_selected(self.json_files_list_widget, current_theme_label) ) - # Add widgets to the layout customization_layout.addWidget(theme_background_checkbox) customization_layout.addWidget(current_theme_label) customization_layout.addWidget(json_files_label) customization_layout.addWidget(self.json_files_list_widget) - # Button to download themes download_themes_button = QPushButton("Download More Themes") download_themes_button.clicked.connect(self.download_themes_window) - customization_layout.addWidget(download_themes_button) customization_tab.setLayout(customization_layout) - # Add the tabs to the TabWidget + # --- Java Tab --- + java_tab = QWidget() + java_layout = QVBoxLayout() + + # Java path input with browse button + java_path_layout = QHBoxLayout() + java_path_input = QLineEdit() + java_path_input.setPlaceholderText("Custom Java Installation Path") + java_path_input.setText(self.config.get("JavaPath", "")) + browse_button = QPushButton("Examine") + browse_button.clicked.connect(lambda: self.browse_java_path(java_path_input)) + java_path_layout.addWidget(java_path_input) + java_path_layout.addWidget(browse_button) + + ram_layout = QHBoxLayout() + ram_label = QLabel("Assigned RAM:") + ram_selector = QLineEdit() + ram_selector.setPlaceholderText("2G") # Show default placeholder + + # RAM selector + ram_layout = QHBoxLayout() + ram_label = QLabel("Assigned RAM:") + ram_selector = QLineEdit() + ram_selector.setPlaceholderText("2G") # Show default placeholder + + # Set initial value from config, ensuring it ends with 'G' + initial_ram = self.config.get("MaxRAM", "2G") + if not initial_ram.endswith('G'): + initial_ram += 'G' + ram_selector.setText(initial_ram) + + # Ensure 'G' is always present when focus is lost + def ensure_g_suffix(): + current_text = ram_selector.text() + if not current_text.endswith('G'): + ram_selector.setText(current_text + 'G') + + ram_selector.editingFinished.connect(ensure_g_suffix) + + ram_layout.addWidget(ram_label) + ram_layout.addWidget(ram_selector) + + # Manage Java checkbox + manage_java_checkbox = QCheckBox("Manage Java") + manage_java_checkbox.setChecked(self.config.get("ManageJava", False)) + manage_java_info = QLabel( + "Disclaimer: Experimental feature. Do not change these settings " + "unless you are sure of what you are doing. " + " If Manage Java is enabledthe launcher will download Java binaries for your OS only for Minecraft compatibility purposes.") + manage_java_info.setWordWrap(True) + + # Add to layout + java_layout.addLayout(java_path_layout) + java_layout.addLayout(ram_layout) + java_layout.addWidget(manage_java_checkbox) + java_layout.addWidget(manage_java_info) + + java_tab.setLayout(java_layout) + + # Add all tabs tab_widget.addTab(settings_tab, "Settings") tab_widget.addTab(customization_tab, "Customization") + tab_widget.addTab(java_tab, "Java") # Save button save_button = QPushButton('Save') @@ -398,11 +440,13 @@ class PicomcVersionSelector(QWidget): check_updates_checkbox.isChecked(), theme_background_checkbox.isChecked(), self.selected_theme, - bleeding_edge_checkbox.isChecked() + bleeding_edge_checkbox.isChecked(), + java_path_input.text(), + ram_selector.text(), + manage_java_checkbox.isChecked() ) ) - # Main layout main_layout = QVBoxLayout() main_layout.addWidget(tab_widget) main_layout.addWidget(save_button) @@ -410,6 +454,13 @@ class PicomcVersionSelector(QWidget): dialog.setLayout(main_layout) dialog.exec_() + def browse_java_path(self, java_path_input): + path, _ = QFileDialog.getOpenFileName(self, "Select Java Executable") + if path: + java_path_input.setText(path) + + + def show_bleeding_edge_popup(self, checkbox): if checkbox.isChecked(): response = QMessageBox.question( @@ -638,20 +689,31 @@ class PicomcVersionSelector(QWidget): ## REPOSITORY BLOCK ENDS - def save_settings(self, is_rcp_enabled, check_updates_on_start, theme_background, selected_theme, is_bleeding): + def save_settings( + self, + is_rcp_enabled, + check_updates_on_start, + theme_background, + selected_theme, + is_bleeding, + java_path, + ram_allocation, + manage_java_enabled + ): config_path = "config.json" updated_config = { "IsRCPenabled": is_rcp_enabled, "CheckUpdate": check_updates_on_start, "ThemeBackground": theme_background, "Theme": selected_theme, - "IsBleeding": is_bleeding + "IsBleeding": is_bleeding, + "ManageJava": manage_java_enabled, + "MaxRAM": ram_allocation, + "JavaPath": java_path, } - # Update config values self.config.update(updated_config) - # Save updated config to file with open(config_path, "w") as config_file: json.dump(self.config, config_file, indent=4) @@ -830,40 +892,44 @@ class PicomcVersionSelector(QWidget): def run_game(self, selected_instance): try: - # Set current_state to the selected instance self.current_state = selected_instance self.start_time = time.time() - # Read the config.json to get the "Instance" value + # Read config with open('config.json', 'r') as config_file: config = json.load(config_file) - instance_value = config.get("Instance", "default") # Default to "default" if not found + instance_value = config.get("Instance", "default") + max_ram = config.get("MaxRAM", 2) + manage_java = config.get("ManageJava", False) + java_path = config.get("JavaPath", "") - # Update lastplayed field in config.json on a separate thread + # Update last played on a thread update_thread = threading.Thread(target=self.update_last_played, args=(selected_instance,)) update_thread.start() - # Run the game using the modulecli module - command = f"instance launch --version-override {selected_instance} {instance_value}" + # Build command + command = f"instance launch {instance_value} --version-override {selected_instance} --assigned-ram {max_ram}" + if manage_java: + command += " --manage-java" + if java_path: + command += f" --java {java_path}" + + print(f"Launching command: {command}") + output = modulecli.run_command(command) - + print(f"modulecli output: {output}") if not output: raise Exception("Failed to get output from modulecli") except Exception as e: error_message = f"Error playing {selected_instance}: {e}" + print(error_message) # Add this for debugging logging.error(error_message) - # Use QMetaObject.invokeMethod to call showError safely - QMetaObject.invokeMethod( - self, "showError", Qt.QueuedConnection, - Q_ARG(str, "Error"), Q_ARG(str, error_message) - ) + # (Show error in UI if necessary) finally: - # Reset current_state to "menu" after the game closes self.current_state = "menu" self.update_total_playtime(self.start_time) - - + def update_last_played(self, selected_instance): config_path = "config.json" self.config["LastPlayed"] = selected_instance @@ -1137,7 +1203,7 @@ class PicomcVersionSelector(QWidget): about_message = ( f"PicoDulce Launcher (v{version_number})\n\n" - "A simple Minecraft launcher built using Qt, based on the picomc project.\n\n" + "A simple Minecraft launcher built using Qt, based on the zucaro backend.\n\n" "Credits:\n" "Nixietab: Code and UI design\n" "Wabaano: Graphic design\n"