· 5 months ago · Apr 29, 2025, 02:10 PM
1import subprocess
2import sys
3import importlib.metadata
4import tkinter as tk
5from tkinter import ttk, messagebox, scrolledtext, filedialog, simpledialog
6import requests
7import os
8import json
9import threading
10import re
11import hashlib
12import pyperclip
13import zipfile
14import tarfile # Added for .tar.gz archives (Modrinth mods)
15from datetime import datetime
16import platform
17import webbrowser
18from packaging import version as pkg_version
19import humanize
20from colorama import init, Fore, Style
21from PIL import Image, ImageTk
22import time # For tooltips
23
24# Inicjalizacja colorama
25init(autoreset=True)
26
27# --- Utility: Tooltip ---
28class Tooltip:
29 def __init__(self, widget, text):
30 self.widget = widget
31 self.text = text
32 self.tooltip_window = None
33 self.widget.bind("<Enter>", self.show_tooltip)
34 self.widget.bind("<Leave>", self.hide_tooltip)
35 self.id = None
36
37 def show_tooltip(self, event=None):
38 self.id = self.widget.after(500, self._show) # Delay tooltip appearance
39
40 def hide_tooltip(self, event=None):
41 if self.id:
42 self.widget.after_cancel(self.id)
43 self._hide()
44
45 def _show(self):
46 if self.tooltip_window:
47 return
48 x, y, cx, cy = self.widget.bbox("insert")
49 x += self.widget.winfo_rootx() + 25
50 y += self.widget.winfo_rooty() + 20
51
52 self.tooltip_window = tk.Toplevel(self.widget)
53 self.tooltip_window.wm_overrideredirect(True) # Hide window borders
54 self.tooltip_window.wm_geometry(f"+{x}+{y}")
55
56 label = tk.Label(self.tooltip_window, text=self.text, background="#ffffc0", relief="solid", borderwidth=1, font=("tahoma", "8", "normal"))
57 label.pack()
58
59 def _hide(self):
60 if self.tooltip_window:
61 self.tooltip_window.destroy()
62 self.tooltip_window = None
63# --- End Tooltip ---
64
65# Instalacja zależności (przeniesione na początek skryptu, ale po importach)
66def install_requirements():
67 required_libraries = ['requests', 'pyperclip', 'packaging', 'humanize', 'colorama', 'pillow']
68 print(f"{Fore.CYAN}Sprawdzanie zależności...{Style.RESET_ALL}")
69 for library in required_libraries:
70 try:
71 importlib.metadata.version(library)
72 print(f"{Fore.GREEN}[OK] {library} już zainstalowane.")
73 except importlib.metadata.PackageNotFoundError:
74 try:
75 print(f"{Fore.YELLOW}[INFO] Instaluję {library}...")
76 subprocess.check_call([sys.executable, "-m", "pip", "install", library], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
77 print(f"{Fore.CYAN}[INFO] {library} zainstalowane.")
78 except subprocess.CalledProcessError as e:
79 print(f"{Fore.RED}[ERROR] Nie udało się zainstalować {library}. Błąd: {e.stderr.decode().strip()}")
80 except Exception as e:
81 print(f"{Fore.RED}[ERROR] Nieznany błąd podczas instalacji {library}: {e}")
82 print(f"{Fore.CYAN}Zakończono sprawdzanie zależności.{Style.RESET_ALL}")
83
84install_requirements()
85
86# Ścieżki
87BASE_DIR = os.path.join(os.getcwd(), "minecraft_launcher_data") # Changed base dir to avoid conflicts
88CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
89ASSETS_DIR = os.path.join(BASE_DIR, "assets")
90LIBRARIES_DIR = os.path.join(BASE_DIR, "libraries")
91NATIVES_DIR = os.path.join(BASE_DIR, "natives")
92LOGS_DIR = os.path.join(BASE_DIR, "logs")
93JAVA_DIR = os.path.join(BASE_DIR, "java")
94ICONS_DIR = os.path.join(BASE_DIR, "icons")
95# Ensure icons directory exists if we plan to use custom icons
96os.makedirs(ICONS_DIR, exist_ok=True)
97
98# Kolory i styl (Ulepszone nazewnictwo)
99PRIMARY_BG = "#1a1a1a"
100SECONDARY_BG = "#2a2a2a"
101TERTIARY_BG = "#3a3a3a"
102PRIMARY_FG = "#ffffff"
103ACCENT_COLOR = "#2a9fd6" # Blue
104SUCCESS_COLOR = "#5cb85c" # Green
105ERROR_COLOR = "#d9534f" # Red
106WARNING_COLOR = "#f0ad4e" # Yellow
107INFO_COLOR = "#337ab7" # Light Blue
108
109CONSOLE_BG = "#0d0d0d"
110CONSOLE_FG_DEFAULT = "#cccccc"
111CONSOLE_FG_INFO = INFO_COLOR
112CONSOLE_FG_SUCCESS = SUCCESS_COLOR
113CONSOLE_FG_WARNING = WARNING_COLOR
114CONSOLE_FG_ERROR = ERROR_COLOR
115
116BUTTON_BG = TERTIARY_BG
117BUTTON_HOVER = SECONDARY_BG
118SIDEBAR_BG = "#222222"
119ACTIVE_TAB_COLOR = ACCENT_COLOR # Active tab is the accent color
120HOVER_TAB_COLOR = "#444444" # Slightly lighter hover
121
122# Globalne zmienne
123# Initialize variables before potentially loading config
124pending_instance_settings = {}
125pending_version = ""
126download_thread = None
127download_active = False
128global_progress_bar = None
129global_status_label = None
130instances = {}
131java_versions_cache = {}
132console = None
133username_var = tk.StringVar(value="Player")
134memory_var = tk.StringVar(value="2")
135shared_assets_var = tk.BooleanVar(value=True)
136shared_libraries_var = tk.BooleanVar(value=True)
137shared_natives_var = tk.BooleanVar(value=True)
138snapshots_var = tk.BooleanVar(value=True)
139releases_var = tk.BooleanVar(value=True)
140alpha_var = tk.BooleanVar(value=False)
141beta_var = tk.BooleanVar(value=False)
142current_tab = tk.StringVar(value="Instancje")
143selected_modrinth_instance_var = tk.StringVar()
144
145
146# Funkcje narzędziowe
147def log_to_console(console_widget, message, level="INFO"):
148 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
149 log_message = f"[{timestamp}] {level}: {message}\n"
150 # Log to file always
151 os.makedirs(LOGS_DIR, exist_ok=True)
152 try:
153 with open(os.path.join(LOGS_DIR, "launcher.log"), "a", encoding="utf-8") as f:
154 f.write(log_message)
155 except Exception as e:
156 print(f"Failed to write to launcher.log: {e}") # Fallback print
157
158 # Log to console widget if available
159 if console_widget:
160 try:
161 console_widget.config(state="normal")
162 tag = level.lower()
163 if tag not in console_widget.tag_names():
164 color = CONSOLE_FG_DEFAULT
165 if level == "ERROR": color = CONSOLE_FG_ERROR
166 elif level == "WARNING": color = CONSOLE_FG_WARNING
167 elif level == "SUCCESS": color = CONSOLE_FG_SUCCESS
168 elif level == "INFO": color = CONSOLE_FG_INFO
169 console_widget.tag_config(tag, foreground=color)
170
171 console_widget.insert(tk.END, log_message, tag)
172 console_widget.see(tk.END)
173 console_widget.config(state="disabled")
174 except Exception as e:
175 # This shouldn't happen if console_widget is valid, but as a fallback
176 print(f"Failed to write to console widget: {e}")
177
178
179def verify_sha1(file_path, expected_sha1):
180 if not os.path.exists(file_path):
181 return False
182 sha1 = hashlib.sha1()
183 try:
184 with open(file_path, "rb") as f:
185 # Read in chunks to handle large files
186 while chunk := f.read(4096):
187 sha1.update(chunk)
188 return sha1.hexdigest() == expected_sha1
189 except Exception as e:
190 log_to_console(console, f"Error verifying SHA1 for {file_path}: {e}", "ERROR")
191 return False
192
193def save_config():
194 config = {
195 "default_settings": {
196 "username": username_var.get(),
197 "memory": memory_var.get(),
198 "shared_assets": shared_assets_var.get(),
199 "shared_libraries": shared_libraries_var.get(),
200 "shared_natives": shared_natives_var.get()
201 },
202 "version_filters": {
203 "snapshots": snapshots_var.get(),
204 "releases": releases_var.get(),
205 "alpha": alpha_var.get(),
206 "beta": beta_var.get()
207 },
208 "instances": instances,
209 "java_versions": java_versions_cache # Cache found/downloaded java paths
210 }
211 os.makedirs(BASE_DIR, exist_ok=True)
212 try:
213 with open(CONFIG_FILE, "w", encoding="utf-8") as f:
214 json.dump(config, f, indent=4)
215 log_to_console(console, "Konfiguracja zapisana.", "INFO")
216 except Exception as e:
217 log_to_console(console, f"Błąd podczas zapisywania konfiguracji: {e}", "ERROR")
218
219
220def load_config():
221 global instances, java_versions_cache
222 # Variables are initialized globally before this function is called.
223
224 if os.path.exists(CONFIG_FILE):
225 try:
226 with open(CONFIG_FILE, "r", encoding="utf-8") as f:
227 config = json.load(f)
228
229 if "default_settings" in config:
230 default_settings = config["default_settings"]
231 username_var.set(default_settings.get("username", "Player"))
232 memory_var.set(default_settings.get("memory", "2"))
233 shared_assets_var.set(default_settings.get("shared_assets", True))
234 shared_libraries_var.set(default_settings.get("shared_libraries", True))
235 shared_natives_var.set(default_settings.get("shared_natives", True))
236
237 if "version_filters" in config:
238 version_filters = config["version_filters"]
239 snapshots_var.set(version_filters.get("snapshots", True))
240 releases_var.set(version_filters.get("releases", True))
241 alpha_var.set(version_filters.get("alpha", False))
242 beta_var.set(version_filters.get("beta", False))
243
244 # Load instances, ensuring required keys exist with defaults
245 instances = config.get("instances", {})
246 for version, data in instances.items():
247 data.setdefault("settings", {})
248 data["settings"].setdefault("username", username_var.get())
249 data["settings"].setdefault("memory", memory_var.get())
250 data["settings"].setdefault("shared_assets", shared_assets_var.get())
251 data["settings"].setdefault("shared_libraries", shared_libraries_var.get())
252 data["settings"].setdefault("shared_natives", shared_natives_var.get())
253 data["settings"].setdefault("loader_type", "vanilla") # Default to vanilla
254 data["settings"].setdefault("loader_version", "")
255 data["settings"].setdefault("server_ip", "")
256 data["settings"].setdefault("server_port", "")
257 data.setdefault("java_path", "")
258 data.setdefault("java_version", "")
259 data.setdefault("required_java", "1.8")
260 data.setdefault("ready", False) # Assume not ready until verified or downloaded
261 data.setdefault("timestamp", datetime.now().isoformat()) # Add timestamp if missing
262
263
264 java_versions_cache = config.get("java_versions", {})
265
266 log_to_console(console, "Konfiguracja wczytana.", "INFO")
267 return instances, java_versions_cache
268 except Exception as e:
269 log_to_console(console, f"Błąd wczytywania konfiguracji. Resetowanie do domyślnych: {e}", "ERROR")
270 # Reset to default values in case of error
271 instances = {}
272 java_versions_cache = {}
273 # Variables like username_var etc. are already set to defaults
274 return instances, java_versions_cache
275 else:
276 log_to_console(console, "Plik konfiguracji nie istnieje. Użyto domyślnych ustawień.", "INFO")
277 # Default values if file doesn't exist
278 instances = {}
279 java_versions_cache = {}
280 return instances, java_versions_cache
281
282def get_versions():
283 try:
284 url = "https://launchermeta.mojang.com/mc/game/version_manifest.json"
285 resp = requests.get(url, timeout=10)
286 resp.raise_for_status()
287 manifest = resp.json()
288 versions = []
289 allowed_types = []
290 if snapshots_var.get():
291 allowed_types.append("snapshot")
292 if releases_var.get():
293 allowed_types.append("release")
294 if alpha_var.get():
295 allowed_types.append("old_alpha")
296 if beta_var.get():
297 allowed_types.append("old_beta")
298 for v in manifest["versions"]:
299 if v["type"] in allowed_types:
300 versions.append(v["id"])
301 log_to_console(console, f"Pobrano {len(versions)} wersji (filtry: {', '.join(allowed_types)})", "INFO")
302 # Sort by parsing version string, releases first, then snapshots, then old
303 def version_sort_key(v_id):
304 v_info = next((item for item in manifest['versions'] if item['id'] == v_id), None)
305 if v_info:
306 v_type = v_info['type']
307 # Assign a numeric value to types for sorting: release > snapshot > beta > alpha
308 type_order = {'release': 4, 'snapshot': 3, 'old_beta': 2, 'old_alpha': 1}
309 type_priority = type_order.get(v_type, 0)
310 try:
311 # Use packaging.version for robust version comparison
312 parsed_version = pkg_version.parse(v_id.split('-')[0]) # Parse core version part
313 return (-type_priority, -parsed_version.release[0] if parsed_version.release else 0, -parsed_version.public[0] if parsed_version.public else 0, v_id) # Sort descending by type priority, then version parts, then id
314 except:
315 # Handle invalid versions by putting them last
316 return (-type_priority, -float('inf'), v_id)
317 return (0, v_id) # Default for unknown versions
318
319 return sorted(versions, key=version_sort_key, reverse=True) # Reverse again to get newest first within categories
320 except Exception as e:
321 log_to_console(console, f"Nie udało się pobrać listy wersji: {e}", "ERROR")
322 return []
323
324def get_version_info(version):
325 try:
326 url = "https://launchermeta.mojang.com/mc/game/version_manifest.json"
327 resp = requests.get(url, timeout=10)
328 resp.raise_for_status()
329 manifest = resp.json()
330 for v in manifest["versions"]:
331 if v["id"] == version:
332 version_url = v["url"]
333 resp_info = requests.get(version_url, timeout=10)
334 resp_info.raise_for_status()
335 return resp_info.json()
336 log_to_console(console, f"Nie znaleziono wersji {version} w manifeście.", "WARNING")
337 return None
338 except Exception as e:
339 log_to_console(console, f"Nie udało się pobrać info o wersji {version}: {e}", "ERROR")
340 return None
341
342def download_java(java_version, console_widget):
343 try:
344 system = platform.system().lower()
345 arch = "x64"
346 base_url = "https://api.adoptium.net/v3/binary/latest/"
347 # Map Minecraft required version strings to Adoptium feature versions
348 java_map = {
349 "1.8": "8",
350 "9": "9",
351 "10": "10",
352 "11": "11",
353 "12": "12",
354 "13": "13",
355 "14": "14",
356 "15": "15",
357 "16": "16",
358 "17": "17",
359 "18": "18",
360 "19": "19",
361 "20": "20",
362 "21": "21",
363 "22": "22"
364 }
365 feature_version = java_map.get(str(java_version), str(java_version)) # Use string keys for map lookup
366
367 # Adoptium URL structure: /feature_version/release_type/os/arch/image_type/jvm_impl/heap_size/vendor
368 # release_type: ga (General Availability)
369 # os: windows, mac, linux, etc.
370 # arch: x64, arm, etc.
371 # image_type: jdk, jre
372 # jvm_impl: hotspot (OpenJDK's default)
373 # heap_size: normal (default)
374 # vendor: eclipse (Temurin)
375 url = f"{base_url}{feature_version}/ga/{system}/{arch}/jdk/hotspot/normal/eclipse"
376 log_to_console(console_widget, f"Szukam Javy {feature_version} (Adoptium) dla {system}/{arch}...", "INFO")
377
378 resp_metadata = requests.get(url, timeout=10)
379 resp_metadata.raise_for_status()
380 metadata = resp_metadata.json()
381 if not metadata:
382 log_to_console(console_widget, f"Nie znaleziono dostępnych pakietów Javy {feature_version} dla {system}/{arch} na Adoptium.", "ERROR")
383 return None, None
384
385 # Find a suitable package (e.g., with a .zip or .tar.gz link)
386 download_link = None
387 for package in metadata:
388 if package['binary']['os'] == system and package['binary']['architecture'] == arch and package['binary']['image_type'] == 'jdk':
389 download_link = package['binary']['package']['link']
390 expected_size = package['binary']['package'].get('size', 0)
391 expected_sha256 = package['binary']['package'].get('checksum', None) # Adoptium uses SHA256
392 break # Take the first suitable one
393
394 if not download_link:
395 log_to_console(console_widget, f"Nie znaleziono linku do pobrania pakietu Javy {feature_version} dla {system}/{arch}.", "ERROR")
396 return None, None
397
398 file_extension = ".zip" if system == "windows" else ".tar.gz"
399 java_target_dir = os.path.join(JAVA_DIR, f"jdk-{feature_version}-{system}-{arch}")
400 os.makedirs(java_target_dir, exist_ok=True)
401 archive_path = os.path.join(java_target_dir, f"jdk-{feature_version}{file_extension}")
402
403 log_to_console(console_widget, f"Pobieranie Javy {feature_version} z {download_link}", "INFO")
404
405 resp_file = requests.get(download_link, stream=True, timeout=30) # Increased timeout
406 resp_file.raise_for_status()
407 total_size = int(resp_file.headers.get('content-length', 0)) or expected_size
408
409 downloaded_size = 0
410 with open(archive_path, "wb") as f:
411 for chunk in resp_file.iter_content(chunk_size=8192):
412 if chunk:
413 f.write(chunk)
414 downloaded_size += len(chunk)
415 if total_size > 0 and global_progress_bar:
416 update_progress(global_progress_bar, global_status_label,
417 (downloaded_size/total_size)*100, total_size-downloaded_size, f"Java {feature_version}", downloaded_size, total_size) # Pass bytes directly
418
419 # Verification (SHA256 for Adoptium) - Need sha256 function if not using sha1
420 # For simplicity here, we'll skip sha256 verification for now or rely on sha1 if available (less common for modern downloads)
421 # if expected_sha256 and not verify_sha256(archive_path, expected_sha256):
422 # log_to_console(console_widget, f"Błąd SHA256 dla archiwum Javy!", "ERROR")
423 # os.remove(archive_path) # Clean up
424 # return None, None
425 # Note: verify_sha1 exists, but Adoptium uses sha256. Implementing sha256 needed for proper verification.
426
427 log_to_console(console_widget, f"Rozpakowywanie Javy do {java_target_dir}", "INFO")
428 extracted_folder = None
429 try:
430 if system == "windows":
431 with zipfile.ZipFile(archive_path, 'r') as zip_ref:
432 zip_ref.extractall(java_target_dir)
433 # Find the actual JDK folder inside the zip (usually one level deep)
434 extracted_folder = next((os.path.join(java_target_dir, name) for name in zip_ref.namelist() if name.endswith('/bin/') or name.endswith('\\bin\\')), None)
435 if extracted_folder:
436 extracted_folder = os.path.dirname(extracted_folder) # Go up one level to the JDK root
437 else: # macOS/Linux
438 with tarfile.open(archive_path, 'r:gz') as tar_ref:
439 tar_ref.extractall(java_target_dir)
440 # Find the actual JDK folder inside the tar.gz
441 extracted_folder = next((os.path.join(java_target_dir, name) for name in tar_ref.getnames() if name.endswith('/bin/') or name.endswith('\\bin\\')), None)
442 if extracted_folder:
443 extracted_folder = os.path.dirname(extracted_folder) # Go up one level to the JDK root
444
445 os.remove(archive_path) # Clean up archive
446 log_to_console(console_widget, "Archiwum Javy rozpakowane.", "INFO")
447
448 except Exception as e:
449 log_to_console(console_widget, f"Błąd rozpakowywania archiwum Javy: {e}", "ERROR")
450 if os.path.exists(archive_path):
451 os.remove(archive_path)
452 return None, None
453
454
455 # Find java executable within extracted folder
456 if extracted_folder:
457 java_exec_name = "java.exe" if system == "windows" else "java"
458 # Search for java executable recursively in case the inner structure is complex
459 for root, _, files in os.walk(extracted_folder):
460 if java_exec_name in files:
461 java_path = os.path.join(root, java_exec_name)
462 version = get_java_version(java_path) # Use the correct console widget
463 if version:
464 log_to_console(console_widget, f"Pobrano i zainstalowano Javę: {java_path} (wersja: {version})", "SUCCESS")
465 # Add to cache
466 java_versions_cache[version] = java_path
467 save_config()
468 return java_path, version
469 log_to_console(console_widget, "Nie znaleziono java(.exe) w pobranym i rozpakowanym folderze!", "ERROR")
470 return None, None
471 else:
472 log_to_console(console_widget, "Nie udało się znaleźć ścieżki JDK po rozpakowaniu.", "ERROR")
473 return None, None
474
475 except requests.exceptions.RequestException as e:
476 log_to_console(console_widget, f"Błąd sieci podczas pobierania Javy {java_version}: {e}", "ERROR")
477 return None, None
478 except Exception as e:
479 log_to_console(console_widget, f"Ogólny błąd pobierania/instalacji Javy {java_version}: {e}", "ERROR")
480 return None, None
481
482def find_java(required_version=None):
483 possible_paths = []
484 system = platform.system()
485 java_exec_name = "java.exe" if system == "Windows" else "java"
486 log_to_console(console, f"Szukam Javy {required_version or 'dowolnej'} ({java_exec_name})...", "INFO")
487
488 # Check cache first
489 for ver, path in java_versions_cache.items():
490 if os.path.exists(path) and get_java_version(path): # Verify path still exists and is valid java
491 if (not required_version or check_java_version(ver, required_version)):
492 possible_paths.append((path, ver, "Cache"))
493 log_to_console(console, f"Znaleziono Javę w cache: {path} (wersja: {ver})", "INFO")
494
495 # Check custom JAVA_DIR installations
496 if os.path.exists(JAVA_DIR):
497 for java_folder in os.listdir(JAVA_DIR):
498 java_path = os.path.join(JAVA_DIR, java_folder) # Point to the root of the JDK folder
499 # Search for java executable inside this folder structure
500 found_exec = None
501 for root, _, files in os.walk(java_path):
502 if java_exec_name in files:
503 found_exec = os.path.join(root, java_exec_name)
504 break # Found the executable
505
506 if found_exec:
507 version = get_java_version(found_exec)
508 if version and (not required_version or check_java_version(version, required_version)):
509 if (found_exec, version, "Pobrana") not in possible_paths: # Avoid duplicates
510 possible_paths.append((found_exec, version, "Pobrana"))
511 log_to_console(console, f"Znaleziono Javę w {JAVA_DIR}: {found_exec} (wersja: {version})", "INFO")
512
513 # Check standard system locations
514 java_home = os.environ.get("JAVA_HOME")
515 if java_home:
516 java_path = os.path.join(java_home, "bin", java_exec_name)
517 if os.path.exists(java_path):
518 version = get_java_version(java_path)
519 if version and (not required_version or check_java_version(version, required_version)):
520 if (java_path, version, "JAVA_HOME") not in possible_paths:
521 possible_paths.append((java_path, version, "JAVA_HOME"))
522 log_to_console(console, f"Znaleziono Javę w JAVA_HOME: {java_path} (wersja: {version})", "INFO")
523
524 # Check PATH
525 try:
526 # Use 'where' on Windows, 'which' on Unix-like
527 command = ["where", java_exec_name] if system == "Windows" else ["which", java_exec_name]
528 out = subprocess.check_output(command, stderr=subprocess.DEVNULL).decode().strip()
529 for line in out.splitlines():
530 line = line.strip()
531 if os.path.exists(line):
532 version = get_java_version(line)
533 if version and (not required_version or check_java_version(version, required_version)):
534 if (line, version, "PATH") not in possible_paths:
535 possible_paths.append((line, version, "PATH"))
536 log_to_console(console, f"Znaleziono Javę w PATH: {line} (wersja: {version})", "INFO")
537 except:
538 pass # 'where' or 'which' command not found or java not in PATH
539
540 # Specific Windows paths (less reliable, but can catch some installs)
541 if system == "Windows":
542 for base in [os.environ.get('ProgramFiles'), os.environ.get('ProgramFiles(x86)'), "C:\\Program Files\\Java", "C:\\Program Files (x86)\\Java"]:
543 if base and os.path.isdir(base):
544 java_root = os.path.join(base, "Java")
545 if os.path.isdir(java_root):
546 for item in os.listdir(java_root):
547 java_path = os.path.join(java_root, item, "bin", java_exec_name)
548 if os.path.exists(java_path):
549 version = get_java_version(java_path)
550 if version and (not required_version or check_java_version(version, required_version)):
551 if (java_path, version, "Program Files") not in possible_paths:
552 possible_paths.append((java_path, version, "Program Files"))
553 log_to_console(console, f"Znaleziono Javę w {java_root}: {java_path} (wersja: {version})", "INFO")
554
555
556 # Ensure paths are unique (based on path itself) and prefer cached/downloaded ones
557 unique_paths = {}
558 for path, ver, source in possible_paths:
559 if path not in unique_paths:
560 unique_paths[path] = (path, ver, source)
561 else:
562 # Prefer 'Pobrana' or 'Cache' over others if duplicates exist
563 existing_source = unique_paths[path][2]
564 if source in ["Cache", "Pobrana"] and existing_source not in ["Cache", "Pobrana"]:
565 unique_paths[path] = (path, ver, source)
566
567 # Convert back to list, maintaining preference order if possible
568 # Simple sort order preference: Cache > Pobrana > JAVA_HOME > PATH > Others
569 source_order = {"Cache": 0, "Pobrana": 1, "JAVA_HOME": 2, "PATH": 3}
570 sorted_unique_paths = sorted(unique_paths.values(), key=lambda item: (source_order.get(item[2], 99), item[0]))
571
572
573 if not sorted_unique_paths:
574 log_to_console(console, f"Nie znaleziono Javy {required_version if required_version else ''} w 64-bitowej wersji!", "WARNING")
575 else:
576 log_to_console(console, f"Zakończono wyszukiwanie Javy. Znaleziono {len(sorted_unique_paths)} pasujących ścieżek.", "INFO")
577
578 # Return list of (path, version) tuples
579 return [(p, v) for p, v, _ in sorted_unique_paths]
580
581
582def get_java_version(java_path):
583 if not os.path.exists(java_path):
584 return None
585 try:
586 # Use 'java -version' which outputs to stderr
587 result = subprocess.run([java_path, "-version"], capture_output=True, text=True, check=True, encoding='utf-8', errors='ignore')
588 version_line = result.stderr.split('\n')[0].strip()
589 version_match = re.search(r'version "([^"]+)"', version_line)
590 if version_match:
591 version = version_match.group(1)
592 # Check if it's a 64-Bit JVM
593 is_64bit = "64-Bit" in result.stderr or "64-bit" in result.stderr or "x86_64" in result.stderr.lower()
594
595 # Use only the main version part for key comparisons (e.g., 1.8, 16, 17, 21)
596 # Handle different version formats (e.g., "1.8.0_301", "17.0.1", "9")
597 major_version_match = re.match(r'(\d+\.\d+|\d+)', version)
598 if major_version_match:
599 simple_version = major_version_match.group(1)
600 # Special case: 1.8 is commonly referred to as 8
601 if simple_version.startswith("1.8"):
602 simple_version = "1.8"
603 elif "." in simple_version: # For >= 9, just the first number is often enough
604 simple_version = simple_version.split('.')[0]
605 # Store the full version but key it by the simple version in cache if needed
606 if is_64bit:
607 java_versions_cache[version] = java_path # Cache full version string to path
608 return version # Return full version for display
609 else:
610 log_to_console(console, f"Java found at {java_path} is not 64-bit. Skipping.", "WARNING")
611 return None
612 else:
613 log_to_console(console, f"Could not parse version from '{version_line}' for Java at {java_path}.", "WARNING")
614 return None
615 else:
616 log_to_console(console, f"Could not find version string in output for Java at {java_path}. Output: {result.stderr.strip()}", "WARNING")
617 return None
618 except FileNotFoundError:
619 log_to_console(console, f"Java executable not found at {java_path}", "ERROR")
620 return None
621 except subprocess.CalledProcessError as e:
622 log_to_console(console, f"Error running java -version for {java_path}: {e.stderr.strip()}", "ERROR")
623 return None
624 except Exception as e:
625 log_to_console(console, f"Unexpected error checking version for {java_path}: {e}", "ERROR")
626 return None
627
628def check_java_version(installed_version_str, required_version_str):
629 """
630 Checks if the installed Java version meets the required minimum version.
631 Handles versions like "1.8", "9", "16", "17", "21".
632 """
633 try:
634 # Normalize required version for comparison (e.g., "1.8" -> "8")
635 required_normalized = required_version_str
636 if required_version_str == "1.8":
637 required_normalized = "8"
638 elif "." in required_version_str:
639 required_normalized = required_version_str.split('.')[0] # Use major version for comparison
640
641 # Extract major version from installed string (e.g., "1.8.0_301" -> "1.8", "17.0.1" -> "17")
642 installed_major_match = re.match(r'(\d+\.\d+|\d+)', installed_version_str)
643 if not installed_major_match:
644 log_to_console(console, f"Nie można sparsować wersji zainstalowanej Javy: {installed_version_str}", "WARNING")
645 return False
646 installed_simple = installed_major_match.group(1)
647 # Special case: 1.8 comparison
648 if required_version_str == "1.8":
649 return installed_simple.startswith("1.8.") # Java 8 needs 1.8.x
650 # For >= 9, compare as integers if possible
651 try:
652 installed_major_int = int(installed_simple.split('.')[0])
653 required_major_int = int(required_normalized)
654 return installed_major_int >= required_major_int
655 except ValueError:
656 # Fallback to string comparison if not simple integers
657 log_to_console(console, f"Porównanie wersji Javy jako stringi: '{installed_version_str}' vs '{required_version_str}'", "INFO")
658 return installed_version_str.startswith(required_version_str) # Simple prefix check
659
660 except Exception as e:
661 log_to_console(console, f"Błąd podczas porównania wersji Javy: zainstalowana='{installed_version_str}', wymagana='{required_version_str}' - {e}", "ERROR")
662 return False
663
664
665def is_new_launcher(version):
666 """Checks if the version uses the new launcher arguments (>= 1.6)."""
667 try:
668 ver = pkg_version.parse(version)
669 # New launcher arguments were introduced around 1.6
670 return ver >= pkg_version.parse("1.6")
671 except pkg_version.InvalidVersion:
672 # Assume newer arguments for unparseable versions
673 return True
674
675def get_required_java(version, version_info):
676 """Determines the required major Java version for a given MC version."""
677 if version_info and "javaVersion" in version_info:
678 major_version_info = version_info["javaVersion"].get("majorVersion")
679 if major_version_info:
680 return str(major_version_info)
681 # Fallback to component version if majorVersion is missing but component is present
682 component_version = version_info["javaVersion"].get("component")
683 if component_version:
684 major_match = re.match(r'jre([\d]+)', component_version)
685 if major_match:
686 return major_match.group(1)
687
688 # Fallback based on known version ranges
689 try:
690 ver = pkg_version.parse(version)
691 if ver >= pkg_version.parse("1.20.5"): # Placeholder, check actual version manifests
692 return "21" # MC 1.20.5+ requires Java 21
693 if ver >= pkg_version.parse("1.18"):
694 return "17" # MC 1.18+ requires Java 17 (1.17 also uses 17 technically)
695 if ver >= pkg_version.parse("1.17"):
696 return "16" # MC 1.17 requires Java 16 (often works with 17 too)
697 # MC versions 1.6 to 1.16 require Java 8
698 if ver >= pkg_version.parse("1.6"):
699 return "1.8"
700
701 # Older versions might use Java 6 or 7, but Java 8 is often compatible
702 log_to_console(console, f"Using default Java 8 for older/unknown version {version}", "INFO")
703 return "1.8" # Default for old/unparsed versions
704 except pkg_version.InvalidVersion:
705 log_to_console(console, f"Invalid version string '{version}', using default Java 8.", "WARNING")
706 return "1.8" # Default for invalid versions
707
708def download_file(url, path, progress_callback=None, expected_sha1=None, console_widget=None, description="plik"):
709 os.makedirs(os.path.dirname(path), exist_ok=True)
710 try:
711 log_to_console(console_widget, f"Pobieranie {description}: {url} do {path}", "INFO")
712 resp = requests.get(url, stream=True, timeout=30) # Increased timeout
713 resp.raise_for_status()
714 total_size = int(resp.headers.get('content-length', 0)) or 1 # Avoid division by zero if size is unknown
715 downloaded_size = 0
716 with open(path, "wb") as f:
717 for chunk in resp.iter_content(chunk_size=8192):
718 if chunk:
719 f.write(chunk)
720 downloaded_size += len(chunk)
721 if progress_callback:
722 progress_callback(downloaded_size, total_size)
723 log_to_console(console_widget, f"Pobrano {description}: {os.path.basename(path)}", "INFO")
724
725 if expected_sha1 and not verify_sha1(path, expected_sha1):
726 log_to_console(console_widget, f"Błąd SHA1 dla {description} {path}", "ERROR")
727 # Optionally delete the corrupted file
728 # os.remove(path)
729 return False
730 return True
731 except requests.exceptions.RequestException as e:
732 log_to_console(console_widget, f"Błąd sieci podczas pobierania {description} {path}: {e}", "ERROR")
733 return False
734 except Exception as e:
735 log_to_console(console_widget, f"Ogólny błąd podczas pobierania {description} {path}: {e}", "ERROR")
736 return False
737
738
739def download_libraries(libs_info, version_dir, shared_libraries, console_widget, progress_callback):
740 libraries_dir = LIBRARIES_DIR if shared_libraries else os.path.join(version_dir, "libraries")
741 os.makedirs(libraries_dir, exist_ok=True)
742
743 libs_to_download = []
744 total_libs_size = 0
745 for lib in libs_info:
746 # Check for rules allowing/denying the library based on OS (simplistic check)
747 is_allowed = True
748 if "rules" in lib:
749 is_allowed = False # Start assuming denied unless explicitly allowed
750 for rule in lib["rules"]:
751 action = rule.get("action", "allow")
752 os_info = rule.get("os")
753 if os_info:
754 current_os = platform.system().lower()
755 if os_info.get("name") == current_os:
756 if action == "allow":
757 is_allowed = True
758 break # Found an allowing rule for this OS
759 elif action == "deny":
760 is_allowed = False # Found a denying rule, stop checking
761 break
762 else: # Rule applies to all OS if no 'os' is specified
763 if action == "allow":
764 is_allowed = True
765 break
766 elif action == "deny":
767 is_allowed = False
768 break
769
770 if is_allowed and "downloads" in lib and "artifact" in lib["downloads"]:
771 artifact = lib["downloads"]["artifact"]
772 lib_path = artifact["path"].replace("/", os.sep)
773 full_lib_path = os.path.join(libraries_dir, lib_path)
774 if not os.path.exists(full_lib_path) or not verify_sha1(full_lib_path, artifact["sha1"]):
775 libs_to_download.append((lib, artifact))
776 total_libs_size += artifact.get("size", 0)
777
778 log_to_console(console_widget, f"Pobieranie {len(libs_to_download)} bibliotek (łączny rozmiar: {humanize.naturalsize(total_libs_size)})", "INFO")
779 downloaded_libs_size = 0
780 total_libs = len(libs_to_download)
781
782 for i, (lib, artifact) in enumerate(libs_to_download):
783 lib_path_rel = artifact["path"].replace("/", os.sep) # Relative path within libraries dir
784 full_lib_path = os.path.join(libraries_dir, lib_path_rel)
785 lib_url = artifact["url"]
786 lib_sha1 = artifact["sha1"]
787 lib_size = artifact.get("size", 0)
788
789 if not download_file(
790 lib_url, full_lib_path,
791 lambda d, t: progress_callback(downloaded_libs_size + d, total_libs_size, "biblioteki", i + (d / t) if t > 0 else i, total_libs),
792 expected_sha1=lib_sha1, console_widget=console_widget, description=f"biblioteka {os.path.basename(lib_path_rel)}"
793 ):
794 # If a library download fails, it might be critical. Report error but continue trying others.
795 log_to_console(console_widget, f"Nie udało się pobrać lub zweryfikować biblioteki {lib_path_rel}", "ERROR")
796 # Decide whether to fail completely or just mark instance as not ready
797 # For now, just log error and continue
798 else:
799 downloaded_libs_size += lib_size # Only add size if download was successful
800
801 # Final progress update for this stage
802 progress_callback(total_libs_size, total_libs_size, "biblioteki", total_libs, total_libs)
803 log_to_console(console_widget, "Pobieranie bibliotek zakończone.", "INFO")
804 return True # Assuming we don't stop on individual library failures
805
806
807def download_natives(libs_info, version_dir, shared_natives, console_widget, progress_callback):
808 system = platform.system().lower()
809 arch = platform.architecture()[0] # '32bit' or '64bit'
810 natives_dir = NATIVES_DIR if shared_natives else os.path.join(version_dir, "natives")
811 os.makedirs(natives_dir, exist_ok=True)
812
813 natives_classifier = f"natives-{system}" # e.g., natives-windows
814 # Some classifiers might include architecture, check manifest details if needed
815 # e.g., natives-windows-64
816
817 natives_to_download = []
818 total_size = 0
819
820 for lib in libs_info:
821 # Apply rules same as libraries
822 is_allowed = True
823 if "rules" in lib:
824 is_allowed = False
825 for rule in lib["rules"]:
826 action = rule.get("action", "allow")
827 os_info = rule.get("os")
828 if os_info:
829 current_os = platform.system().lower()
830 if os_info.get("name") == current_os:
831 # Basic architecture check if present in rule os info
832 rule_arch = os_info.get("arch")
833 if rule_arch and rule_arch != arch.replace("bit", ""):
834 continue # Rule doesn't match current architecture
835
836 if action == "allow":
837 is_allowed = True
838 break
839 elif action == "deny":
840 is_allowed = False
841 break
842 else:
843 if action == "allow":
844 is_allowed = True
845 break
846 elif action == "deny":
847 is_allowed = False
848 break
849
850 if is_allowed and "downloads" in lib and "classifiers" in lib["downloads"]:
851 classifiers = lib["downloads"]["classifiers"]
852 # Find the most specific classifier that matches the system and architecture
853 matching_classifier = None
854 # Prioritize architecture specific if available
855 if f"{natives_classifier}-{arch.replace('bit','')}" in classifiers:
856 matching_classifier = classifiers[f"{natives_classifier}-{arch.replace('bit','')}"]
857 elif natives_classifier in classifiers:
858 matching_classifier = classifiers[natives_classifier]
859
860 if matching_classifier:
861 natives_to_download.append(matching_classifier)
862 total_size += matching_classifier.get("size", 0)
863
864 log_to_console(console_widget, f"Pobieranie {len(natives_to_download)} natywnych bibliotek (łączny rozmiar: {humanize.naturalsize(total_size)})", "INFO")
865 downloaded_size = 0
866 total_natives = len(natives_to_download)
867
868 temp_native_dir = os.path.join(version_dir, "temp_natives_extract") # Temp dir for extraction
869
870 for i, artifact in enumerate(natives_to_download):
871 native_url = artifact["url"]
872 native_sha1 = artifact["sha1"]
873 native_size = artifact.get("size", 0)
874 # Download natives as temp files
875 native_temp_path = os.path.join(version_dir, f"temp_native_{i}.jar") # Or .zip, depending on content-type/url
876
877 if not download_file(
878 native_url, native_temp_path,
879 lambda d, t: progress_callback(downloaded_size + d, total_size, "natives", i + (d/t) if t > 0 else i, total_natives),
880 expected_sha1=native_sha1, console_widget=console_widget, description=f"natywna biblioteka {os.path.basename(native_url)}"
881 ):
882 log_to_console(console_widget, f"Nie udało się pobrać lub zweryfikować natywnej biblioteki {native_url}", "ERROR")
883 # Decide whether to fail completely or just mark instance as not ready
884 # For now, just log error and continue
885 continue # Skip extraction for this failed download
886
887 # Extract contents of the native JAR/ZIP
888 os.makedirs(temp_native_dir, exist_ok=True)
889 try:
890 with zipfile.ZipFile(native_temp_path, 'r') as zip_ref:
891 # Extract only files, ignoring directories and META-INF (as per Mojang launcher)
892 for file_info in zip_ref.infolist():
893 if not file_info.is_dir() and not file_info.filename.startswith('META-INF/'):
894 target_path = os.path.join(natives_dir, os.path.basename(file_info.filename)) # Extract directly to natives_dir
895 # Avoid extracting duplicates if multiple natives archives have the same file
896 if not os.path.exists(target_path):
897 zip_ref.extract(file_info, path=natives_dir) # Extract directly
898 else:
899 log_to_console(console_widget, f"Plik {os.path.basename(file_info.filename)} już istnieje w {natives_dir}. Pomijam.", "INFO")
900
901 os.remove(native_temp_path) # Clean up temp file
902 downloaded_size += native_size # Only add size if download was successful and extraction attempted
903 except Exception as e:
904 log_to_console(console_widget, f"Błąd rozpakowywania natywnej biblioteki {os.path.basename(native_temp_path)}: {e}", "ERROR")
905 if os.path.exists(native_temp_path):
906 os.remove(native_temp_path)
907 continue # Continue with next native
908
909 # Clean up temp native directory if it was created and is empty
910 if os.path.exists(temp_native_dir) and not os.listdir(temp_native_dir):
911 os.rmdir(temp_native_dir)
912 elif os.path.exists(temp_native_dir):
913 log_to_console(console_widget, f"Nie udało się usunąć tymczasowego katalogu natywnego: {temp_native_dir}. Może zawierać pliki.", "WARNING")
914
915
916 # Final progress update for this stage
917 progress_callback(total_size, total_size, "natives", total_natives, total_natives)
918 log_to_console(console_widget, "Pobieranie i rozpakowywanie natywnych bibliotek zakończone.", "INFO")
919 # We return True even if some failed, the verify_instance function will check integrity
920 return True
921
922
923def download_assets(version_info, version_dir, shared_assets, console_widget, progress_callback):
924 try:
925 asset_index = version_info.get("assetIndex", {})
926 asset_url = asset_index.get("url")
927 asset_id = asset_index.get("id")
928 if not asset_url:
929 log_to_console(console_widget, "Brak assetIndex dla tej wersji.", "WARNING")
930 return True # Not critical, some old versions might not have assets this way
931
932 resp = requests.get(asset_url, timeout=10)
933 resp.raise_for_status()
934 assets_data = resp.json()
935
936 assets_base_dir = ASSETS_DIR if shared_assets else os.path.join(version_dir, "assets")
937 objects_dir = os.path.join(assets_base_dir, "objects")
938 indexes_dir = os.path.join(assets_base_dir, "indexes")
939 os.makedirs(objects_dir, exist_ok=True)
940 os.makedirs(indexes_dir, exist_ok=True)
941
942 index_file = os.path.join(indexes_dir, f"{asset_id}.json")
943 # Save index file even if assets are shared, as it's version-specific
944 log_to_console(console_widget, f"Zapisywanie indeksu assetów: {index_file}", "INFO")
945 try:
946 with open(index_file, "w", encoding="utf-8") as f:
947 json.dump(assets_data, f, indent=4)
948 except Exception as e:
949 log_to_console(console_widget, f"Błąd zapisu indeksu assetów {index_file}: {e}", "WARNING")
950
951 assets_to_download = []
952 total_assets_size = 0
953
954 for asset_name, asset_info in assets_data.get("objects", {}).items():
955 asset_hash = asset_info["hash"]
956 asset_size = asset_info["size"]
957 asset_subpath = f"{asset_hash[:2]}/{asset_hash}"
958 asset_path = os.path.join(objects_dir, asset_subpath)
959 # Check if asset exists and is valid (optional but good practice)
960 if not os.path.exists(asset_path) or not verify_sha1(asset_path, asset_hash):
961 assets_to_download.append((asset_name, asset_info))
962 total_assets_size += asset_size
963
964 log_to_console(console_widget, f"Pobieranie {len(assets_to_download)} assetów (łączny rozmiar: {humanize.naturalsize(total_assets_size)})", "INFO")
965 downloaded_size = 0
966 total_assets = len(assets_to_download)
967
968 for i, (asset_name, asset_info) in enumerate(assets_to_download):
969 asset_hash = asset_info["hash"]
970 asset_size = asset_info["size"]
971 asset_subpath = f"{asset_hash[:2]}/{asset_hash}"
972 asset_path = os.path.join(objects_dir, asset_subpath)
973 asset_url = f"https://resources.download.minecraft.net/{asset_subpath}"
974
975 if not download_file(
976 asset_url, asset_path,
977 lambda d, t: progress_callback(downloaded_size + d, total_assets_size, "assets", i + (d / t) if t > 0 else i, total_assets),
978 expected_sha1=asset_hash, console_widget=console_widget, description=f"asset {asset_name}"
979 ):
980 log_to_console(console_widget, f"Nie udało się pobrać lub zweryfikować assetu {asset_name}", "ERROR")
981 # Continue despite failure, verification will catch missing/corrupt files
982 else:
983 downloaded_size += asset_size # Only add size if download was successful
984
985 # Final progress update
986 progress_callback(total_assets_size, total_assets_size, "assets", total_assets, total_assets)
987 log_to_console(console_widget, "Pobieranie assetów zakończone.", "INFO")
988 return True # Verification function will check if all required assets are present later
989 except requests.exceptions.RequestException as e:
990 log_to_console(console_widget, f"Błąd sieci podczas pobierania assetów: {e}", "ERROR")
991 return False
992 except Exception as e:
993 log_to_console(console_widget, f"Ogólny błąd pobierania assetów: {e}", "ERROR")
994 return False
995
996def verify_instance(version, console_widget=None):
997 """
998 Verifies if an instance is complete based on its version manifest.
999 Can optionally take a console widget to log progress.
1000 Returns True if complete and valid, False otherwise.
1001 """
1002 instance = instances.get(version)
1003 if not instance:
1004 log_to_console(console_widget, f"Instancja {version} nie istnieje w konfiguracji!", "ERROR")
1005 return False
1006
1007 version_dir = instance["path"]
1008 settings = instance.get("settings", {}) # Use .get for safety
1009 info = get_version_info(version)
1010 if not info:
1011 log_to_console(console_widget, f"Nie udało się pobrać manifestu wersji {version}. Nie można zweryfikować.", "ERROR")
1012 instance["ready"] = False
1013 save_config()
1014 return False
1015
1016 log_to_console(console_widget, f"Weryfikacja instancji {version}...", "INFO")
1017 all_valid = True
1018 missing_files = []
1019 corrupt_files = []
1020
1021 # 1. Verify client.jar
1022 client_path = os.path.join(version_dir, f"{version}.jar")
1023 client_download_info = info.get("downloads", {}).get("client")
1024 if client_download_info:
1025 client_sha1 = client_download_info.get("sha1")
1026 if not os.path.exists(client_path):
1027 log_to_console(console_widget, f"Brak client.jar dla {version}", "WARNING")
1028 missing_files.append(f"client.jar ({version})")
1029 all_valid = False
1030 elif client_sha1 and not verify_sha1(client_path, client_sha1):
1031 log_to_console(console_widget, f"Błąd SHA1 dla client.jar ({version})", "WARNING")
1032 corrupt_files.append(f"client.jar ({version})")
1033 all_valid = False
1034 else:
1035 log_to_console(console_widget, f"client.jar OK", "INFO")
1036 else:
1037 log_to_console(console_widget, f"Brak informacji o pobieraniu client.jar w manifeście wersji {version}.", "WARNING")
1038 # Cannot verify, assume missing for older versions or incomplete manifests
1039 missing_files.append(f"client.jar ({version}) - Brak info w manifeście?")
1040 all_valid = False
1041
1042
1043 # 2. Verify libraries
1044 libraries_dir = LIBRARIES_DIR if settings.get("shared_libraries", True) else os.path.join(version_dir, "libraries")
1045 for lib in info.get("libraries", []):
1046 # Apply rules just like in download
1047 is_required = True
1048 if "rules" in lib:
1049 is_required = False # Start assuming denied unless explicitly allowed
1050 for rule in lib["rules"]:
1051 action = rule.get("action", "allow")
1052 os_info = rule.get("os")
1053 if os_info:
1054 current_os = platform.system().lower()
1055 if os_info.get("name") == current_os:
1056 rule_arch = os_info.get("arch")
1057 current_arch = platform.architecture()[0].replace("bit", "")
1058 if rule_arch and rule_arch != current_arch:
1059 continue
1060
1061 if action == "allow":
1062 is_required = True
1063 break
1064 elif action == "deny":
1065 is_required = False
1066 break
1067 else: # Rule applies to all OS if no 'os' is specified
1068 if action == "allow":
1069 is_required = True
1070 break
1071 elif action == "deny":
1072 is_required = False
1073 break
1074
1075
1076 if is_required and "downloads" in lib and "artifact" in lib["downloads"]:
1077 artifact = lib["downloads"]["artifact"]
1078 lib_path_rel = artifact["path"].replace("/", os.sep)
1079 full_lib_path = os.path.join(libraries_dir, lib_path_rel)
1080 lib_sha1 = artifact["sha1"]
1081 if not os.path.exists(full_lib_path):
1082 log_to_console(console_widget, f"Brak wymaganej biblioteki {lib_path_rel}", "WARNING")
1083 missing_files.append(f"biblioteka {lib_path_rel}")
1084 all_valid = False
1085 elif not verify_sha1(full_lib_path, lib_sha1):
1086 log_to_console(console_widget, f"Błąd SHA1 dla biblioteki {lib_path_rel}", "WARNING")
1087 corrupt_files.append(f"biblioteka {lib_path_rel}")
1088 all_valid = False
1089 # else:
1090 # log_to_console(console_widget, f"Biblioteka {lib_path_rel} OK", "INFO") # Too verbose
1091
1092 # 3. Verify assets (checks index and potentially some objects if shared)
1093 assets_base_dir = ASSETS_DIR if settings.get("shared_assets", True) else os.path.join(version_dir, "assets")
1094 asset_index_info = info.get("assetIndex", {})
1095 asset_id = asset_index_info.get("id", version) # Default asset index id to version if missing
1096 index_file_path = os.path.join(assets_base_dir, "indexes", f"{asset_id}.json")
1097 objects_dir = os.path.join(assets_base_dir, "objects")
1098
1099 if not os.path.exists(index_file_path):
1100 log_to_console(console_widget, f"Brak pliku indeksu assetów: {index_file_path}", "WARNING")
1101 missing_files.append(f"indeks assetów ({asset_id})")
1102 all_valid = False
1103 elif asset_index_info.get("sha1") and not verify_sha1(index_file_path, asset_index_info["sha1"]):
1104 log_to_console(console_widget, f"Błąd SHA1 dla indeksu assetów {index_file_path}", "WARNING")
1105 corrupt_files.append(f"indeks assetów ({asset_id})")
1106 all_valid = False
1107 else:
1108 log_to_console(console_widget, f"Indeks assetów OK: {index_file_path}", "INFO")
1109 # If index is okay, check some random assets or a critical few
1110 # Full asset verification is too slow. Rely on download process to get them.
1111 # Just check if the objects directory exists and has content if shared assets are used.
1112 if settings.get("shared_assets", True) and not os.path.exists(objects_dir) or (os.path.exists(objects_dir) and not os.listdir(objects_dir)):
1113 log_to_console(console_widget, f"Współdzielony folder assetów ({objects_dir}) wydaje się pusty.", "WARNING")
1114 # This isn't necessarily an error if no assets were needed yet, but worth a warning.
1115 pass # Don't set all_valid = False just for this warning
1116
1117 # 4. Verify natives (basic check if folder exists and has content if not shared)
1118 natives_dir = NATIVES_DIR if settings.get("shared_natives", True) else os.path.join(version_dir, "natives")
1119 if not os.path.exists(natives_dir) or (os.path.exists(natives_dir) and not os.listdir(natives_dir)):
1120 log_to_console(console_widget, f"Folder natywnych bibliotek ({natives_dir}) wydaje się pusty.", "WARNING")
1121 # Natywne są krytyczne do uruchomienia.
1122 missing_files.append("natywne biblioteki")
1123 all_valid = False
1124 else:
1125 log_to_console(console_widget, f"Folder natywnych bibliotek ({natives_dir}) wygląda OK.", "INFO")
1126
1127
1128 # 5. Verify Java Path
1129 java_path = instance.get("java_path")
1130 required_java_version = instance.get("required_java")
1131 if not java_path or not os.path.exists(java_path):
1132 log_to_console(console_widget, f"Brak ścieżki Javy lub plik nie istnieje: {java_path}", "ERROR")
1133 all_valid = False
1134 # Try to find a suitable Java if the saved one is missing
1135 log_to_console(console_widget, f"Próbuję znaleźć pasującą Javę {required_java_version}", "INFO")
1136 found_javas = find_java(required_java_version)
1137 if found_javas:
1138 new_java_path, new_java_version = found_javas[0]
1139 log_to_console(console_widget, f"Znaleziono alternatywną Javę: {new_java_path} (wersja: {new_java_version}). Zapisuję w konfiguracji.", "INFO")
1140 instance["java_path"] = new_java_path
1141 instance["java_version"] = new_java_version
1142 # all_valid remains False because the original config had an issue,
1143 # but the instance data is updated for the next launch attempt.
1144 else:
1145 log_to_console(console_widget, f"Nie znaleziono żadnej pasującej Javy {required_java_version}. Nie można uruchomić.", "ERROR")
1146
1147 elif required_java_version:
1148 actual_java_version = get_java_version(java_path)
1149 if not actual_java_version or not check_java_version(actual_java_version, required_java_version):
1150 log_to_console(console_widget, f"Wybrana Java ({java_path}, wersja: {actual_java_version}) nie spełnia wymagań wersji {required_java_version}.", "ERROR")
1151 all_valid = False
1152 else:
1153 log_to_console(console_widget, f"Wybrana Java ({java_path}, wersja: {actual_java_version}) spełnia wymagania wersji {required_java_version}.", "INFO")
1154
1155
1156 # 6. Check and regenerate start.bat
1157 # We regenerate it every time during verification to ensure it's up-to-date
1158 # with current settings (like username, memory, java path, loader)
1159 log_to_console(console_widget, f"Generowanie/regenerowanie start.bat dla {version}...", "INFO")
1160 try:
1161 regenerate_start_bat(version, instance, info, console_widget)
1162 log_to_console(console_widget, f"start.bat dla {version} zregenerowany.", "SUCCESS")
1163 except Exception as e:
1164 log_to_console(console_widget, f"Błąd podczas regeneracji start.bat dla {version}: {e}", "ERROR")
1165 # Failure to regenerate start.bat is critical
1166 all_valid = False
1167
1168
1169 # Final status update
1170 if all_valid:
1171 log_to_console(console_widget, f"Instancja {version} zweryfikowana poprawnie. Jest gotowa!", "SUCCESS")
1172 instance["ready"] = True
1173 message = f"Instancja {version} jest kompletna i gotowa do gry!"
1174 else:
1175 log_to_console(console_widget, f"Weryfikacja instancji {version} zakończona z błędami.", "WARNING")
1176 instance["ready"] = False
1177 message = f"Instancja {version} ma problemy:\n"
1178 if missing_files:
1179 message += "\nBrakuje plików:\n" + "\n".join(missing_files)
1180 if corrupt_files:
1181 message += "\nUszkodzone pliki:\n" + "\n".join(corrupt_files)
1182 if not instance.get("java_path") or not os.path.exists(instance["java_path"]):
1183 message += f"\nNie znaleziono działającej Javy ({instance.get('java_path')})."
1184 elif required_java_version and (not instance.get("java_version") or not check_java_version(instance["java_version"], required_java_version)):
1185 message += f"\nWybrana Java ({instance.get('java_path')}, wersja {instance.get('java_version')}) nie spełnia wymagań ({required_java_version})."
1186
1187 message += "\n\nSpróbuj pobrać instancję ponownie lub edytować ustawienia Javy."
1188
1189 save_config() # Save changes to instance data (java path, ready status)
1190
1191 # Provide feedback to user
1192 if all_valid:
1193 messagebox.showinfo("Weryfikacja zakończona", message)
1194 else:
1195 messagebox.showwarning("Weryfikacja zakończona", message)
1196
1197 refresh_instances() # Update the GUI list
1198 return all_valid
1199
1200
1201def regenerate_start_bat(version, instance_data, version_info, console_widget=None):
1202 """Generates or regenerates the start.bat file for an instance."""
1203 if not version_info:
1204 log_to_console(console_widget, f"Nie można zregenerować start.bat dla {version}: brak info o wersji.", "ERROR")
1205 raise ValueError("Brak informacji o wersji.") # Raise error to indicate failure
1206
1207 version_dir = instance_data["path"]
1208 settings = instance_data.get("settings", {})
1209 java_path = instance_data.get("java_path")
1210 loader_type = settings.get("loader_type", "vanilla")
1211 server_ip = settings.get("server_ip", "")
1212 server_port = settings.get("server_port", "")
1213
1214
1215 if not java_path or not os.path.exists(java_path):
1216 log_to_console(console_widget, f"Nie można zregenerować start.bat: brak ścieżki Javy lub plik nie istnieje: {java_path}", "ERROR")
1217 # We could try to find Java here, but verify_instance already does this.
1218 # Just raise the error to signal failure.
1219 raise FileNotFoundError(f"Java executable not found at {java_path}")
1220
1221 bat_path = os.path.join(version_dir, "start.bat")
1222 libraries_dir = LIBRARIES_DIR if settings.get("shared_libraries", True) else os.path.join(version_dir, "libraries")
1223 assets_path = ASSETS_DIR if settings.get("shared_assets", True) else os.path.join(version_dir, "assets")
1224
1225 # Build the classpath
1226 # Start with client JAR
1227 classpath_entries = [f'"{os.path.join(version_dir, f"{version}.jar")}"']
1228
1229 # Add libraries based on manifest rules
1230 for lib in version_info.get("libraries", []):
1231 is_required = True
1232 # Apply rules similar to download/verify
1233 if "rules" in lib:
1234 is_required = False
1235 for rule in lib["rules"]:
1236 action = rule.get("action", "allow")
1237 os_info = rule.get("os")
1238 if os_info:
1239 current_os = platform.system().lower()
1240 if os_info.get("name") == current_os:
1241 rule_arch = os_info.get("arch")
1242 current_arch = platform.architecture()[0].replace("bit", "")
1243 if rule_arch and rule_arch != current_arch:
1244 continue
1245 if action == "allow":
1246 is_required = True
1247 break
1248 elif action == "deny":
1249 is_required = False
1250 break
1251 else:
1252 if action == "allow":
1253 is_required = True
1254 break
1255 elif action == "deny":
1256 is_required = False
1257 break
1258
1259 if is_required and "downloads" in lib and "artifact" in lib["downloads"]:
1260 artifact = lib["downloads"]["artifact"]
1261 lib_path_rel = artifact["path"].replace("/", os.sep)
1262 full_lib_path = os.path.join(libraries_dir, lib_path_rel)
1263 if os.path.exists(full_lib_path): # Only add if the file exists
1264 classpath_entries.append(f'"{full_lib_path}"')
1265 else:
1266 log_to_console(console_widget, f"Biblioteka '{lib_path_rel}' brakuje, nie dodano do class ścieżki.", "WARNING")
1267
1268
1269 classpath = ';'.join(classpath_entries) # Use ';' for Windows classpath
1270
1271 # Determine main class and arguments based on loader type
1272 mc_args = version_info.get("minecraftArguments") # Old style args
1273 arguments = version_info.get("arguments", {}) # New style args
1274 main_class = version_info.get("mainClass")
1275
1276 jvm_args = []
1277 game_args = []
1278
1279 if arguments: # New style arguments (1.13+)
1280 log_to_console(console_widget, f"Używam nowych argumentów uruchamiania (version_info['arguments']).", "INFO")
1281 # Parse JVM arguments
1282 jvm_arg_list = arguments.get("jvm", [])
1283 for arg in jvm_arg_list:
1284 if isinstance(arg, str):
1285 jvm_args.append(arg)
1286 elif isinstance(arg, dict):
1287 # Handle rules within arguments (simplified - just check for os rule)
1288 is_allowed = True
1289 if "rules" in arg:
1290 is_allowed = False
1291 for rule in arg["rules"]:
1292 action = rule.get("action", "allow")
1293 os_info = rule.get("os")
1294 if os_info:
1295 current_os = platform.system().lower()
1296 if os_info.get("name") == current_os:
1297 if action == "allow":
1298 is_allowed = True
1299 break
1300 elif action == "deny":
1301 is_allowed = False
1302 break
1303 else: # Rule applies to all OS
1304 if action == "allow":
1305 is_allowed = True
1306 break
1307 elif action == "deny":
1308 is_allowed = False
1309 break
1310 if is_allowed and "value" in arg:
1311 value = arg["value"]
1312 if isinstance(value, list): # Handle multi-value args like system properties
1313 jvm_args.extend(value)
1314 else:
1315 jvm_args.append(value)
1316
1317 # Parse game arguments
1318 game_arg_list = arguments.get("game", [])
1319 for arg in game_arg_list:
1320 if isinstance(arg, str):
1321 game_args.append(arg)
1322 elif isinstance(arg, dict):
1323 is_allowed = True # Simplified rule checking
1324 if "rules" in arg:
1325 is_allowed = False
1326 for rule in arg["rules"]:
1327 action = rule.get("action", "allow")
1328 if action == "allow" and ("features" not in rule or all(settings.get(f, False) for f in rule["features"])): # Basic feature check (not fully implemented)
1329 is_allowed = True
1330 break
1331 elif action == "deny" and ("features" not in rule or all(settings.get(f, False) for f in rule["features"])):
1332 is_allowed = False
1333 break
1334
1335 if is_allowed and "value" in arg:
1336 value = arg["value"]
1337 if isinstance(value, list):
1338 game_args.extend(value)
1339 else:
1340 game_args.append(value)
1341
1342 # Replace placeholders (basic implementation)
1343 replacements = {
1344 "${auth_player_name}": settings.get("username", "Player"),
1345 "${version_name}": version,
1346 "${game_directory}": ".", # Relative to the instance folder
1347 "${assets_root}": f'"{assets_path}"',
1348 "${assets_index}": version_info.get("assetIndex", {}).get("id", version),
1349 "${auth_uuid}": "0", # Dummy UUID for offline mode
1350 "${auth_access_token}": "null", # Dummy token for offline mode
1351 "${user_type}": "legacy", # or "mojang" depending on version/auth
1352 "${version_type}": version_info.get("type", "release"),
1353 # Add other common placeholders as needed
1354 "${natives_directory}": f'"{natives_path}"' # Added for newer JVM args
1355 }
1356 # Apply replacements to game args first (as they are more user-facing)
1357 processed_game_args = []
1358 for arg in game_args:
1359 processed_arg = arg
1360 for placeholder, value in replacements.items():
1361 processed_arg = processed_arg.replace(placeholder, str(value))
1362 processed_game_args.append(processed_arg)
1363
1364 # Apply replacements to JVM args
1365 processed_jvm_args = []
1366 for arg in jvm_args:
1367 processed_arg = arg
1368 for placeholder, value in replacements.items():
1369 processed_arg = processed_arg.replace(placeholder, str(value))
1370 processed_jvm_args.append(processed_arg)
1371
1372 final_game_args = processed_game_args
1373 final_jvm_args = processed_jvm_args
1374
1375
1376 elif mc_args: # Old style arguments (pre-1.13) - simpler placeholder replacement
1377 log_to_console(console_widget, f"Używam starych argumentów uruchamiania (version_info['minecraftArguments']).", "INFO")
1378 # Split old args string and replace placeholders
1379 arg_string = mc_args
1380 replacements = {
1381 "${auth_player_name}": settings.get("username", "Player"),
1382 "${version_name}": version,
1383 "${game_directory}": ".",
1384 "${assets_root}": f'"{assets_path}"',
1385 "${assets_index}": version_info.get("assetIndex", {}).get("id", version),
1386 "${auth_uuid}": "0",
1387 "${auth_access_token}": "null",
1388 "${user_type}": "legacy",
1389 "${version_type}": version_info.get("type", "release"),
1390 # Old format doesn't usually include natives dir in args, uses -Djava.library.path
1391 }
1392 # Split, replace, and join
1393 old_game_args = arg_string.split()
1394 final_game_args = []
1395 for arg in old_game_args:
1396 processed_arg = arg
1397 for placeholder, value in replacements.items():
1398 processed_arg = processed_arg.replace(placeholder, str(value))
1399 final_game_args.append(processed_arg)
1400
1401 # Old versions typically used default JVM args or few custom ones
1402 # Set some common default JVM args
1403 final_jvm_args = [
1404 f'-Djava.library.path="{natives_path}"', # Specify natives path
1405 '-Dorg.lwjgl.util.Debug=true' # Optional LWJGL debug
1406 ]
1407 # Add memory argument
1408 final_jvm_args.insert(0, f'-Xmx{settings.get("memory", "2")}G')
1409
1410
1411 else:
1412 log_to_console(console_widget, f"Brak argumentów uruchamiania w manifeście wersji {version}.", "WARNING")
1413 # Fallback to a basic set of arguments if manifest is weird
1414 main_class = "net.minecraft.client.main.Main" # Assume vanilla main class
1415 final_jvm_args = [
1416 f'-Xmx{settings.get("memory", "2")}G',
1417 f'-Djava.library.path="{natives_path}"'
1418 ]
1419 final_game_args = [
1420 f'--username', settings.get("username", "Player"),
1421 f'--version', version,
1422 f'--gameDir', '.',
1423 f'--assetsDir', f'"{assets_path}"',
1424 f'--assetIndex', version_info.get("assetIndex", {}).get("id", version),
1425 f'--accessToken', 'null',
1426 f'--uuid', '0',
1427 f'--userType', 'legacy'
1428 ]
1429 log_to_console(console_widget, f"Generowanie start.bat z domyślnymi argumentami.", "INFO")
1430
1431
1432 # Adjust main class and add loader args if a mod loader is selected
1433 loader_main_class = None
1434 loader_extra_jvm_args = []
1435 loader_extra_game_args = []
1436 loader_extra_classpath_entries = []
1437
1438 if loader_type and loader_type != "vanilla":
1439 log_to_console(console_widget, f"Instancja używa mod loadera: {loader_type}", "INFO")
1440 if loader_type == "fabric":
1441 # Fabric's main class and arguments vary slightly by version,
1442 # but net.fabricmc.loader.launch.knot.KnotClient is common for newer Fabric.
1443 # The fabric-loader-*.jar needs to be on the classpath *before* the client.jar.
1444 # The installer places necessary files (including fabric-loader-*.jar) into the libraries folder.
1445 loader_main_class = "net.fabricmc.loader.launch.knot.KnotClient" # Common for modern Fabric
1446 # Need to find the Fabric loader JAR in libraries and add it to classpath
1447 fabric_loader_jar = None
1448 # Search for a JAR matching a pattern, e.g., 'fabric-loader-*'
1449 try:
1450 for root, _, files in os.walk(libraries_dir):
1451 for fname in files:
1452 if fname.startswith("fabric-loader-") and fname.endswith(".jar"):
1453 fabric_loader_jar = os.path.join(root, fname)
1454 break
1455 if fabric_loader_jar: break
1456 except Exception as e:
1457 log_to_console(console_widget, f"Błąd szukania Fabric loader JAR: {e}", "ERROR")
1458
1459 if fabric_loader_jar and os.path.exists(fabric_loader_jar):
1460 loader_extra_classpath_entries.append(f'"{fabric_loader_jar}"')
1461 log_to_console(console_widget, f"Dodano Fabric loader JAR do class ścieżki: {fabric_loader_jar}", "INFO")
1462 else:
1463 log_to_console(console_widget, "Nie znaleziono Fabric loader JAR w folderze bibliotek. Upewnij się, że Fabric jest zainstalowany.", "WARNING")
1464 # Decide if this should be a fatal error or just a warning
1465 # For now, let's allow generation but log warning.
1466
1467 elif loader_type == "forge":
1468 # Forge's main class is typically cpw.mods.modlauncher.Launcher for newer versions.
1469 # Similar to Fabric, its main JAR needs to be on the classpath, often after libraries but before client.jar.
1470 # Installer places files in libraries and a 'forge-*.jar' or similar.
1471 loader_main_class = "cpw.mods.modlauncher.Launcher" # Common for modern Forge
1472 forge_launcher_jar = None
1473 # Search for forge launcher JAR
1474 try:
1475 # Forge might place it directly in the instance root or libraries
1476 candidate_paths = [
1477 os.path.join(version_dir, f"forge-{version}-{settings.get('loader_version', '')}-universal.jar"), # Example name
1478 os.path.join(version_dir, f"forge-{version}-{settings.get('loader_version', '')}-launcher.jar"), # Example name
1479 ]
1480 # Also search libraries dir more generally
1481 for root, _, files in os.walk(libraries_dir):
1482 for fname in files:
1483 if fname.startswith("forge-") and "universal" in fname and fname.endswith(".jar"):
1484 candidate_paths.append(os.path.join(root, fname))
1485 break # Found one, assume it's the right one for now
1486 if forge_launcher_jar: break
1487
1488 for p in candidate_paths:
1489 if os.path.exists(p):
1490 forge_launcher_jar = p
1491 break
1492
1493 except Exception as e:
1494 log_to_console(console_widget, f"Błąd szukania Forge launcher JAR: {e}", "ERROR")
1495
1496 if forge_launcher_jar and os.path.exists(forge_launcher_jar):
1497 # Forge's structure and required classpath entries can be complex.
1498 # Often the installer creates a JSON profile (like a mini version_info) in the instance folder
1499 # that contains the exact classpath and main class.
1500 # A robust launcher would parse this.
1501 # For this example, we'll just add the *presumed* main JAR and hope the installer handled the rest of the classpath.
1502 loader_extra_classpath_entries.append(f'"{forge_launcher_jar}"')
1503 log_to_console(console_widget, f"Dodano Forge launcher JAR do class ścieżki: {forge_launcher_jar}", "INFO")
1504
1505 # Forge might also need specific game arguments like --launchwrapper.tweaker
1506 # These are typically found in the installer-generated JSON profile.
1507 # Adding basic placeholder arguments for now.
1508 # loader_extra_game_args.extend(["--launchwrapper.tweaker", "cpw.mods.fml.common.launcher.FMLTweaker"]) # Example for older Forge
1509 pass # Placeholder for potential Forge args parsing
1510
1511 else:
1512 log_to_console(console_widget, "Nie znaleziono Forge launcher JAR. Upewnij się, że Forge jest zainstalowany.", "WARNING")
1513 # Decide if this is fatal... likely is for Forge.
1514
1515 # Add other loaders (NeoForge, etc.) here...
1516 # elif loader_type == "neoforge":
1517 # ...
1518
1519 # Prepend loader classpath entries if any
1520 if loader_extra_classpath_entries:
1521 classpath = ';'.join(loader_extra_classpath_entries) + ';' + classpath
1522
1523 # Use loader's main class if found, otherwise fallback to vanilla
1524 if loader_main_class:
1525 main_class = loader_main_class
1526 elif loader_type != "vanilla":
1527 log_to_console(console_widget, f"Nie znaleziono głównej klasy dla loadera '{loader_type}'. Używam domyślnej vanilla.", "ERROR")
1528 main_class = version_info.get("mainClass", "net.minecraft.client.main.Main") # Fallback
1529
1530
1531 # Add server arguments if specified
1532 if server_ip:
1533 loader_extra_game_args.extend(["--server", server_ip])
1534 if server_port:
1535 loader_extra_game_args.extend(["--port", server_port])
1536
1537 # Combine all game args
1538 final_game_args_combined = final_game_args + loader_extra_game_args
1539
1540 # Build the final command string
1541 # Use cmd /c start "" ... for Windows to run in a new window, or just the command
1542 # Using just the command is simpler for Popen/subprocess
1543 command_parts = []
1544 command_parts.append(f'"{java_path}"') # Java executable in quotes
1545 command_parts.extend(final_jvm_args) # JVM args
1546 command_parts.extend([f'-cp', f'"{classpath}"']) # Classpath
1547 command_parts.append(main_class) # Main class
1548 command_parts.extend(final_game_args_combined) # Game args
1549
1550 # Construct the final bat content
1551 # Escape special characters for .bat? No, python subprocess handles quotes well.
1552 # Need to quote paths with spaces. Already doing this.
1553 bat_content = f'@echo off\n'
1554 bat_content += f'title Minecraft {version} ({loader_type})\n'
1555 # Use PUSHD and POPD to ensure relative paths work correctly if batch file is run from elsewhere
1556 bat_content += f'PUSHD "{version_dir}"\n'
1557 bat_content += f'"{java_path}" ' # Java executable path
1558 bat_content += ' '.join([f'"{arg}"' if ' ' in arg and not arg.startswith('"') else arg for arg in final_jvm_args]) + ' ' # Quoted JVM args
1559 bat_content += f'-cp "{classpath}" ' # Classpath
1560 bat_content += main_class + ' ' # Main class
1561 bat_content += ' '.join([f'"{arg}"' if ' ' in arg and not arg.startswith('"') else arg for arg in final_game_args_combined]) # Quoted game args
1562 bat_content += '\n'
1563 bat_content += f'POPD\n' # Return to original directory
1564 bat_content += f'pause\n' # Keep window open after execution
1565
1566 try:
1567 with open(bat_path, "w", encoding="utf-8") as f:
1568 f.write(bat_content)
1569 log_to_console(console_widget, f"Plik {bat_path} został utworzony/zaktualizowany.", "SUCCESS")
1570 except Exception as e:
1571 log_to_console(console_widget, f"Błąd zapisu pliku start.bat {bat_path}: {e}", "ERROR")
1572 raise # Re-raise to indicate failure in generation
1573
1574
1575def update_progress(progress_bar, status_label, current_bytes, total_bytes, stage, current_item, total_items):
1576 """Updates the progress bar and status label."""
1577 if progress_bar and status_label:
1578 try:
1579 progress = (current_bytes / total_bytes) * 100 if total_bytes > 0 else 0
1580 progress_bar["value"] = progress
1581 remaining_bytes = max(0, total_bytes - current_bytes) # Ensure not negative
1582 remaining_str = humanize.naturalsize(remaining_bytes) if remaining_bytes > 0 else "0 B"
1583 total_str = humanize.naturalsize(total_bytes) if total_bytes > 0 else "Nieznany rozmiar"
1584
1585 status_label.config(text=f"[{stage}] Postęp: {progress:.1f}% | Pobrano: {humanize.naturalsize(current_bytes)} / {total_str} | {current_item}/{total_items}")
1586 root.update_idletasks() # Update GUI immediately
1587 except Exception as e:
1588 # This can happen if widgets are destroyed during download
1589 print(f"Error updating progress bar: {e}")
1590 pass
1591
1592
1593def download_version(version, instance_settings, console_widget):
1594 global download_active
1595 download_active = True
1596 instance_created = False # Flag to know if we successfully created the instance entry
1597
1598 try:
1599 log_to_console(console_widget, f"Rozpoczynam pobieranie instancji: {version}", "INFO")
1600 global_status_label.config(text="Pobieranie manifestu wersji...")
1601 root.update_idletasks()
1602
1603 info = get_version_info(version)
1604 if not info:
1605 log_to_console(console_widget, f"Nie udało się pobrać manifestu dla wersji {version}. Anulowano.", "ERROR")
1606 messagebox.showerror("Błąd pobierania", f"Nie udało się pobrać manifestu dla wersji {version}.")
1607 return # Exit thread
1608
1609 # Determine required Java version early
1610 java_major_version = get_required_java(version, info)
1611 log_to_console(console_widget, f"Wymagana wersja Javy: {java_major_version}", "INFO")
1612
1613 # Find or download Java
1614 global_status_label.config(text="Weryfikacja/pobieranie Javy...")
1615 root.update_idletasks()
1616 java_path_setting = instance_settings.get("java_path") # User specified path if any
1617
1618 if java_path_setting and os.path.exists(java_path_setting) and get_java_version(java_path_setting):
1619 # Use user-specified Java if valid
1620 java_path = java_path_setting
1621 java_version = get_java_version(java_path)
1622 log_to_console(console_widget, f"Używam wybranej Javy: {java_path} (wersja {java_version})", "INFO")
1623 else:
1624 # Find or download the required version
1625 java_paths = find_java(java_major_version)
1626 if not java_paths:
1627 log_to_console(console_widget, f"Nie znaleziono zainstalowanej Javy {java_major_version}.", "WARNING")
1628 # Ask user if they want to download
1629 if messagebox.askyesno("Brak Javy", f"Nie znaleziono 64-bitowej Javy {java_major_version}. Czy chcesz pobrać ją automatycznie?"):
1630 global_status_label.config(text=f"Pobieranie Javy {java_major_version}...")
1631 root.update_idletasks()
1632 java_path, java_version = download_java(java_major_version, console_widget)
1633 if not java_path:
1634 log_to_console(console_widget, f"Nie udało się pobrać Javy {java_major_version}!", "ERROR")
1635 messagebox.showerror("Błąd pobierania", f"Nie udało się pobrać Javy {java_major_version}. Anulowano instalację.")
1636 return # Exit thread
1637 log_to_console(console_widget, f"Pobrano i zainstalowano Javę: {java_path} (wersja: {java_version})", "SUCCESS")
1638 else:
1639 log_to_console(console_widget, f"Pobieranie Javy {java_major_version} anulowane przez użytkownika.", "WARNING")
1640 messagebox.showerror("Brak Javy", f"Nie wybrano/pobrano Javy {java_major_version}. Nie można zainstalować instancji.")
1641 return # Exit thread
1642 else:
1643 # Use the first suitable Java found
1644 java_path, java_version = java_paths[0]
1645 log_to_console(console_widget, f"Używam znalezionej Javy: {java_path} (wersja {java_version})", "INFO")
1646
1647
1648 # Prepare instance directory structure
1649 version_dir = os.path.join(BASE_DIR, "instances", version)
1650 os.makedirs(version_dir, exist_ok=True)
1651 # Sub-directories like 'mods', 'config', 'saves' can be created here too
1652 os.makedirs(os.path.join(version_dir, "mods"), exist_ok=True)
1653 os.makedirs(os.path.join(version_dir, "config"), exist_ok=True)
1654 os.makedirs(os.path.join(version_dir, "saves"), exist_ok=True)
1655 os.makedirs(os.path.join(version_dir, "resourcepacks"), exist_ok=True)
1656
1657
1658 # Add instance to global list and save config early
1659 # Mark as not ready initially
1660 instances[version] = {
1661 "path": version_dir,
1662 "java_path": java_path,
1663 "java_version": java_version,
1664 "required_java": java_major_version,
1665 "settings": instance_settings,
1666 "ready": False, # Not ready until all parts are verified
1667 "timestamp": datetime.now().isoformat()
1668 }
1669 save_config()
1670 instance_created = True # Instance entry is now in config
1671
1672 # --- Download Steps ---
1673 # Total progress tracking needs the sum of sizes for all steps.
1674 # Getting total sizes requires pre-fetching some info (assets, libraries).
1675 # A simpler approach for progress is to treat each major stage as a step (e.g., 5 steps).
1676 # Or, refine the update_progress to take current bytes / total bytes for the *current file*,
1677 # and overall stage progress (e.g., 1/5 stages complete). Let's use the latter.
1678
1679 # Step 1: Client.jar
1680 total_stages = 5 # Client, Libraries, Natives, Assets, start.bat
1681 global_status_label.config(text="[1/5] Pobieranie client.jar...")
1682 root.update_idletasks()
1683 client_download_info = info.get("downloads", {}).get("client")
1684 if not client_download_info:
1685 log_to_console(console_widget, "Brak informacji o pobieraniu client.jar.", "ERROR")
1686 messagebox.showerror("Błąd", "Brak informacji o pobieraniu client.jar w manifeście.")
1687 # Mark instance as not ready and return
1688 instances[version]["ready"] = False
1689 save_config()
1690 return
1691
1692 client_url = client_download_info.get("url")
1693 client_sha1 = client_download_info.get("sha1")
1694 client_size = client_download_info.get("size", 0)
1695 client_path = os.path.join(version_dir, f"{version}.jar")
1696
1697 if not os.path.exists(client_path) or not verify_sha1(client_path, client_sha1):
1698 if not download_file(
1699 client_url, client_path,
1700 lambda d, t: update_progress(global_progress_bar, global_status_label, d, t, "client.jar", 1, 1),
1701 expected_sha1=client_sha1, console_widget=console_widget, description="client.jar"
1702 ):
1703 log_to_console(console_widget, "Pobieranie client.jar nie powiodło się.", "ERROR")
1704 # Mark instance as not ready and return
1705 instances[version]["ready"] = False
1706 save_config()
1707 messagebox.showerror("Błąd pobierania", "Pobieranie client.jar nie powiodło się.")
1708 return
1709 else:
1710 log_to_console(console_widget, "client.jar już istnieje i jest poprawny. Pomijam pobieranie.", "INFO")
1711 update_progress(global_progress_bar, global_status_label, client_size, client_size, "client.jar", 1, 1) # Update progress visually
1712
1713 # Step 2: Libraries
1714 global_status_label.config(text="[2/5] Pobieranie bibliotek...")
1715 root.update_idletasks()
1716 # Pass a sub-progress callback that maps library progress to overall stage 2 progress
1717 lib_progress_callback = lambda current_b, total_b, stage_name, current_i, total_i: \
1718 update_progress(global_progress_bar, global_status_label, current_b, total_b, f"[2/5] {stage_name}", current_i, total_i)
1719
1720 if not download_libraries(info.get("libraries", []), version_dir, instance_settings.get("shared_libraries", True), console_widget, lib_progress_callback):
1721 log_to_console(console_widget, "Pobieranie bibliotek zakończone z błędami.", "WARNING")
1722 # Continue, verification will catch missing libs
1723
1724 # Step 3: Natives
1725 global_status_label.config(text="[3/5] Pobieranie natywnych bibliotek...")
1726 root.update_idletasks()
1727 native_progress_callback = lambda current_b, total_b, stage_name, current_i, total_i: \
1728 update_progress(global_progress_bar, global_status_label, current_b, total_b, f"[3/5] {stage_name}", current_i, total_i)
1729
1730 if not download_natives(info.get("libraries", []), version_dir, instance_settings.get("shared_natives", True), console_widget, native_progress_callback):
1731 log_to_console(console_widget, "Pobieranie natywnych bibliotek zakończone z błędami.", "WARNING")
1732 # Continue, verification will catch missing natives
1733
1734 # Step 4: Assets
1735 global_status_label.config(text="[4/5] Pobieranie assetów...")
1736 root.update_idletasks()
1737 asset_progress_callback = lambda current_b, total_b, stage_name, current_i, total_i: \
1738 update_progress(global_progress_bar, global_status_label, current_b, total_b, f"[4/5] {stage_name}", current_i, total_i)
1739
1740 if not download_assets(info, version_dir, instance_settings.get("shared_assets", True), console_widget, asset_progress_callback):
1741 log_to_console(console_widget, "Pobieranie assetów zakończone z błędami.", "WARNING")
1742 # Continue, verification will catch missing assets
1743
1744
1745 # Step 5: Generate start.bat
1746 global_status_label.config(text="[5/5] Generowanie start.bat...")
1747 root.update_idletasks()
1748 try:
1749 regenerate_start_bat(version, instances[version], info, console_widget)
1750 update_progress(global_progress_bar, global_status_label, 1, 1, "start.bat", 1, 1) # Indicate completion for this small step
1751 except Exception as e:
1752 log_to_console(console_widget, f"Nie udało się zregenerować start.bat: {e}. Instancja może nie działać poprawnie.", "ERROR")
1753 # This is a critical error, instance is not ready
1754 instances[version]["ready"] = False
1755 save_config()
1756 messagebox.showerror("Błąd", "Nie udało się wygenerować pliku start.bat. Sprawdź logi konsoli.")
1757 return
1758
1759
1760 # --- Finalization ---
1761 log_to_console(console_widget, f"Pobieranie dla instancji {version} zakończone. Weryfikuję pliki...", "INFO")
1762 global_status_label.config(text="Weryfikacja instancji...")
1763 root.update_idletasks()
1764
1765 # Run a final verification
1766 if verify_instance(version, console_widget):
1767 log_to_console(console_widget, f"Instancja {version} pobrana i zweryfikowana pomyślnie!", "SUCCESS")
1768 instances[version]["ready"] = True
1769 messagebox.showinfo("Sukces", f"Instancja {version} gotowa do uruchomienia!")
1770 else:
1771 log_to_console(console_widget, f"Instancja {version} pobrana, ale weryfikacja zakończyła się błędami. Sprawdź logi.", "WARNING")
1772 instances[version]["ready"] = False
1773 messagebox.showwarning("Uwaga", f"Instancja {version} została pobrana, ale wystąpiły błędy weryfikacji. Sprawdź logi konsoli.")
1774
1775 save_config() # Save final instance state
1776
1777 except requests.exceptions.RequestException as e:
1778 log_to_console(console_widget, f"Błąd sieci podczas pobierania instancji {version}: {e}", "ERROR")
1779 if instance_created:
1780 instances[version]["ready"] = False # Mark as not ready if failed after creation
1781 save_config()
1782 messagebox.showerror("Błąd sieci", f"Wystąpił błąd sieci podczas pobierania instancji {version}: {e}")
1783
1784 except Exception as e:
1785 log_to_console(console_widget, f"Ogólny błąd podczas pobierania instancji {version}: {e}", "ERROR")
1786 if instance_created:
1787 instances[version]["ready"] = False # Mark as not ready if failed after creation
1788 save_config()
1789 messagebox.showerror("Nieoczekiwany błąd", f"Wystąpił nieoczekiwany błąd podczas pobierania instancji {version}: {e}")
1790
1791 finally:
1792 # Ensure progress bar and status are reset regardless of success/failure
1793 if global_progress_bar:
1794 global_progress_bar["value"] = 0
1795 if global_status_label:
1796 global_status_label.config(text="Gotowe do działania!")
1797 refresh_instances() # Update list regardless
1798 download_active = False
1799
1800
1801def run_game(version):
1802 instance = instances.get(version)
1803 if not instance:
1804 messagebox.showerror("Błąd", f"Instancja {version} nie istnieje!")
1805 return
1806 if not instance.get("ready"):
1807 if messagebox.askyesno("Instancja niegotowa", f"Instancja {version} nie jest oznaczona jako gotowa. Spróbować ją zweryfikować przed uruchomieniem?"):
1808 # Verify first on main thread (or show console and verify there)
1809 # For simplicity, let's just warn and let the user verify manually via context menu
1810 # Alternatively, start a verification thread and then run if successful.
1811 # Let's just warn for now.
1812 messagebox.showwarning("Uwaga", f"Instancja {version} nie jest oznaczona jako gotowa. Może nie działać poprawnie.")
1813 # Proceed with attempting to run anyway
1814
1815 bat_path = os.path.join(instance["path"], "start.bat")
1816 if os.path.exists(bat_path):
1817 try:
1818 # Use subprocess.Popen to run without waiting and in a new window
1819 # shell=True is needed for .bat files
1820 log_to_console(console, f"Uruchamiam Minecraft {version} z '{bat_path}' w katalogu '{instance['path']}'", "INFO")
1821 subprocess.Popen(bat_path, cwd=instance["path"], shell=True)
1822 log_to_console(console, f"Uruchomiono Minecraft {version}", "SUCCESS")
1823 except Exception as e:
1824 log_to_console(console, f"Nie udało się uruchomić gry: {e}", "ERROR")
1825 messagebox.showerror("Błąd uruchamiania", f"Nie udało się uruchomić gry: {e}")
1826 else:
1827 log_to_console(console, f"Brak pliku start.bat dla instancji {version}. Spróbuj zweryfikować lub pobrać instancję ponownie.", "ERROR")
1828 messagebox.showerror("Błąd uruchamiania", f"Brak pliku start.bat dla {version}! Spróbuj zweryfikować lub pobrać instancję.")
1829
1830
1831def delete_instance(version):
1832 if messagebox.askyesno("Potwierdź usunięcie", f"Na pewno usunąć instancję {version}?\n\nSpowoduje to usunięcie folderu:\n{instances[version]['path']}"):
1833 instance = instances.get(version)
1834 if instance:
1835 import shutil
1836 instance_path = instance["path"]
1837 try:
1838 if os.path.exists(instance_path):
1839 shutil.rmtree(instance_path)
1840 log_to_console(console, f"Folder instancji {version} usunięty: {instance_path}", "INFO")
1841 del instances[version]
1842 save_config()
1843 log_to_console(console, f"Instancja {version} usunięta z konfiguracji.", "SUCCESS")
1844 refresh_instances()
1845 except Exception as e:
1846 log_to_console(console, f"Błąd usuwania instancji {version} w {instance_path}: {e}", "ERROR")
1847 messagebox.showerror("Błąd usuwania", f"Nie udało się usunąć instancji {version}: {e}\n\nSpróbuj usunąć folder ręcznie.")
1848
1849def open_instance_folder(version):
1850 instance = instances.get(version)
1851 if not instance:
1852 messagebox.showwarning("Błąd", "Wybierz instancję.")
1853 return
1854
1855 instance_path = instance["path"]
1856 if os.path.exists(instance_path):
1857 try:
1858 system = platform.system()
1859 if system == "Windows":
1860 os.startfile(instance_path)
1861 elif system == "Darwin": # macOS
1862 subprocess.Popen(["open", instance_path])
1863 else: # Linux and others
1864 subprocess.Popen(["xdg-open", instance_path])
1865 log_to_console(console, f"Otwieram folder instancji: {instance_path}", "INFO")
1866 except FileNotFoundError:
1867 messagebox.showerror("Błąd", f"Nie znaleziono programu do otwarcia folderu.")
1868 log_to_console(console, f"Nie znaleziono programu do otwarcia folderu {instance_path}", "ERROR")
1869 except Exception as e:
1870 messagebox.showerror("Błąd", f"Nie udało się otworzyć folderu: {e}")
1871 log_to_console(console, f"Nie udało się otworzyć folderu {instance_path}: {e}", "ERROR")
1872 else:
1873 messagebox.showwarning("Uwaga", f"Folder instancji nie istnieje:\n{instance_path}")
1874 log_to_console(console, f"Próba otwarcia nieistniejącego folderu instancji: {instance_path}", "WARNING")
1875
1876
1877def copy_console_content(console_widget):
1878 if not console_widget: return
1879 try:
1880 console_widget.config(state="normal")
1881 content = console_widget.get("1.0", tk.END).strip()
1882 pyperclip.copy(content)
1883 console_widget.config(state="disabled")
1884 messagebox.showinfo("Skopiowano", "Zawartość konsoli skopiowana do schowka.")
1885 log_to_console(console_widget, "Zawartość konsoli skopiowana do schowka.", "INFO")
1886 except pyperclip.PyperclipException as e:
1887 messagebox.showerror("Błąd", f"Nie udało się skopiować do schowka: {e}")
1888 log_to_console(console_widget, f"Błąd kopiowania do schowka: {e}", "ERROR")
1889 except Exception as e:
1890 messagebox.showerror("Błąd", f"Nieoczekiwany błąd podczas kopiowania: {e}")
1891 log_to_console(console_widget, f"Nieoczekiwany błąd podczas kopiowania: {e}", "ERROR")
1892
1893
1894def clear_console_content(console_widget):
1895 if not console_widget: return
1896 if messagebox.askyesno("Potwierdź", "Na pewno wyczyścić zawartość konsoli?"):
1897 try:
1898 console_widget.config(state="normal")
1899 console_widget.delete("1.0", tk.END)
1900 console_widget.config(state="disabled")
1901 log_to_console(console_widget, "Konsola została wyczyszczona.", "INFO") # This log won't appear unless added before clear
1902 except Exception as e:
1903 log_to_console(console_widget, f"Błąd czyszczenia konsoli: {e}", "ERROR")
1904
1905
1906def import_config():
1907 file_path = filedialog.askopenfilename(
1908 title="Wybierz plik konfiguracji",
1909 filetypes=(("JSON files", "*.json"), ("All files", "*.*"))
1910 )
1911 if not file_path:
1912 return
1913
1914 try:
1915 with open(file_path, "r", encoding="utf-8") as f:
1916 imported_config = json.load(f)
1917
1918 # Merge/overwrite current configuration
1919 # Ask user how to handle existing instances? For simplicity, just overwrite if key exists.
1920 if messagebox.askyesno("Import konfiguracji", "Czy chcesz zastąpić obecne instancje i ustawienia importowaną konfiguracją?\n(Zaleca się wykonanie eksportu przed importem!)"):
1921 global instances, java_versions_cache
1922 # Reset to imported state
1923 instances = imported_config.get("instances", {})
1924 java_versions_cache = imported_config.get("java_versions", {})
1925
1926 # Update default settings variables (optional, could just load them next time)
1927 default_settings = imported_config.get("default_settings", {})
1928 username_var.set(default_settings.get("username", "Player"))
1929 memory_var.set(default_settings.get("memory", "2"))
1930 shared_assets_var.set(default_settings.get("shared_assets", True))
1931 shared_libraries_var.set(default_settings.get("shared_libraries", True))
1932 shared_natives_var.set(default_settings.get("shared_natives", True))
1933
1934 # Update version filters variables
1935 version_filters = imported_config.get("version_filters", {})
1936 snapshots_var.set(version_filters.get("snapshots", True))
1937 releases_var.set(version_filters.get("releases", True))
1938 alpha_var.set(version_filters.get("alpha", False))
1939 beta_var.set(version_filters.get("beta", False))
1940
1941
1942 save_config() # Save the merged config
1943 log_to_console(console, f"Konfiguracja wczytana z {file_path}.", "SUCCESS")
1944 messagebox.showinfo("Sukces", "Konfiguracja została zaimportowana.")
1945 refresh_instances() # Refresh GUI based on new data
1946 else:
1947 log_to_console(console, "Import konfiguracji anulowany przez użytkownika.", "INFO")
1948
1949 except FileNotFoundError:
1950 messagebox.showerror("Błąd importu", "Wybrany plik nie istnieje.")
1951 log_to_console(console, f"Błąd importu: Plik nie znaleziono {file_path}", "ERROR")
1952 except json.JSONDecodeError:
1953 messagebox.showerror("Błąd importu", "Nieprawidłowy format pliku JSON.")
1954 log_to_console(console, f"Błąd importu: Nieprawidłowy format JSON w {file_path}", "ERROR")
1955 except Exception as e:
1956 messagebox.showerror("Błąd importu", f"Nieoczekiwany błąd podczas importu: {e}")
1957 log_to_console(console, f"Nieoczekiwany błąd podczas importu z {file_path}: {e}", "ERROR")
1958
1959
1960def export_config():
1961 file_path = filedialog.asksaveasfilename(
1962 title="Zapisz plik konfiguracji",
1963 defaultextension=".json",
1964 filetypes=(("JSON files", "*.json"), ("All files", "*.*"))
1965 )
1966 if not file_path:
1967 return
1968
1969 try:
1970 # Get current configuration structure
1971 config = {
1972 "default_settings": {
1973 "username": username_var.get(),
1974 "memory": memory_var.get(),
1975 "shared_assets": shared_assets_var.get(),
1976 "shared_libraries": shared_libraries_var.get(),
1977 "shared_natives": shared_natives_var.get()
1978 },
1979 "version_filters": {
1980 "snapshots": snapshots_var.get(),
1981 "releases": releases_var.get(),
1982 "alpha": alpha_var.get(),
1983 "beta": beta_var.get()
1984 },
1985 "instances": instances,
1986 "java_versions": java_versions_cache
1987 }
1988
1989 with open(file_path, "w", encoding="utf-8") as f:
1990 json.dump(config, f, indent=4)
1991
1992 log_to_console(console, f"Konfiguracja wyeksportowana do {file_path}.", "SUCCESS")
1993 messagebox.showinfo("Sukces", f"Konfiguracja została wyeksportowana do:\n{file_path}")
1994
1995 except Exception as e:
1996 messagebox.showerror("Błąd eksportu", f"Nie udało się wyeksportować konfiguracji: {e}")
1997 log_to_console(console, f"Błąd eksportu do {file_path}: {e}", "ERROR")
1998
1999
2000# --- GUI Functions ---
2001def switch_tab(tab_name):
2002 global download_active
2003 # Check if a download is active before switching away from Download tab
2004 if download_active and current_tab.get() == "Pobieranie" and tab_name != "Pobieranie":
2005 if not messagebox.askyesno("Ostrzeżenie", "Pobieranie w toku! Zmiana zakładki przerwie proces. Kontynuować?"):
2006 return
2007 log_to_console(console, "Pobieranie przerwane przez użytkownika (zmiana zakładki).", "WARNING")
2008 download_active = False # Stop the download flag
2009 # The download thread will likely finish its current operation or encounter an error and exit
2010
2011 current_tab.set(tab_name)
2012 for name, btn in tab_buttons.items():
2013 if name == tab_name:
2014 btn.config(bg=ACTIVE_TAB_COLOR, relief="sunken")
2015 else:
2016 btn.config(bg=SIDEBAR_BG, relief="flat")
2017 # Re-bind hover effects only for non-active tabs
2018 if name != tab_name:
2019 btn.bind("<Enter>", lambda e, b=btn: b.config(bg=HOVER_TAB_COLOR))
2020 btn.bind("<Leave>", lambda e, b=btn: b.config(bg=SIDEBAR_BG))
2021 else:
2022 btn.unbind("<Enter>")
2023 btn.unbind("<Leave>")
2024
2025
2026 # Clear current content frame
2027 for widget in content_frame.winfo_children():
2028 widget.destroy()
2029
2030 # Show content for the selected tab
2031 if tab_name == "Instancje":
2032 show_instances()
2033 elif tab_name == "Pobieranie":
2034 show_download()
2035 elif tab_name == "Ustawienia":
2036 show_settings()
2037 elif tab_name == "Konsola":
2038 show_console()
2039 elif tab_name == "Modrinth":
2040 show_modrinth_browser()
2041 elif tab_name == "Narzędzia":
2042 show_tools() # Import/Export and other tools
2043
2044
2045def show_instances():
2046 frame = ttk.Frame(content_frame, padding="10")
2047 frame.pack(fill="both", expand=True)
2048
2049 header_frame = ttk.Frame(frame)
2050 header_frame.pack(fill="x", pady=(0, 10))
2051 ttk.Label(header_frame, text="Twoje instancje Minecraft", style="TLabel", font=("Segoe UI", 14, "bold")).pack(side="left")
2052
2053 btn_frame = ttk.Frame(header_frame)
2054 btn_frame.pack(side="right")
2055
2056 # Ensure global_progress_bar and global_status_label are visible if needed elsewhere
2057 # They are packed at the bottom of the root window, so they are always visible.
2058
2059 columns = ("version", "loader", "java", "status", "date", "path") # Added loader and path
2060 tree = ttk.Treeview(
2061 frame, columns=columns, show="headings", selectmode="browse",
2062 style="Treeview"
2063 )
2064
2065 tree.heading("version", text="Wersja", anchor="w")
2066 tree.heading("loader", text="Loader", anchor="w")
2067 tree.heading("java", text="Java", anchor="w")
2068 tree.heading("status", text="Status", anchor="w")
2069 tree.heading("date", text="Utworzona", anchor="w")
2070 tree.heading("path", text="Ścieżka", anchor="w") # Hidden by default
2071
2072 tree.column("version", width=100, anchor="w", stretch=tk.NO)
2073 tree.column("loader", width=80, anchor="w", stretch=tk.NO)
2074 tree.column("java", width=100, anchor="w", stretch=tk.NO)
2075 tree.column("status", width=80, anchor="w", stretch=tk.NO)
2076 tree.column("date", width=120, anchor="w", stretch=tk.NO)
2077 tree.column("path", width=250, anchor="w") # Default width, can be expanded
2078
2079 # Hide the 'path' column
2080 tree.column("path", width=0, stretch=tk.NO)
2081 tree.heading("path", text="") # Clear header text when hidden
2082
2083 scrollbar = ttk.Scrollbar(frame, orient="vertical", command=tree.yview)
2084 scrollbar.pack(side="right", fill="y")
2085 tree.configure(yscrollcommand=scrollbar.set)
2086 tree.pack(side="left", fill="both", expand=True)
2087
2088 # Populate the treeview
2089 populate_instances_treeview(tree)
2090
2091 # Context Menu (Right-click)
2092 instance_context_menu = tk.Menu(root, tearoff=0)
2093 instance_context_menu.add_command(label="Uruchom", command=lambda: run_selected_instance(tree))
2094 instance_context_menu.add_command(label="Edytuj", command=lambda: edit_selected_instance(tree))
2095 instance_context_menu.add_command(label="Weryfikuj", command=lambda: verify_selected_instance(tree))
2096 instance_context_menu.add_command(label="Otwórz folder", command=lambda: open_selected_instance_folder(tree))
2097 instance_context_menu.add_separator()
2098 instance_context_menu.add_command(label="Zmień nazwę", command=lambda: rename_selected_instance(tree))
2099 instance_context_menu.add_command(label="Duplikuj", command=lambda: duplicate_selected_instance(tree))
2100 instance_context_menu.add_command(label="Eksportuj", command=lambda: export_selected_instance(tree))
2101 instance_context_menu.add_separator()
2102 instance_context_menu.add_command(label="Usuń", command=lambda: delete_selected_instance(tree))
2103
2104
2105 def show_context_menu(event):
2106 selected_item = tree.focus()
2107 if selected_item:
2108 try:
2109 instance_context_menu.tk_popup(event.x_root, event.y_root)
2110 finally:
2111 instance_context_menu.grab_release()
2112
2113 tree.bind("<Button-3>", show_context_menu) # Bind right-click
2114
2115
2116 # Action Buttons (below the treeview)
2117 action_frame = ttk.Frame(frame)
2118 action_frame.pack(fill="x", pady=(10, 0))
2119
2120 ttk.Button(action_frame, text="Nowa instancja", command=create_instance, style="TButton", width=18).pack(side="left", padx=5)
2121 ttk.Button(action_frame, text="Uruchom Wybraną", command=lambda: run_selected_instance(tree), style="TButton", width=18).pack(side="left", padx=5)
2122 ttk.Button(action_frame, text="Edytuj Wybraną", command=lambda: edit_selected_instance(tree), style="TButton", width=18).pack(side="left", padx=5)
2123 ttk.Button(action_frame, text="Usuń Wybraną", command=lambda: delete_selected_instance(tree), style="TButton", width=18).pack(side="right", padx=5)
2124 ttk.Button(action_frame, text="Weryfikuj Wybraną", command=lambda: verify_selected_instance(tree), style="TButton", width=18).pack(side="right", padx=5)
2125
2126
2127 ttk.Label(frame, text="Wybierz instancję z listy, kliknij prawym przyciskiem lub użyj przycisków poniżej.", style="TLabel", font=("Segoe UI", 9)).pack(fill="x", pady=(10, 0))
2128
2129def populate_instances_treeview(tree):
2130 """Clears and repopulates the instance treeview."""
2131 # Clear existing items
2132 for item in tree.get_children():
2133 tree.delete(item)
2134
2135 # Sort instances by version (newest first)
2136 # Get list of versions, sort using pkg_version.parse if possible
2137 sorted_versions = sorted(instances.keys(), key=lambda v: pkg_version.parse(v) if v[0].isdigit() else v, reverse=True)
2138
2139 # Add instances to treeview
2140 for version in sorted_versions:
2141 data = instances[version] # Get data using the sorted version key
2142 instance_settings = data.get("settings", {})
2143 tree.insert("", "end", iid=version, values=(
2144 version,
2145 instance_settings.get("loader_type", "vanilla").capitalize(),
2146 data.get("java_version", "?"),
2147 "Gotowe" if data.get("ready", False) else "Błąd/Niegotowe",
2148 datetime.fromisoformat(data["timestamp"]).strftime("%Y-%m-%d %H:%M") if "timestamp" in data else "Nieznana",
2149 data["path"]
2150 ))
2151
2152def refresh_instances():
2153 """Refreshes the instances tab GUI."""
2154 # Check if the current tab is 'Instancje' before refreshing the GUI
2155 if current_tab.get() == "Instancje":
2156 for widget in content_frame.winfo_children():
2157 widget.destroy()
2158 show_instances()
2159 # If not on the instances tab, just update the data and rely on switching back to refresh
2160
2161
2162def get_selected_instance_version(tree):
2163 """Gets the version of the currently selected item in the treeview."""
2164 selected_item = tree.focus()
2165 if selected_item:
2166 # Assuming the version is stored as the iid or the first value
2167 return tree.item(selected_item, "iid") # Using iid is safer if values order changes
2168 # return tree.item(selected_item)["values"][0] # Alternative if version is always the first column
2169 return None # Nothing selected
2170
2171def run_selected_instance(tree):
2172 version = get_selected_instance_version(tree)
2173 if version:
2174 run_game(version)
2175 else:
2176 messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby ją uruchomić.")
2177
2178def edit_selected_instance(tree):
2179 version = get_selected_instance_version(tree)
2180 if version:
2181 edit_instance(version)
2182 else:
2183 messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby ją edytować.")
2184
2185def verify_selected_instance(tree):
2186 version = get_selected_instance_version(tree)
2187 if version:
2188 # Verification can take time, run in a thread and show console
2189 switch_tab("Konsola") # Switch to console tab to see progress
2190 threading.Thread(target=verify_instance, args=(version, console), daemon=True).start()
2191 else:
2192 messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby ją zweryfikować.")
2193
2194def delete_selected_instance(tree):
2195 version = get_selected_instance_version(tree)
2196 if version:
2197 delete_instance(version)
2198 else:
2199 messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby ją usunąć.")
2200
2201def open_selected_instance_folder(tree):
2202 version = get_selected_instance_version(tree)
2203 if version:
2204 open_instance_folder(version)
2205 else:
2206 messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby otworzyć jej folder.")
2207
2208def rename_selected_instance(tree):
2209 version = get_selected_instance_version(tree)
2210 if not version:
2211 messagebox.showwarning("Uwaga", "Wybierz instancję do zmiany nazwy.")
2212 return
2213 # NOTE: Minecraft instances are fundamentally tied to their version ID as the folder name.
2214 # Renaming here would only change the display name *in the launcher*.
2215 # This is a complex feature as it clashes with the folder structure.
2216 # For now, let's add a note field instead of renaming.
2217
2218 messagebox.showinfo("Funkcja niedostępna", "Zmiana nazwy instancji nie jest obecnie obsługiwana, ponieważ nazwa jest powiązana z wersją Minecrafta.")
2219 # Implement adding a note instead if desired
2220 # add_instance_note(version)
2221
2222
2223def duplicate_selected_instance(tree):
2224 version = get_selected_instance_version(tree)
2225 if not version:
2226 messagebox.showwarning("Uwaga", "Wybierz instancję do zduplikowania.")
2227 return
2228
2229 messagebox.showinfo("Funkcja niedostępna", "Duplikowanie instancji nie jest obecnie zaimplementowane.")
2230 # This would involve copying the instance folder and adding a new entry in the config.
2231
2232def export_selected_instance(tree):
2233 version = get_selected_instance_version(tree)
2234 if not version:
2235 messagebox.showwarning("Uwaga", "Wybierz instancję do eksportu.")
2236 return
2237
2238 # Exporting a single instance as a zip file containing its folder and config snippet
2239 instance = instances.get(version)
2240 if not instance:
2241 messagebox.showerror("Błąd", "Dane instancji nie znaleziono.")
2242 return
2243
2244 export_path = filedialog.asksaveasfilename(
2245 title=f"Eksportuj instancję {version}",
2246 initialfile=f"minecraft_instance_{version}.zip",
2247 defaultextension=".zip",
2248 filetypes=(("Zip files", "*.zip"), ("All files", "*.*"))
2249 )
2250 if not export_path:
2251 return
2252
2253 try:
2254 # Create a temporary directory to prepare the export
2255 temp_dir = os.path.join(BASE_DIR, "temp_export", version)
2256 os.makedirs(temp_dir, exist_ok=True)
2257
2258 # Copy the instance folder content (excluding potentially large logs/temp files)
2259 instance_folder = instance["path"]
2260 if os.path.exists(instance_folder):
2261 # Use a simpler copy tree, might need exclusions for larger projects
2262 shutil.copytree(instance_folder, os.path.join(temp_dir, version), dirs_exist_ok=True,
2263 ignore=shutil.ignore_patterns('logs', 'temp_*', '*.log', '*.tmp'))
2264 else:
2265 messagebox.showwarning("Uwaga", f"Folder instancji {version} nie istnieje. Eksportowana będzie tylko konfiguracja.")
2266 log_to_console(console, f"Folder instancji {version} nie istnieje, eksportowana tylko konfiguracja.", "WARNING")
2267
2268
2269 # Save a snippet of the config for this instance in the temp dir
2270 instance_config_snippet = {version: instance}
2271 with open(os.path.join(temp_dir, "instance_config.json"), "w", encoding="utf-8") as f:
2272 json.dump(instance_config_snippet, f, indent=4)
2273
2274 # Create the zip archive
2275 with zipfile.ZipFile(export_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
2276 for root_dir, _, files in os.walk(temp_dir):
2277 for file in files:
2278 file_path = os.path.join(root_dir, file)
2279 # Create relative path inside the zip
2280 relative_path = os.path.relpath(file_path, temp_dir)
2281 zipf.write(file_path, arcname=relative_path)
2282
2283 # Clean up the temporary directory
2284 shutil.rmtree(temp_dir)
2285
2286 log_to_console(console, f"Instancja {version} wyeksportowana do {export_path}.", "SUCCESS")
2287 messagebox.showinfo("Sukces", f"Instancja {version} została wyeksportowana do:\n{export_path}")
2288
2289 except Exception as e:
2290 messagebox.showerror("Błąd eksportu", f"Nie udało się wyeksportować instancji {version}: {e}")
2291 log_to_console(console, f"Błąd eksportu instancji {version}: {e}", "ERROR")
2292
2293
2294def create_instance():
2295 window = tk.Toplevel(root)
2296 window.title("Nowa instancja Minecraft")
2297 window.geometry("550x700") # Slightly larger
2298 window.configure(bg=PRIMARY_BG)
2299 window.resizable(False, False)
2300
2301 style = ttk.Style() # Ensure style is available in Toplevel
2302 style.configure("TFrame", background=PRIMARY_BG)
2303 style.configure("TLabel", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10))
2304 style.configure("TButton", background=BUTTON_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10), borderwidth=0)
2305 style.map("TButton",
2306 background=[('active', BUTTON_HOVER), ('pressed', BUTTON_HOVER)],
2307 foreground=[('active', PRIMARY_FG), ('pressed', PRIMARY_FG)])
2308 style.configure("TEntry", fieldbackground="#333333", foreground=PRIMARY_FG, insertcolor=PRIMARY_FG, borderwidth=1)
2309 style.configure("TCombobox", fieldbackground="#333333", foreground=PRIMARY_FG, selectbackground=ACCENT_COLOR, borderwidth=1)
2310 style.configure("TCheckbutton", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 9)) # Style for ttk.Checkbutton
2311
2312 frame = ttk.Frame(window, padding="15")
2313 frame.pack(fill="both", expand=True)
2314
2315 ttk.Label(frame, text="Tworzenie nowej instancji", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
2316
2317 form_frame = ttk.Frame(frame)
2318 form_frame.pack(fill="x", expand=False) # Don't expand form vertically
2319 form_frame.columnconfigure(1, weight=1) # Allow the second column to expand
2320
2321 # Row 0: Version Filters
2322 ttk.Label(form_frame, text="Filtry wersji:", style="TLabel").grid(row=0, column=0, sticky="nw", pady=(0, 5))
2323 filters_frame = ttk.Frame(form_frame)
2324 filters_frame.grid(row=0, column=1, sticky="ew", pady=(0, 5), padx=(5, 0))
2325
2326 # Use tk.Checkbutton for checkboxes as ttk.Checkbutton styling is complex
2327 tk.Checkbutton(filters_frame, text="Snapshoty", variable=snapshots_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
2328 selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
2329 command=lambda: [save_config(), refresh_version_combo_dialog(version_combo, version_var)]).pack(anchor="w", side="left", padx=5)
2330 tk.Checkbutton(filters_frame, text="Release", variable=releases_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
2331 selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
2332 command=lambda: [save_config(), refresh_version_combo_dialog(version_combo, version_var)]).pack(anchor="w", side="left", padx=5)
2333 tk.Checkbutton(filters_frame, text="Alpha", variable=alpha_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
2334 selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
2335 command=lambda: [save_config(), refresh_version_combo_dialog(version_combo, version_var)]).pack(anchor="w", side="left", padx=5)
2336 tk.Checkbutton(filters_frame, text="Beta", variable=beta_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
2337 selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
2338 command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
2339
2340
2341 # Row 1: Minecraft Version
2342 ttk.Label(form_frame, text="Wersja Minecrafta:", style="TLabel").grid(row=1, column=0, sticky="w", pady=(10, 5))
2343 version_var = tk.StringVar()
2344 versions = get_versions()
2345 version_combo = ttk.Combobox(form_frame, textvariable=version_var, state="readonly", values=versions, style="TCombobox")
2346 version_combo.grid(row=1, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2347 if versions:
2348 version_combo.current(0)
2349 else:
2350 version_var.set("Brak wersji!")
2351 version_combo.config(state="disabled")
2352 messagebox.showwarning("Uwaga", "Nie znaleziono żadnych wersji Minecrafta. Sprawdź filtry i połączenie z internetem.")
2353
2354 # Row 2: Username
2355 ttk.Label(form_frame, text="Nazwa użytkownika:", style="TLabel").grid(row=2, column=0, sticky="w", pady=(10, 5))
2356 username_entry = ttk.Entry(form_frame, style="TEntry")
2357 username_entry.insert(0, username_var.get())
2358 username_entry.grid(row=2, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2359
2360 # Row 3: Memory
2361 ttk.Label(form_frame, text="Pamięć RAM (GB):", style="TLabel").grid(row=3, column=0, sticky="w", pady=(10, 5))
2362 memory_spin = tk.Spinbox(form_frame, from_=1, to=32, width=5, bg="#333333", fg=PRIMARY_FG, highlightthickness=0,
2363 buttonbackground=TERTIARY_BG, buttonforeground=PRIMARY_FG, insertbackground=PRIMARY_FG)
2364 memory_spin.delete(0, tk.END)
2365 memory_spin.insert(0, memory_var.get())
2366 memory_spin.grid(row=3, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
2367
2368 # Row 4: Java Path
2369 ttk.Label(form_frame, text="Ścieżka Java:", style="TLabel").grid(row=4, column=0, sticky="w", pady=(10, 5))
2370 java_var = tk.StringVar()
2371 java_paths_found = find_java()
2372 java_options = [f"{p} (v{v} - {src})" for p, v, src in find_java_with_source()] # Include source in display
2373 java_combo = ttk.Combobox(form_frame, textvariable=java_var, state="readonly", values=java_options, style="TCombobox")
2374 java_combo.grid(row=4, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2375 java_combo.set("Szukam Javy...") # Initial text
2376
2377 # Store (path, version) tuples corresponding to the options list
2378 java_path_tuples = find_java() # Use the original list without source for value mapping
2379
2380 # Auto-select best Java based on default/selected MC version
2381 def update_java_selection(event=None):
2382 selected_mc_version = version_var.get()
2383 if not selected_mc_version or not java_path_tuples:
2384 java_combo.set("Brak pasującej Javy")
2385 java_combo.config(state="disabled")
2386 return
2387
2388 java_combo.config(state="readonly") # Enable if versions found
2389
2390 # Fetch version info to get required Java (can be slow, maybe cache?)
2391 # For simplicity in dialog, might use a background fetch or rely on find_java
2392 # Let's just find based on the list we already have.
2393 # A more robust way would fetch manifest and check required version
2394
2395 # Use a simplified required version based on MC version string if manifest not fetched
2396 # OR pre-fetch all manifests? No, too much data.
2397 # Let's just use the version string heuristic for the dialog picker initially.
2398 # The main download logic will do a proper manifest check.
2399 required_java_heuristic = "1.8" # Default
2400 try:
2401 mc_v = pkg_version.parse(selected_mc_version)
2402 if mc_v >= pkg_version.parse("1.20.5"): required_java_heuristic = "21"
2403 elif mc_v >= pkg_version.parse("1.18"): required_java_heuristic = "17"
2404 elif mc_v >= pkg_version.parse("1.17"): required_java_heuristic = "16"
2405 elif mc_v >= pkg_version.parse("1.6"): required_java_heuristic = "1.8"
2406 except pkg_version.InvalidVersion:
2407 pass # Keep default heuristic
2408
2409 best_java_index = -1
2410 for i, (path, ver) in enumerate(java_path_tuples):
2411 if check_java_version(ver, required_java_heuristic):
2412 best_java_index = i
2413 break # Found a suitable one, take the first (highest priority from find_java)
2414
2415 if best_java_index != -1:
2416 java_combo.current(best_java_index)
2417 elif java_path_tuples: # If no suitable Java found, select the first available one
2418 java_combo.current(0)
2419 else: # No Java found at all
2420 java_combo.set("Brak Javy 64-bit")
2421 java_combo.config(state="disabled")
2422 messagebox.showwarning("Brak Javy", "Nie znaleziono żadnej pasującej 64-bitowej instalacji Javy. Upewnij się, że Java 64-bit jest zainstalowana.")
2423
2424
2425 version_combo.bind("<<ComboboxSelected>>", update_java_selection)
2426 # Perform initial selection after populating
2427 if java_options:
2428 update_java_selection()
2429
2430
2431 # Row 5: Mod Loader Type
2432 ttk.Label(form_frame, text="Mod Loader:", style="TLabel").grid(row=5, column=0, sticky="w", pady=(10, 5))
2433 loader_var = tk.StringVar(value="vanilla")
2434 # Add loaders here as supported
2435 loader_options = ["vanilla", "fabric", "forge"] # Add "neoforge" etc. when supported
2436 loader_combo = ttk.Combobox(form_frame, textvariable=loader_var, state="readonly", values=loader_options, style="TCombobox")
2437 loader_combo.grid(row=5, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2438 loader_combo.current(0) # Default to vanilla
2439
2440 # Row 6: Mod Loader Version (Optional, depends on loader)
2441 # This is complex as versions depend on MC version and loader type.
2442 # For simplicity, we won't auto-populate this dropdown in the dialog.
2443 # User might need to manually enter a known working version or run the installer later.
2444 # Alternatively, fetch available loader versions based on selected MC version.
2445 # Let's omit loader version selection in the dialog for now and assume user runs installer.
2446 # The instance config will store the *type*, and start.bat will adapt if loader files are present.
2447 # Added a placeholder label for loader version if we decide to add it later.
2448 # ttk.Label(form_frame, text="Loader Version:", style="TLabel").grid(row=6, column=0, sticky="w", pady=(10, 5))
2449 # loader_version_var = tk.StringVar()
2450 # ttk.Entry(form_frame, textvariable=loader_version_var, style="TEntry").grid(row=6, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2451 # Note: The launcher doesn't auto-install loaders yet, user must run installer JARs.
2452
2453 # Row 7: Shared Folders
2454 ttk.Label(form_frame, text="Opcje współdzielenia:", style="TLabel").grid(row=7, column=0, sticky="nw", pady=(10, 5))
2455 options_frame = ttk.Frame(form_frame)
2456 options_frame.grid(row=7, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2457
2458 shared_assets = tk.BooleanVar(value=shared_assets_var.get())
2459 shared_libs = tk.BooleanVar(value=shared_libraries_var.get())
2460 shared_natives = tk.BooleanVar(value=shared_natives_var.get())
2461
2462 tk.Checkbutton(options_frame, text="Współdziel assets", variable=shared_assets,
2463 bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
2464 font=("Segoe UI", 9)).pack(anchor="w", pady=2)
2465 tk.Checkbutton(options_frame, text="Współdziel biblioteki", variable=shared_libs,
2466 bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
2467 font=("Segoe UI", 9)).pack(anchor="w", pady=2)
2468 tk.Checkbutton(options_frame, text="Współdziel natywne biblioteki", variable=shared_natives,
2469 bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
2470 font=("Segoe UI", 9)).pack(anchor="w", pady=2)
2471
2472 # Row 8: Server Address (Optional)
2473 ttk.Label(form_frame, text="Adres serwera (opcjonalnie):", style="TLabel").grid(row=8, column=0, sticky="w", pady=(10, 5))
2474 server_ip_var = tk.StringVar()
2475 server_ip_entry = ttk.Entry(form_frame, textvariable=server_ip_var, style="TEntry")
2476 server_ip_entry.grid(row=8, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2477
2478 ttk.Label(form_frame, text="Port serwera (opcjonalnie):", style="TLabel").grid(row=9, column=0, sticky="w", pady=(10, 5))
2479 server_port_var = tk.StringVar()
2480 server_port_entry = ttk.Entry(form_frame, textvariable=server_port_var, style="TEntry", width=8) # Smaller width
2481 server_port_entry.grid(row=9, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
2482
2483
2484 # Action buttons at the bottom
2485 btn_frame = ttk.Frame(frame)
2486 btn_frame.pack(fill="x", pady=(15, 0))
2487
2488 ttk.Button(btn_frame, text="Anuluj", command=window.destroy, style="TButton", width=15).pack(side="right", padx=5)
2489 ttk.Button(btn_frame, text="Utwórz Instancję", command=lambda: (
2490 # Validate version selection
2491 selected_version := version_var.get(),
2492 selected_java_option := java_combo.get(),
2493 selected_java_path := java_path_tuples[java_combo.current()][0] if java_combo.current() != -1 else "",
2494
2495 process_create_instance(
2496 selected_version,
2497 {
2498 "username": username_entry.get().strip() or "Player", # Ensure username is not empty
2499 "memory": memory_spin.get(),
2500 "java_path": selected_java_path,
2501 "shared_assets": shared_assets.get(),
2502 "shared_libraries": shared_libs.get(),
2503 "shared_natives": shared_natives.get(),
2504 "loader_type": loader_var.get(),
2505 "loader_version": "", # Loader version not selected in this dialog
2506 "server_ip": server_ip_var.get().strip(),
2507 "server_port": server_port_var.get().strip()
2508 },
2509 window # Pass window to close it
2510 )
2511 ) if version_var.get() and version_var.get() != "Brak wersji!" else messagebox.showwarning("Uwaga", "Wybierz wersję Minecrafta!")).pack(side="right", padx=5)
2512
2513
2514
2515
2516def process_create_instance(version, settings, window):
2517 """Handles the logic after 'Create Instance' button is clicked in the dialog."""
2518 if not version:
2519 messagebox.showwarning("Uwaga", "Wybierz wersję Minecrafta.")
2520 return
2521 if version in instances:
2522 if not messagebox.askyesno("Instancja istnieje", f"Instancja dla wersji {version} już istnieje. Czy chcesz ją usunąć i utworzyć ponownie?"):
2523 return # User cancelled replacement
2524
2525 # If user confirmed replacement, delete the existing instance first
2526 log_to_console(console, f"Użytkownik potwierdził zastąpienie instancji {version}. Usuwam starą...", "INFO")
2527 try:
2528 instance_path = instances[version]["path"]
2529 if os.path.exists(instance_path):
2530 import shutil
2531 shutil.rmtree(instance_path)
2532 log_to_console(console, f"Usunięto folder starej instancji: {instance_path}", "INFO")
2533 del instances[version]
2534 save_config()
2535 log_to_console(console, f"Stara instancja {version} usunięta z konfiguracji.", "INFO")
2536 except Exception as e:
2537 messagebox.showerror("Błąd usuwania", f"Nie udało się usunąć istniejącej instancji {version}: {e}\nAnulowano tworzenie nowej.")
2538 log_to_console(console, f"Błąd podczas usuwania istniejącej instancji {version} przed tworzeniem nowej: {e}", "ERROR")
2539 return # Stop the process if old instance couldn't be removed
2540
2541 # Now proceed with downloading the new instance
2542 # We need to switch to the Download tab and start the process there
2543 # Save the pending settings and version globally
2544 global pending_instance_settings, pending_version
2545 pending_instance_settings = settings
2546 pending_version = version
2547
2548 window.destroy() # Close the dialog
2549 switch_tab("Pobieranie") # Switch to download tab which will pick up pending_version/settings
2550
2551def refresh_version_combo_dialog(combo, version_var):
2552 """Refreshes version list in a combobox, typically in a dialog."""
2553 versions = get_versions()
2554 combo['values'] = versions
2555 if versions:
2556 # Try to keep the selected version if it's still in the filtered list
2557 current_selection = version_var.get()
2558 if current_selection in versions:
2559 version_var.set(current_selection)
2560 else:
2561 combo.current(0) # Select the first one
2562 combo.config(state="readonly")
2563 else:
2564 version_var.set("Brak wersji!")
2565 combo.config(state="disabled")
2566 messagebox.showwarning("Uwaga", "Nie znaleziono żadnych wersji Minecrafta z wybranymi filtrami.")
2567
2568
2569def edit_instance(version):
2570 if not version or version not in instances:
2571 messagebox.showwarning("Uwaga", "Wybierz instancję!")
2572 return
2573 instance = instances[version]
2574 settings = instance.get("settings", {})
2575
2576 window = tk.Toplevel(root)
2577 window.title(f"Edytuj instancję {version}")
2578 window.geometry("550x700") # Same size as create
2579 window.configure(bg=PRIMARY_BG)
2580 window.resizable(False, False)
2581
2582 style = ttk.Style() # Ensure style is available in Toplevel
2583 style.configure("TFrame", background=PRIMARY_BG)
2584 style.configure("TLabel", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10))
2585 style.configure("TButton", background=BUTTON_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10), borderwidth=0)
2586 style.map("TButton",
2587 background=[('active', BUTTON_HOVER), ('pressed', BUTTON_HOVER)],
2588 foreground=[('active', PRIMARY_FG), ('pressed', PRIMARY_FG)])
2589 style.configure("TEntry", fieldbackground="#333333", foreground=PRIMARY_FG, insertcolor=PRIMARY_FG, borderwidth=1)
2590 style.configure("TCombobox", fieldbackground="#333333", foreground=PRIMARY_FG, selectbackground=ACCENT_COLOR, borderwidth=1)
2591 style.configure("TCheckbutton", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 9)) # Style for ttk.Checkbutton
2592
2593
2594 frame = ttk.Frame(window, padding="15")
2595 frame.pack(fill="both", expand=True)
2596
2597 ttk.Label(frame, text=f"Edycja instancji {version}", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
2598
2599 form_frame = ttk.Frame(frame)
2600 form_frame.pack(fill="x", expand=False)
2601 form_frame.columnconfigure(1, weight=1)
2602
2603 # Row 0: Username
2604 ttk.Label(form_frame, text="Nazwa użytkownika:", style="TLabel").grid(row=0, column=0, sticky="w", pady=(0, 5))
2605 username_entry = ttk.Entry(form_frame, style="TEntry")
2606 username_entry.insert(0, settings.get("username", username_var.get()))
2607 username_entry.grid(row=0, column=1, sticky="ew", pady=(0, 5), padx=(5, 0))
2608
2609 # Row 1: Memory
2610 ttk.Label(form_frame, text="Pamięć RAM (GB):", style="TLabel").grid(row=1, column=0, sticky="w", pady=(10, 5))
2611 memory_spin = tk.Spinbox(form_frame, from_=1, to=32, width=5, bg="#333333", fg=PRIMARY_FG, highlightthickness=0,
2612 buttonbackground=TERTIARY_BG, buttonforeground=PRIMARY_FG, insertbackground=PRIMARY_FG)
2613 memory_spin.delete(0, tk.END)
2614 memory_spin.insert(0, settings.get("memory", memory_var.get()))
2615 memory_spin.grid(row=1, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
2616
2617 # Row 2: Java Path
2618 ttk.Label(form_frame, text="Ścieżka Java:", style="TLabel").grid(row=2, column=0, sticky="w", pady=(10, 5))
2619 java_var = tk.StringVar()
2620 # Include the instance's current Java path in options if it's not already found by find_java
2621 java_paths_found = find_java()
2622 java_path_tuples = java_paths_found[:] # Copy the list
2623
2624 current_java_path = instance.get("java_path", settings.get("java_path", ""))
2625 current_java_version = instance.get("java_version", settings.get("java_version", ""))
2626
2627 # Add the instance's current java path if it exists and wasn't found by find_java
2628 is_current_java_found = any(p == current_java_path for p, v in java_path_tuples)
2629 if current_java_path and os.path.exists(current_java_path) and not is_current_java_found:
2630 # Get version again if needed, but avoid checking twice if it's already in instance data
2631 actual_version = get_java_version(current_java_path) or current_java_version or "?"
2632 java_path_tuples.append((current_java_path, actual_version))
2633 java_options = [f"{p} (v{v})" for p, v in java_path_tuples]
2634 log_to_console(console, f"Dodano bieżącą Javę instancji do opcji: {current_java_path}", "INFO")
2635 else:
2636 java_options = [f"{p} (v{v})" for p, v in java_path_tuples]
2637
2638
2639 java_combo = ttk.Combobox(form_frame, textvariable=java_var, state="readonly", values=java_options, style="TCombobox")
2640 java_combo.grid(row=2, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2641
2642 # Set the current Java path in the combobox
2643 if current_java_path:
2644 # Find the index of the current_java_path in the java_path_tuples list
2645 try:
2646 index_to_select = [i for i, (p, v) in enumerate(java_path_tuples) if p == current_java_path][0]
2647 java_combo.current(index_to_select)
2648 except IndexError:
2649 # This shouldn't happen if logic above is correct, but as a fallback
2650 log_to_console(console, f"Nie znaleziono bieżącej ścieżki Javy {current_java_path} w liście opcji.", "WARNING")
2651 java_combo.set(current_java_path) # Display the path even if not in list
2652 java_combo.config(state="normal") # Allow editing if not in list? Or just display. Let's just display.
2653 java_combo.config(state="readonly") # Force readonly after setting
2654
2655
2656 # Row 3: Mod Loader Type
2657 ttk.Label(form_frame, text="Mod Loader:", style="TLabel").grid(row=3, column=0, sticky="w", pady=(10, 5))
2658 loader_var = tk.StringVar(value=settings.get("loader_type", "vanilla"))
2659 loader_options = ["vanilla", "fabric", "forge"] # Add "neoforge" etc. when supported
2660 loader_combo = ttk.Combobox(form_frame, textvariable=loader_var, state="readonly", values=loader_options, style="TCombobox")
2661 loader_combo.grid(row=3, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2662 # Set current loader type
2663 try:
2664 loader_combo.current(loader_options.index(settings.get("loader_type", "vanilla")))
2665 except ValueError:
2666 loader_combo.current(0) # Default to vanilla if saved type is invalid
2667
2668
2669 # Row 4: Shared Folders
2670 ttk.Label(form_frame, text="Opcje współdzielenia:", style="TLabel").grid(row=4, column=0, sticky="nw", pady=(10, 5))
2671 options_frame = ttk.Frame(form_frame)
2672 options_frame.grid(row=4, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2673
2674 shared_assets = tk.BooleanVar(value=settings.get("shared_assets", shared_assets_var.get()))
2675 shared_libs = tk.BooleanVar(value=settings.get("shared_libraries", shared_libraries_var.get()))
2676 shared_natives = tk.BooleanVar(value=settings.get("shared_natives", shared_natives_var.get()))
2677
2678 tk.Checkbutton(options_frame, text="Współdziel assets", variable=shared_assets,
2679 bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
2680 font=("Segoe UI", 9)).pack(anchor="w", pady=2)
2681 tk.Checkbutton(options_frame, text="Współdziel biblioteki", variable=shared_libs,
2682 bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
2683 font=("Segoe UI", 9)).pack(anchor="w", pady=2)
2684 tk.Checkbutton(options_frame, text="Współdziel natywne biblioteki", variable=shared_natives,
2685 bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
2686 font=("Segoe UI", 9)).pack(anchor="w", pady=2)
2687
2688 # Row 5: Server Address (Optional)
2689 ttk.Label(form_frame, text="Adres serwera (opcjonalnie):", style="TLabel").grid(row=5, column=0, sticky="w", pady=(10, 5))
2690 server_ip_var = tk.StringVar(value=settings.get("server_ip", ""))
2691 server_ip_entry = ttk.Entry(form_frame, textvariable=server_ip_var, style="TEntry")
2692 server_ip_entry.grid(row=5, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2693
2694 ttk.Label(form_frame, text="Port serwera (opcjonalnie):", style="TLabel").grid(row=6, column=0, sticky="w", pady=(10, 5))
2695 server_port_var = tk.StringVar(value=settings.get("server_port", ""))
2696 server_port_entry = ttk.Entry(form_frame, textvariable=server_port_var, style="TEntry", width=8)
2697 server_port_entry.grid(row=6, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
2698
2699
2700 # Row 7: Stats (Read-only)
2701 ttk.Label(form_frame, text="Statystyki:", style="TLabel").grid(row=7, column=0, sticky="nw", pady=(10, 5))
2702 stats_frame = ttk.Frame(form_frame)
2703 stats_frame.grid(row=7, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2704
2705 # Calculate stats
2706 stats = get_instance_stats(instance)
2707
2708 ttk.Label(stats_frame, text=f"Ścieżka: {stats['path']}", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
2709 ttk.Label(stats_frame, text=f"Rozmiar: {stats['size']}", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
2710 ttk.Label(stats_frame, text=f"Modów (.jar): {stats['mods']}", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
2711 ttk.Label(stats_frame, text=f"Utworzona: {stats['created']}", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
2712
2713
2714 # Action buttons at the bottom
2715 btn_frame = ttk.Frame(frame)
2716 btn_frame.pack(fill="x", pady=(15, 0))
2717
2718 ttk.Button(btn_frame, text="Anuluj", command=window.destroy, style="TButton", width=15).pack(side="right", padx=5)
2719 ttk.Button(btn_frame, text="Zapisz", command=lambda: (
2720 # Get selected Java path from tuple list
2721 selected_java_path := java_path_tuples[java_combo.current()][0] if java_combo.current() != -1 else current_java_path,
2722
2723 # Update instance with selected settings
2724 update_instance(version, {
2725 "username": username_entry.get().strip() or "Player",
2726 "memory": memory_spin.get(),
2727 "java_path": selected_java_path,
2728 "shared_assets": shared_assets.get(),
2729 "shared_libraries": shared_libs.get(),
2730 "shared_natives": shared_natives.get(),
2731 "loader_type": loader_var.get(),
2732 "loader_version": settings.get("loader_version", ""), # Keep existing loader version
2733 "server_ip": server_ip_var.get().strip(),
2734 "server_port": server_port_var.get().strip()
2735 }),
2736
2737 # Close the window
2738 window.destroy()
2739 ) if version_var.get() and version_var.get() != "Brak wersji!" else messagebox.showwarning("Uwaga", "Wybierz wersję Minecrafta!")).pack(side="right", padx=5)
2740
2741
2742
2743def get_instance_stats(instance):
2744 """Calculates and returns statistics for a given instance."""
2745 stats = {
2746 "path": instance.get("path", "N/A"),
2747 "size": "N/A",
2748 "mods": "N/A",
2749 "created": datetime.fromisoformat(instance["timestamp"]).strftime("%Y-%m-%d %H:%M") if "timestamp" in instance else "Nieznana"
2750 }
2751 instance_path = instance.get("path")
2752 if instance_path and os.path.exists(instance_path):
2753 try:
2754 # Calculate folder size
2755 total_size = 0
2756 for dirpath, dirnames, filenames in os.walk(instance_path):
2757 for f in filenames:
2758 fp = os.path.join(dirpath, f)
2759 if not os.path.islink(fp): # Avoid counting symlinks multiple times
2760 total_size += os.path.getsize(fp)
2761 stats["size"] = humanize.naturalsize(total_size)
2762
2763 # Count mods (simple .jar file count in mods folder)
2764 mods_path = os.path.join(instance_path, "mods")
2765 if os.path.exists(mods_path):
2766 mod_count = len([f for f in os.listdir(mods_path) if f.endswith(".jar")])
2767 stats["mods"] = mod_count
2768 else:
2769 stats["mods"] = 0 # No mods folder
2770
2771 except Exception as e:
2772 log_to_console(console, f"Błąd obliczania statystyk dla instancji {instance_path}: {e}", "ERROR")
2773 pass # Keep N/A if error occurs
2774
2775 return stats
2776
2777
2778def update_instance(version, settings):
2779 """Updates an instance's settings and regenerates its start.bat."""
2780 if version not in instances:
2781 log_to_console(console, f"Próba aktualizacji nieistniejącej instancji {version}.", "ERROR")
2782 return
2783
2784 instance = instances[version]
2785 instance["settings"] = settings
2786
2787 # Update Java info if the path changed
2788 selected_java_path = settings.get("java_path")
2789 if selected_java_path and selected_java_path != instance.get("java_path"):
2790 instance["java_path"] = selected_java_path
2791 instance["java_version"] = get_java_version(selected_java_path) # Update version based on new path
2792 instance["required_java"] = get_required_java(version, get_version_info(version)) # Recalculate required java
2793
2794 # Regenerate start.bat with new settings
2795 info = get_version_info(version) # Need version info again
2796 if info:
2797 try:
2798 regenerate_start_bat(version, instance, info, console)
2799 # If bat regeneration was successful, mark ready? Not necessarily, files might be missing.
2800 # Let verify_instance handle the ready status.
2801 except Exception as e:
2802 log_to_console(console, f"Błąd podczas regeneracji start.bat po edycji instancji {version}: {e}", "ERROR")
2803 messagebox.showwarning("Błąd", f"Nie udało się zaktualizować pliku start.bat dla {version}.\nInstancja może nie działać poprawnie.")
2804 instance["ready"] = False # Mark as not ready if bat failed
2805
2806
2807 save_config()
2808 log_to_console(console, f"Instancja {version} zaktualizowana. Ustawienia i start.bat zostały zapisane.", "SUCCESS")
2809 refresh_instances()
2810
2811
2812def show_download():
2813 frame = ttk.Frame(content_frame, padding="10")
2814 frame.pack(fill="both", expand=True)
2815
2816 ttk.Label(frame, text="Pobierz nową wersję Minecraft", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
2817
2818 form_frame = ttk.Frame(frame)
2819 form_frame.pack(fill="x", expand=False)
2820 form_frame.columnconfigure(1, weight=1)
2821
2822 # Row 0: Version Filters
2823 ttk.Label(form_frame, text="Filtry wersji:", style="TLabel").grid(row=0, column=0, sticky="nw", pady=(0, 5))
2824 filters_frame = ttk.Frame(form_frame)
2825 filters_frame.grid(row=0, column=1, sticky="ew", pady=(0, 5), padx=(5, 0))
2826
2827 # Use tk.Checkbutton
2828 tk.Checkbutton(filters_frame, text="Snapshoty", variable=snapshots_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
2829 selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
2830 command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
2831 tk.Checkbutton(filters_frame, text="Release", variable=releases_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
2832 selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
2833 command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
2834 tk.Checkbutton(filters_frame, text="Alpha", variable=alpha_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
2835 selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
2836 command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
2837 tk.Checkbutton(filters_frame, text="Beta", variable=beta_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
2838 selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
2839 command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
2840
2841
2842 # Row 1: Minecraft Version
2843 ttk.Label(form_frame, text="Wersja Minecrafta:", style="TLabel").grid(row=1, column=0, sticky="w", pady=(10, 5))
2844 version_var = tk.StringVar()
2845 versions = get_versions()
2846 combo = ttk.Combobox(form_frame, textvariable=version_var, state="readonly", values=versions, style="TCombobox")
2847 combo.grid(row=1, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2848
2849 # Set initial selection based on pending_version if any
2850 global pending_version, pending_instance_settings # Declare global before using
2851 if pending_version and pending_version in versions:
2852 version_var.set(pending_version)
2853 # Clear pending flags after setting the value
2854 pending_version = ""
2855 pending_instance_settings = {} # Clear settings too
2856 elif versions:
2857 combo.current(0)
2858 else:
2859 version_var.set("Brak wersji!")
2860 combo.config(state="disabled")
2861 messagebox.showwarning("Uwaga", "Nie znaleziono żadnych wersji Minecrafta. Sprawdź filtry i połączenie z internetem.")
2862
2863 # Row 2: Action Button
2864 btn_frame = ttk.Frame(frame)
2865 btn_frame.pack(fill="x", pady=(15, 0))
2866
2867 download_btn = ttk.Button(btn_frame, text="Pobierz Wybraną Wersję", command=lambda: start_download_process(version_var.get()), style="TButton", width=25)
2868 download_btn.pack()
2869
2870 # Console frame below the form and button
2871 console_frame = ttk.Frame(frame)
2872 console_frame.pack(fill="both", expand=True, pady=(15, 0))
2873
2874
2875 global console
2876 console = scrolledtext.ScrolledText(
2877 console_frame, height=15, wrap=tk.WORD, bg=CONSOLE_BG, fg=CONSOLE_FG_DEFAULT,
2878 state="disabled", font=("Consolas", 9)
2879 )
2880 console.pack(side="top", fill="both", expand=True)
2881
2882 # Console action buttons
2883 console_btn_frame = ttk.Frame(frame)
2884 console_btn_frame.pack(fill="x", pady=(5, 0))
2885 ttk.Button(console_btn_frame, text="Kopiuj konsolę", command=lambda: copy_console_content(console), style="TButton", width=15).pack(side="left", padx=5)
2886 ttk.Button(console_btn_frame, text="Wyczyść konsolę", command=lambda: clear_console_content(console), style="TButton", width=15).pack(side="left", padx=5)
2887
2888
2889def start_download_process(version):
2890 global download_thread, download_active
2891 if download_active:
2892 messagebox.showwarning("Uwaga", "Pobieranie już w toku!")
2893 return
2894 if not version or version == "Brak wersji!":
2895 messagebox.showwarning("Uwaga", "Wybierz wersję Minecrafta do pobrania.")
2896 return
2897
2898 # Ask for instance settings before starting download
2899 # Use default settings unless overridden
2900 # This creates a simpler download flow than the separate create instance dialog
2901 # Let's reuse the create_instance dialog logic to gather settings first.
2902 # The create_instance dialog will then call process_create_instance which sets pending vars and switches tab.
2903 # So, if we are on the Download tab and click "Download Selected Version", it means we skipped the create dialog.
2904 # In this case, use default settings.
2905
2906 # Check if there are pending settings from a cancelled create dialog
2907 global pending_version, pending_instance_settings # Declare global before using them
2908 if pending_version and pending_version == version and pending_instance_settings:
2909 settings_to_use = pending_instance_settings
2910 pending_version = ""
2911 pending_instance_settings = {}
2912 log_to_console(console, f"Używam oczekujących ustawień dla wersji {version}.", "INFO")
2913 else:
2914 # Use default settings if no pending settings
2915 settings_to_use = {
2916 "username": username_var.get(),
2917 "memory": memory_var.get(),
2918 "shared_assets": shared_assets_var.get(),
2919 "shared_libraries": shared_libraries_var.get(),
2920 "shared_natives": shared_natives_var.get(),
2921 "loader_type": "vanilla", # Default download is vanilla
2922 "loader_version": "",
2923 "server_ip": "",
2924 "server_port": ""
2925 }
2926 log_to_console(console, f"Używam domyślnych ustawień dla pobierania wersji {version}.", "INFO")
2927
2928
2929 # Handle case where instance already exists
2930 if version in instances:
2931 if not messagebox.askyesno("Instancja istnieje", f"Instancja dla wersji {version} już istnieje. Czy chcesz ją zastąpić (pobrać ponownie)?\n\nSpowoduje to usunięcie obecnego folderu:\n{instances[version]['path']}"):
2932 log_to_console(console, f"Pobieranie instancji {version} anulowane przez użytkownika (instancja już istnieje).", "INFO")
2933 return # User cancelled replacement
2934
2935 # If user confirmed replacement, delete the existing instance first
2936 log_to_console(console, f"Użytkownik potwierdził zastąpienie instancji {version}. Usuwam starą...", "INFO")
2937 try:
2938 instance_path = instances[version]["path"]
2939 if os.path.exists(instance_path):
2940 import shutil
2941 shutil.rmtree(instance_path)
2942 log_to_console(console, f"Usunięto folder starej instancji: {instance_path}", "INFO")
2943 del instances[version]
2944 save_config()
2945 log_to_console(console, f"Stara instancja {version} usunięta z konfiguracji.", "INFO")
2946 except Exception as e:
2947 messagebox.showerror("Błąd usuwania", f"Nie udało się usunąć istniejącej instancji {version}: {e}\nAnulowano pobieranie nowej.")
2948 log_to_console(console, f"Błąd podczas usuwania istniejącej instancji {version} przed pobieraniem nowej: {e}", "ERROR")
2949 return # Stop the process if old instance couldn't be removed
2950
2951
2952 # Start the download thread
2953 download_active = True
2954 # Clear console before starting
2955 clear_console_content(console)
2956 log_to_console(console, f"Rozpoczynam proces pobierania wersji {version} z ustawieniami...", "INFO")
2957 # Pass the settings gathered or defaulted
2958 download_thread = threading.Thread(
2959 target=download_version,
2960 args=(version, settings_to_use, console),
2961 daemon=True
2962 )
2963 download_thread.start()
2964
2965
2966def show_settings():
2967 frame = ttk.Frame(content_frame, padding="10")
2968 frame.pack(fill="both", expand=True)
2969
2970 ttk.Label(frame, text="Ustawienia Launchera", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
2971
2972 form_frame = ttk.Frame(frame)
2973 form_frame.pack(fill="x", expand=False)
2974 form_frame.columnconfigure(1, weight=1) # Allow second column to expand
2975
2976
2977 # Row 0: Default Username
2978 ttk.Label(form_frame, text="Domyślna nazwa użytkownika:", style="TLabel").grid(row=0, column=0, sticky="w", pady=(0, 5))
2979 ttk.Entry(form_frame, textvariable=username_var, style="TEntry").grid(row=0, column=1, sticky="ew", pady=(0, 5), padx=(5, 0))
2980
2981 # Row 1: Default Memory
2982 ttk.Label(form_frame, text="Domyślna pamięć RAM (GB):", style="TLabel").grid(row=1, column=0, sticky="w", pady=(10, 5))
2983 memory_spin = tk.Spinbox(form_frame, from_=1, to=32, textvariable=memory_var, width=5,
2984 bg="#333333", fg=PRIMARY_FG, highlightthickness=0,
2985 buttonbackground=TERTIARY_BG, buttonforeground=PRIMARY_FG, insertbackground=PRIMARY_FG)
2986 memory_spin.grid(row=1, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
2987
2988 # Row 2: Default Shared Folders
2989 ttk.Label(form_frame, text="Domyślne opcje współdzielenia:", style="TLabel").grid(row=2, column=0, sticky="nw", pady=(10, 5))
2990 options_frame = ttk.Frame(form_frame)
2991 options_frame.grid(row=2, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
2992
2993 tk.Checkbutton(options_frame, text="Współdziel assets między instancjami", variable=shared_assets_var,
2994 bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
2995 font=("Segoe UI", 9), command=save_config).pack(anchor="w", pady=2)
2996 tk.Checkbutton(options_frame, text="Współdziel biblioteki między instancjami", variable=shared_libraries_var,
2997 bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
2998 font=("Segoe UI", 9), command=save_config).pack(anchor="w", pady=2)
2999 tk.Checkbutton(options_frame, text="Współdziel natywne biblioteki między instancjami", variable=shared_natives_var,
3000 bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
3001 font=("Segoe UI", 9), command=save_config).pack(anchor="w", pady=2)
3002
3003 # Row 3: Default Version Filters
3004 ttk.Label(form_frame, text="Domyślne filtry wersji:", style="TLabel").grid(row=3, column=0, sticky="nw", pady=(10, 5))
3005 filters_frame = ttk.Frame(form_frame)
3006 filters_frame.grid(row=3, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
3007
3008 tk.Checkbutton(filters_frame, text="Snapshoty", variable=snapshots_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
3009 selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
3010 command=save_config).pack(anchor="w", pady=2)
3011 tk.Checkbutton(filters_frame, text="Release", variable=releases_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
3012 selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
3013 command=save_config).pack(anchor="w", pady=2)
3014 tk.Checkbutton(filters_frame, text="Alpha", variable=alpha_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
3015 selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
3016 command=save_config).pack(anchor="w", pady=2)
3017 tk.Checkbutton(filters_frame, text="Beta", variable=beta_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
3018 selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
3019 command=save_config).pack(anchor="w", pady=2)
3020
3021 # Row 4: Java Paths Cache (Display only)
3022 ttk.Label(form_frame, text="Znalezione Javy (Cache):", style="TLabel").grid(row=4, column=0, sticky="nw", pady=(10, 5))
3023 java_cache_frame = ttk.Frame(form_frame)
3024 java_cache_frame.grid(row=4, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
3025
3026 if java_versions_cache:
3027 cache_text = "\n".join([f"v{ver}: {path}" for ver, path in java_versions_cache.items()])
3028 ttk.Label(java_cache_frame, text=cache_text, style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
3029 else:
3030 ttk.Label(java_cache_frame, text="Brak znalezionych Jav w cache.", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
3031
3032 # Action button - Save is handled by checkboxes and entry lose focus (implicitly by config save)
3033 # Adding an explicit save button for completeness, though not strictly needed for current vars
3034 btn_frame = ttk.Frame(frame)
3035 btn_frame.pack(fill="x", pady=(15, 0))
3036 ttk.Button(btn_frame, text="Zapisz Ustawienia", command=save_config, style="TButton", width=20).pack()
3037
3038
3039def show_console():
3040 frame = ttk.Frame(content_frame, padding="10")
3041 frame.pack(fill="both", expand=True)
3042
3043 ttk.Label(frame, text="Konsola Launchera", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
3044
3045 console_frame = ttk.Frame(frame)
3046 console_frame.pack(fill="both", expand=True)
3047
3048 global console
3049 # Recreate the console widget if it was destroyed when switching tabs
3050 if console is None or not console.winfo_exists():
3051 console = scrolledtext.ScrolledText(
3052 console_frame, height=20, wrap=tk.WORD, bg=CONSOLE_BG, fg=CONSOLE_FG_DEFAULT,
3053 state="disabled", font=("Consolas", 9)
3054 )
3055 # Apply tags if they don't exist (will be applied on first log or in log_to_console)
3056
3057 console.pack(side="top", fill="both", expand=True)
3058
3059 # Console action buttons
3060 btn_frame = ttk.Frame(frame)
3061 btn_frame.pack(fill="x", pady=(10, 0))
3062
3063 ttk.Button(btn_frame, text="Kopiuj", command=lambda: copy_console_content(console), style="TButton", width=15).pack(side="left", padx=5)
3064 ttk.Button(btn_frame, text="Wyczyść", command=lambda: clear_console_content(console), style="TButton", width=15).pack(side="left", padx=5)
3065
3066
3067def show_tools():
3068 frame = ttk.Frame(content_frame, padding="10")
3069 frame.pack(fill="both", expand=True)
3070
3071 ttk.Label(frame, text="Narzędzia Launchera", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
3072
3073 tools_frame = ttk.Frame(frame)
3074 tools_frame.pack(fill="both", expand=True)
3075 # tools_frame.columnconfigure(0, weight=1) # Allow buttons to expand? Or just pack.
3076
3077 # Config Import/Export
3078 ttk.Label(tools_frame, text="Konfiguracja:", font=("Segoe UI", 11, "bold"), style="TLabel").pack(anchor="w", pady=(10, 5))
3079 config_btn_frame = ttk.Frame(tools_frame)
3080 config_btn_frame.pack(fill="x", pady=(5, 10))
3081 ttk.Button(config_btn_frame, text="Eksportuj Konfigurację", command=export_config, style="TButton", width=25).pack(side="left", padx=10)
3082 ttk.Button(config_btn_frame, text="Importuj Konfigurację", command=import_config, style="TButton", width=25).pack(side="left", padx=10)
3083 ttk.Label(tools_frame, text="Eksport/Import dotyczy wszystkich instancji i domyślnych ustawień launchera.", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w", padx=10)
3084
3085
3086 # Java Management (Optional, could list found Javas and allow removing from cache)
3087 # ttk.Label(tools_frame, text="Zarządzanie Javą:", font=("Segoe UI", 11, "bold"), style="TLabel").pack(anchor="w", pady=(10, 5))
3088 # Java management could be a complex table listing found Javas and options. Omit for now.
3089
3090 # Other potential tools:
3091 # - Clean temporary files
3092 # - Repair shared assets/libraries
3093 # - Re-discover Java installations
3094
3095
3096def show_modrinth_browser():
3097 frame = ttk.Frame(content_frame, padding="10")
3098 frame.pack(fill="both", expand=True)
3099
3100 ttk.Label(frame, text="Przeglądaj Mody (Modrinth)", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
3101
3102 search_frame = ttk.Frame(frame)
3103 search_frame.pack(fill="x", expand=False, pady=(0, 10))
3104 search_frame.columnconfigure(1, weight=1)
3105
3106 ttk.Label(search_frame, text="Szukaj:", style="TLabel").grid(row=0, column=0, sticky="w", padx=5)
3107 search_term_var = tk.StringVar()
3108 search_entry = ttk.Entry(search_frame, textvariable=search_term_var, style="TEntry")
3109 search_entry.grid(row=0, column=1, sticky="ew", padx=5)
3110
3111 ttk.Label(search_frame, text="Wersja MC:", style="TLabel").grid(row=0, column=2, sticky="w", padx=5)
3112 mc_versions = sorted(list(set([inst["settings"].get("version", v) for v, inst in instances.items()] + [v for v in instances.keys()])), key=lambda v: pkg_version.parse(v) if v[0].isdigit() else v, reverse=True) # Get MC versions from existing instances + keys
3113 mc_version_var = tk.StringVar(value=mc_versions[0] if mc_versions else "") # Default to newest instance version or empty
3114 mc_version_combo = ttk.Combobox(search_frame, textvariable=mc_version_var, state="readonly", values=mc_versions, style="TCombobox", width=12)
3115 mc_version_combo.grid(row=0, column=3, sticky="w", padx=5)
3116 if mc_versions:
3117 mc_version_combo.current(0)
3118
3119
3120 ttk.Label(search_frame, text="Loader:", style="TLabel").grid(row=0, column=4, sticky="w", padx=5)
3121 loader_types = ["any", "fabric", "forge", "quilt", "neoforge"] # Modrinth supports these loaders
3122 loader_var = tk.StringVar(value="any")
3123 loader_combo = ttk.Combobox(search_frame, textvariable=loader_var, state="readonly", values=loader_types, style="TCombobox", width=8)
3124 loader_combo.grid(row=0, column=5, sticky="w", padx=5)
3125 loader_combo.current(0)
3126
3127 search_button = ttk.Button(search_frame, text="Szukaj", command=lambda: search_modrinth(search_term_var.get(), mc_version_var.get(), loader_var.get(), modrinth_tree), style="TButton", width=10)
3128 search_button.grid(row=0, column=6, sticky="e", padx=5)
3129
3130 # Mod List (Treeview)
3131 mod_list_frame = ttk.Frame(frame)
3132 mod_list_frame.pack(fill="both", expand=True)
3133
3134 columns = ("title", "author", "downloads", "version", "loaders", "description") # Added description, loaders, version
3135 modrinth_tree = ttk.Treeview(
3136 mod_list_frame, columns=columns, show="headings", selectmode="browse",
3137 style="Treeview"
3138 )
3139
3140 modrinth_tree.heading("title", text="Tytuł Modu", anchor="w")
3141 modrinth_tree.heading("author", text="Autor", anchor="w")
3142 modrinth_tree.heading("downloads", text="Pobrania", anchor="e")
3143 modrinth_tree.heading("version", text="Wersja Modu", anchor="w") # Version of the mod file, not MC version
3144 modrinth_tree.heading("loaders", text="Loadery", anchor="w") # Loaders supported by the mod file
3145 modrinth_tree.heading("description", text="Opis", anchor="w") # Hidden by default
3146
3147 modrinth_tree.column("title", width=200, anchor="w")
3148 modrinth_tree.column("author", width=100, anchor="w")
3149 modrinth_tree.column("downloads", width=80, anchor="e", stretch=tk.NO)
3150 modrinth_tree.column("version", width=100, anchor="w", stretch=tk.NO)
3151 modrinth_tree.column("loaders", width=120, anchor="w")
3152 modrinth_tree.column("description", width=0, stretch=tk.NO) # Hide description initially
3153
3154 scrollbar = ttk.Scrollbar(mod_list_frame, orient="vertical", command=modrinth_tree.yview)
3155 scrollbar.pack(side="right", fill="y")
3156 modrinth_tree.configure(yscrollcommand=scrollbar.set)
3157 modrinth_tree.pack(side="left", fill="both", expand=True)
3158
3159 # Modrinth item context menu
3160 mod_context_menu = tk.Menu(root, tearoff=0)
3161 mod_context_menu.add_command(label="Pobierz do instancji", command=lambda: download_mod_to_instance(modrinth_tree))
3162 mod_context_menu.add_command(label="Otwórz na Modrinth", command=lambda: open_modrinth_page(modrinth_tree))
3163
3164 def show_modrinth_context_menu(event):
3165 selected_item = modrinth_tree.focus()
3166 if selected_item:
3167 try:
3168 mod_context_menu.tk_popup(event.x_root, event.y_root)
3169 finally:
3170 mod_context_menu.grab_release()
3171
3172 modrinth_tree.bind("<Button-3>", show_modrinth_context_menu)
3173
3174 # Action frame below mod list
3175 mod_action_frame = ttk.Frame(frame)
3176 mod_action_frame.pack(fill="x", pady=(10, 0))
3177
3178 ttk.Label(mod_action_frame, text="Pobierz do instancji:", style="TLabel").pack(side="left", padx=5)
3179 # Dropdown to select instance to download to
3180 instance_versions = sorted(instances.keys(), key=lambda v: pkg_version.parse(v) if v[0].isdigit() else v, reverse=True)
3181 global selected_modrinth_instance_var
3182 selected_modrinth_instance_var.set(instance_versions[0] if instance_versions else "")
3183 instance_combo = ttk.Combobox(mod_action_frame, textvariable=selected_modrinth_instance_var, state="readonly", values=instance_versions, style="TCombobox", width=20)
3184 instance_combo.pack(side="left", padx=5)
3185
3186 download_mod_button = ttk.Button(mod_action_frame, text="Pobierz Wybrany Mod", command=lambda: download_mod_to_instance(modrinth_tree), style="TButton", width=20)
3187 download_mod_button.pack(side="left", padx=5)
3188
3189 if not instance_versions:
3190 instance_combo.config(state="disabled")
3191 download_mod_button.config(state="disabled")
3192 ttk.Label(mod_action_frame, text="Brak instancji do pobrania modów.", style="TLabel", foreground=WARNING_COLOR).pack(side="left", padx=5)
3193 else:
3194 # Add tooltip to download button
3195 Tooltip(download_mod_button, "Pobiera wybrany mod do folderu mods/ wybranej instancji.\nUpewnij się, że instancja ma odpowiedni mod loader!")
3196
3197
3198def search_modrinth(search_term, mc_version, loader, tree):
3199 """Searches Modrinth API and populates the treeview."""
3200 # Clear previous results
3201 for item in tree.get_children():
3202 tree.delete(item)
3203
3204 if not search_term and not mc_version:
3205 # No search criteria
3206 log_to_console(console, "Wpisz frazę do wyszukania lub wybierz wersję MC.", "WARNING")
3207 return
3208
3209 log_to_console(console, f"Szukam modów na Modrinth: '{search_term}', wersja MC='{mc_version}', loader='{loader}'...", "INFO")
3210
3211 # Modrinth API Endpoint: https://api.modrinth.com/v2/search
3212 url = "https://api.modrinth.com/v2/search"
3213 params = {
3214 "query": search_term,
3215 "limit": 50, # Limit results
3216 "facets": json.dumps([ # Filter by version and loader
3217 [f"versions:{mc_version}"] if mc_version else [],
3218 [f"project_type:mod"], # Search only for mods
3219 [f"loaders:{loader}"] if loader != "any" else []
3220 ])
3221 }
3222
3223 try:
3224 response = requests.get(url, params=params, timeout=10)
3225 response.raise_for_status() # Raise an exception for bad status codes
3226 results = response.json()
3227
3228 if results and "hits" in results:
3229 log_to_console(console, f"Znaleziono {results['total_hits']} wyników.", "INFO")
3230 for hit in results["hits"]:
3231 # Need to fetch file details to get exact mod version, supported loaders for that file etc.
3232 # The search results ("hits") provide project-level info, not file-level details directly related to the filters.
3233 # This makes selecting the *correct file* for download tricky from just the search result.
3234 # A more robust approach would be:
3235 # 1. Search projects.
3236 # 2. For a selected project, fetch its versions.
3237 # 3. Filter project versions by MC version and loader.
3238 # 4. Display file(s) for the matching versions.
3239 # 5. User selects a specific file to download.
3240
3241 # Simplified Approach for now: Display project info. The download button will *attempt* to find a matching file.
3242 # This might download the wrong file or fail if no exact match exists.
3243
3244 tree.insert("", "end", iid=hit["project_id"], values=(
3245 hit.get("title", "Bez tytułu"),
3246 hit.get("author", "Nieznany autor"),
3247 hit.get("downloads", 0), # Total project downloads
3248 hit.get("latest_version", "N/A"), # Latest project version string (might not be for selected MC/loader)
3249 ", ".join(hit.get("loaders", [])), # Project-level loaders
3250 hit.get("description", "Bez opisu")
3251 ))
3252 # Add tooltip to treeview items to show description? Or show description in a label below?
3253 # Tooltip on item is cleaner but requires more complex event binding.
3254 else:
3255 log_to_console(console, "Nie znaleziono modów spełniających kryteria.", "INFO")
3256
3257 except requests.exceptions.RequestException as e:
3258 log_to_console(console, f"Błąd API Modrinth podczas wyszukiwania: {e}", "ERROR")
3259 messagebox.showerror("Błąd API Modrinth", f"Nie udało się wyszukać modów: {e}")
3260 except Exception as e:
3261 log_to_console(console, f"Nieoczekiwany błąd podczas wyszukiwania Modrinth: {e}", "ERROR")
3262 messagebox.showerror("Nieoczekiwany błąd", f"Wystąpił błąd podczas wyszukiwania: {e}")
3263
3264
3265def download_mod_to_instance(tree):
3266 """Downloads the selected mod file from Modrinth to the selected instance's mods folder."""
3267 selected_item = tree.focus()
3268 if not selected_item:
3269 messagebox.showwarning("Uwaga", "Wybierz mod z listy, aby go pobrać.")
3270 return
3271
3272 project_id = tree.item(selected_item, "iid")
3273 if not project_id:
3274 messagebox.showerror("Błąd", "Nie udało się uzyskać ID projektu Modrinth.")
3275 return
3276
3277 target_instance_version = selected_modrinth_instance_var.get()
3278 if not target_instance_version or target_instance_version not in instances:
3279 messagebox.showwarning("Uwaga", "Wybierz instancję docelową z listy rozwijanej pod wynikami wyszukiwania.")
3280 return
3281
3282 instance = instances.get(target_instance_version)
3283 if not instance: # Should not happen if in instances list, but check
3284 messagebox.showerror("Błąd", "Dane wybranej instancji nie zostały znalezione.")
3285 return
3286
3287 log_to_console(console, f"Przygotowanie do pobrania modu '{tree.item(selected_item)['values'][0]}' (ID: {project_id}) do instancji '{target_instance_version}'...", "INFO")
3288
3289 # Need to find the correct file for the selected instance's MC version and loader type.
3290 # Modrinth API: /project/{id}/version
3291 versions_url = f"https://api.modrinth.com/v2/project/{project_id}/version"
3292 instance_mc_version = target_instance_version # MC version is the instance key
3293 instance_loader = instance["settings"].get("loader_type", "vanilla")
3294
3295 # Filter by game versions and loaders
3296 params = {
3297 "game_versions": json.dumps([instance_mc_version]),
3298 "loaders": json.dumps([instance_loader]) if instance_loader != "vanilla" else json.dumps([]) # Filter by loader unless vanilla
3299 }
3300 if instance_loader == "vanilla":
3301 # Vanilla instances technically don't need a loader filter, but mods listed might require one.
3302 # Maybe search for files that list NO loaders or 'any'? Modrinth facets might handle this.
3303 # For simplicity, if instance is vanilla, we filter by MC version only.
3304 # If mod requires a loader, it might still be listed, but won't work. User needs to know.
3305 params = {"game_versions": json.dumps([instance_mc_version])}
3306
3307
3308 try:
3309 response = requests.get(versions_url, params=params, timeout=10)
3310 response.raise_for_status()
3311 mod_versions = response.json() # This is a list of versions of the mod project
3312
3313 if not mod_versions:
3314 log_to_console(console, f"Nie znaleziono wersji modu '{tree.item(selected_item)['values'][0]}' kompatybilnych z MC {instance_mc_version} i loaderem '{instance_loader}'.", "WARNING")
3315 messagebox.showwarning("Brak kompatybilnej wersji", f"Nie znaleziono wersji modu '{tree.item(selected_item)['values'][0]}' kompatybilnych z Twoją instancją (MC {instance_mc_version}, Loader: {instance_loader}).")
3316 return
3317
3318 # Find the most recent version that matches criteria and has a downloadable file
3319 # Modrinth versions are often sorted newest first by default, but explicitly sort by release date or version string
3320 sorted_mod_versions = sorted(mod_versions, key=lambda v: v.get("date_published", ""), reverse=True)
3321
3322 best_file = None
3323 mod_version_info = None
3324 for mod_v in sorted_mod_versions:
3325 # Find a primary file in this version
3326 primary_file = next((f for f in mod_v.get("files", []) if f.get("primary")), None)
3327 if not primary_file and mod_v.get("files"):
3328 primary_file = mod_v["files"][0] # Take first file if no primary marked
3329
3330 if primary_file and primary_file.get("url"):
3331 # Optional: double check loaders listed in the file itself match instance loader
3332 # This is more precise than project-level loaders
3333 file_loaders = primary_file.get("loaders", [])
3334 if instance_loader != "vanilla" and instance_loader not in file_loaders:
3335 # Skip this file if instance requires a loader but file doesn't list it
3336 continue
3337 if instance_loader == "vanilla" and file_loaders:
3338 # Skip this file if instance is vanilla but file requires a loader
3339 continue
3340
3341 best_file = primary_file
3342 mod_version_info = mod_v
3343 break # Found a suitable file, break loop
3344
3345 if not best_file:
3346 log_to_console(console, f"Znaleziono wersje modu, ale brak pliku do pobrania kompatybilnego z MC {instance_mc_version} i loaderem '{instance_loader}'.", "WARNING")
3347 messagebox.showwarning("Brak pliku", f"Znaleziono wersje modu '{tree.item(selected_item)['values'][0]}', ale brak pliku do pobrania kompatybilnego z Twoją instancją (MC {instance_mc_version}, Loader: {instance_loader}).")
3348 return
3349
3350
3351 file_url = best_file["url"]
3352 file_name = best_file.get("filename") or file_url.split("/")[-1] # Use provided filename or extract from URL
3353 file_sha1 = best_file.get("hashes", {}).get("sha1") # Modrinth provides sha1 and sha512
3354
3355 mod_target_path = os.path.join(instance["path"], "mods", file_name)
3356
3357 if os.path.exists(mod_target_path) and file_sha1 and verify_sha1(mod_target_path, file_sha1):
3358 log_to_console(console, f"Mod '{file_name}' już istnieje i jest poprawny w instancji {target_instance_version}. Pomijam pobieranie.", "INFO")
3359 messagebox.showinfo("Już istnieje", f"Mod '{file_name}' już istnieje i jest poprawny w instancji {target_instance_version}.")
3360 return
3361
3362 # Start download in a thread
3363 log_to_console(console, f"Pobieranie modu '{file_name}' ({mod_version_info.get('version_number', 'N/A')}) do instancji {target_instance_version}...", "INFO")
3364 # Pass a specific progress bar/label for mod downloads if needed, or use global ones
3365 # Using global ones requires careful state management if simultaneous downloads are possible (they aren't currently)
3366 # Let's use the global progress bar, maybe temporarily update status label format
3367 # Store original status label text and restore after mod download
3368 original_status_text = global_status_label.cget("text")
3369
3370 def mod_download_callback(downloaded, total):
3371 # Use the global progress bar, but format status label for mod download
3372 if global_progress_bar and global_status_label:
3373 progress = (downloaded / total) * 100 if total > 0 else 0
3374 global_progress_bar["value"] = progress
3375 remaining_bytes = max(0, total - downloaded)
3376 global_status_label.config(text=f"[Modrinth] Pobieranie '{file_name}': {progress:.1f}% | Pozostało: {humanize.naturalsize(remaining_bytes)}")
3377 root.update_idletasks()
3378
3379 def mod_download_complete():
3380 # Restore original status label text
3381 if global_status_label:
3382 global_status_label.config(text=original_status_text)
3383 global_progress_bar["value"] = 0 # Reset bar
3384 log_to_console(console, f"Pobieranie modu '{file_name}' zakończone.", "INFO")
3385 # Optionally run verify_instance again, but it's slow.
3386 # Just show success message.
3387 messagebox.showinfo("Pobrano Mod", f"Mod '{file_name}' został pomyślnie pobrany do instancji {target_instance_version}.")
3388 # Check for dependencies? Very complex. Omit for now.
3389
3390 def mod_download_failed(error_msg):
3391 if global_status_label:
3392 global_status_label.config(text=original_status_text)
3393 global_progress_bar["value"] = 0 # Reset bar
3394 log_to_console(console, f"Pobieranie modu '{file_name}' nie powiodło się: {error_msg}", "ERROR")
3395 messagebox.showerror("Błąd Pobierania Modu", f"Nie udało się pobrać modu '{file_name}': {error_msg}")
3396
3397
3398 # Run download in a new thread
3399 mod_download_thread = threading.Thread(
3400 target=lambda: [
3401 log_to_console(console, f"Rozpoczynam wątek pobierania modu {file_name}", "INFO"),
3402 download_success := download_file(file_url, mod_target_path, mod_download_callback, file_sha1, console, description=f"mod {file_name}"),
3403 mod_download_complete() if download_success else mod_download_failed("Błąd pobierania lub weryfikacji")
3404 ],
3405 daemon=True # Allow thread to close with main app
3406 )
3407 mod_download_thread.start()
3408
3409 except requests.exceptions.RequestException as e:
3410 log_to_console(console, f"Błąd API Modrinth podczas pobierania modu: {e}", "ERROR")
3411 messagebox.showerror("Błąd API Modrinth", f"Nie udało się pobrać szczegółów modu: {e}")
3412 except Exception as e:
3413 log_to_console(console, f"Nieoczekiwany błąd podczas pobierania modu Modrinth: {e}", "ERROR")
3414 messagebox.showerror("Nieoczekiwany błąd", f"Wystąpił błąd podczas pobierania modu: {e}")
3415
3416
3417def open_modrinth_page(tree):
3418 """Opens the Modrinth project page for the selected mod."""
3419 selected_item = tree.focus()
3420 if not selected_item:
3421 messagebox.showwarning("Uwaga", "Wybierz mod z listy.")
3422 return
3423
3424 project_id = tree.item(selected_item, "iid")
3425 if not project_id:
3426 messagebox.showerror("Błąd", "Nie udało się uzyskać ID projektu Modrinth.")
3427 return
3428
3429 project_slug = tree.item(selected_item)['values'][0].replace(" ", "-").lower() # Attempt to create a slug
3430 # A more reliable way would be to fetch project details to get the actual slug
3431 # Modrinth URL is https://modrinth.com/mod/{slug} or /project/{slug}
3432
3433 url = f"https://modrinth.com/project/{project_id}" # Using ID is more reliable than guessing slug
3434
3435 try:
3436 webbrowser.open(url)
3437 log_to_console(console, f"Otwieram stronę Modrinth dla projektu {project_id}: {url}", "INFO")
3438 except Exception as e:
3439 messagebox.showerror("Błąd otwierania strony", f"Nie udało się otworzyć strony w przeglądarce: {e}")
3440 log_to_console(console, f"Nie udało się otworzyć strony Modrinth {url}: {e}", "ERROR")
3441
3442
3443def find_java_with_source():
3444 """Same as find_java but returns tuples including the source (Cache, Pobrana, JAVA_HOME, etc.)."""
3445 # This is largely duplicated from find_java for clarity, could refactor
3446 possible_paths = []
3447 system = platform.system()
3448 java_exec_name = "java.exe" if system == "Windows" else "java"
3449
3450 # Check cache first
3451 for ver, path in java_versions_cache.items():
3452 if os.path.exists(path):
3453 # Check if it's actually a 64-bit java executable without verbose logging
3454 try:
3455 result = subprocess.run([path, "-version"], capture_output=True, text=True, timeout=1, encoding='utf-8', errors='ignore')
3456 is_64bit = "64-Bit" in result.stderr or "64-bit" in result.stderr or "x86_64" in result.stderr.lower()
3457 if is_64bit:
3458 possible_paths.append((path, ver, "Cache"))
3459 except:
3460 pass # Ignore errors for paths in cache that might be invalid
3461
3462 # Check custom JAVA_DIR installations
3463 if os.path.exists(JAVA_DIR):
3464 for java_folder in os.listdir(JAVA_DIR):
3465 java_path = os.path.join(JAVA_DIR, java_folder)
3466 found_exec = None
3467 for root, _, files in os.walk(java_path):
3468 if java_exec_name in files:
3469 found_exec = os.path.join(root, java_exec_name)
3470 break
3471 if found_exec:
3472 version = get_java_version(found_exec) # This one *will* log
3473 if version: # get_java_version only returns 64-bit versions
3474 possible_paths.append((found_exec, version, "Pobrana"))
3475
3476
3477 # Check standard system locations (JAVA_HOME, PATH, Program Files) - these will also log
3478 java_home = os.environ.get("JAVA_HOME")
3479 if java_home:
3480 java_path = os.path.join(java_home, "bin", java_exec_name)
3481 if os.path.exists(java_path):
3482 version = get_java_version(java_path)
3483 if version:
3484 possible_paths.append((java_path, version, "JAVA_HOME"))
3485
3486 try:
3487 command = ["where", java_exec_name] if system == "Windows" else ["which", java_exec_name]
3488 out = subprocess.check_output(command, stderr=subprocess.DEVNULL).decode().strip()
3489 for line in out.splitlines():
3490 line = line.strip()
3491 if os.path.exists(line):
3492 version = get_java_version(line)
3493 if version:
3494 possible_paths.append((line, version, "PATH"))
3495 except:
3496 pass
3497
3498 if system == "Windows":
3499 for base in [os.environ.get('ProgramFiles'), os.environ.get('ProgramFiles(x86)'), "C:\\Program Files\\Java", "C:\\Program Files (x86)\\Java"]:
3500 if base and os.path.isdir(base):
3501 java_root = os.path.join(base, "Java")
3502 if os.path.isdir(java_root):
3503 for item in os.listdir(java_root):
3504 java_path = os.path.join(java_root, item, "bin", java_exec_name)
3505 if os.path.exists(java_path):
3506 version = get_java_version(java_path)
3507 if version:
3508 possible_paths.append((java_path, version, "Program Files"))
3509
3510 # Ensure paths are unique (based on path itself) and prefer certain sources
3511 unique_paths = {}
3512 source_order = {"Cache": 0, "Pobrana": 1, "JAVA_HOME": 2, "PATH": 3, "Program Files": 4}
3513 for path, ver, source in possible_paths:
3514 if path not in unique_paths or source_order.get(source, 99) < source_order.get(unique_paths[path][2], 99):
3515 unique_paths[path] = (path, ver, source)
3516
3517 sorted_unique_paths = sorted(unique_paths.values(), key=lambda item: (source_order.get(item[2], 99), item[0]))
3518
3519 return sorted_unique_paths
3520
3521
3522# --- Main Application Setup ---
3523if __name__ == "__main__":
3524 print("Inicjalizacja Launchera...")
3525
3526 # Ensure base directories exist early
3527 os.makedirs(BASE_DIR, exist_ok=True)
3528 os.makedirs(ASSETS_DIR, exist_ok=True)
3529 os.makedirs(LIBRARIES_DIR, exist_ok=True)
3530 os.makedirs(NATIVES_DIR, exist_ok=True)
3531 os.makedirs(LOGS_DIR, exist_ok=True)
3532 os.makedirs(JAVA_DIR, exist_ok=True)
3533 os.makedirs(ICONS_DIR, exist_ok=True) # For potential custom icons
3534
3535 # Generate a dummy logo if icons dir is empty, so Tkinter doesn't crash trying to load non-existent file
3536 dummy_logo_path = os.path.join(ICONS_DIR, "logo.png")
3537 if not os.path.exists(dummy_logo_path):
3538 try:
3539 dummy_img = Image.new('RGB', (180, 60), color = (40,40,40))
3540 dummy_img.save(dummy_logo_path)
3541 print("Utworzono domyślne logo.")
3542 except Exception as e:
3543 print(f"Nie udało się utworzyć domyślnego logo: {e}")
3544
3545
3546 root = tk.Tk()
3547 root.title("Minecraft Launcher by Paffcio")
3548 root.geometry("1200x800") # Wider window
3549 root.configure(bg=PRIMARY_BG)
3550 root.minsize(1000, 700) # Minimum size
3551
3552 # Load configuration immediately after Tkinter root is created
3553 load_config()
3554
3555 # --- Styling ---
3556 style = ttk.Style()
3557 style.theme_use('clam') # 'clam', 'alt', 'default', 'classic'
3558 # Configure main styles
3559 style.configure("TFrame", background=PRIMARY_BG)
3560 style.configure("TLabel", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10))
3561 style.configure("TButton", background=BUTTON_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10), borderwidth=0)
3562 style.map("TButton",
3563 background=[('active', BUTTON_HOVER), ('pressed', BUTTON_HOVER)],
3564 foreground=[('active', PRIMARY_FG), ('pressed', PRIMARY_FG)])
3565 style.configure("TEntry", fieldbackground="#333333", foreground=PRIMARY_FG, insertcolor=PRIMARY_FG, borderwidth=1)
3566 style.configure("TCombobox", fieldbackground="#333333", foreground=PRIMARY_FG, selectbackground=ACCENT_COLOR, borderwidth=1,
3567 arrowcolor=PRIMARY_FG, selectforeground=PRIMARY_FG, insertcolor=PRIMARY_FG) # Added colors for combobox elements
3568 # Style for Combobox dropdown list (requires accessing TCombobox.Listbox)
3569 # style.configure("TCombobox.Listbox", fieldbackground="#333333", foreground=PRIMARY_FG, selectbackground=ACCENT_COLOR, selectforeground=PRIMARY_FG) # Does not work directly
3570
3571 style.configure("Horizontal.TProgressbar", troughcolor="#333333", background=ACCENT_COLOR, thickness=10)
3572 # Treeview styling
3573 style.configure("Treeview", background=SECONDARY_BG, fieldbackground=SECONDARY_BG, foreground=PRIMARY_FG, rowheight=25, borderwidth=0)
3574 style.map("Treeview", background=[('selected', ACCENT_COLOR)])
3575 style.configure("Treeview.Heading", background=TERTIARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10, "bold"))
3576 style.layout("Treeview", [('Treeview.treearea', {'sticky': 'nswe'})]) # Remove borders around treearea
3577
3578
3579 # --- Layout ---
3580 # Progress bar and status label at the very bottom
3581 progress_frame = ttk.Frame(root)
3582 progress_frame.pack(side="bottom", fill="x", padx=10, pady=(0, 5))
3583 global_progress_bar = ttk.Progressbar(progress_frame, length=400, mode="determinate", style="Horizontal.TProgressbar")
3584 global_progress_bar.pack(side="left", fill="x", expand=True, padx=(0, 10))
3585 global_status_label = ttk.Label(progress_frame, text="Gotowe do działania!", foreground=PRIMARY_FG, background=PRIMARY_BG, font=("Segoe UI", 9))
3586 global_status_label.pack(side="left", padx=(10, 0))
3587
3588
3589 # Sidebar on the left
3590 sidebar = tk.Frame(root, bg=SIDEBAR_BG, width=250) # Wider sidebar
3591 sidebar.pack(side="left", fill="y", padx=0, pady=0)
3592 sidebar.pack_propagate(False) # Prevent sidebar from resizing to fit content
3593
3594 # Logo area
3595 logo_frame = tk.Frame(sidebar, bg=SIDEBAR_BG)
3596 logo_frame.pack(fill="x", pady=(10, 20))
3597 try:
3598 # Load and display logo
3599 logo_img = Image.open(os.path.join(ICONS_DIR, "logo.png"))
3600 # Ensure image is RGB to save space and avoid transparency issues with some loaders
3601 if logo_img.mode == 'RGBA':
3602 logo_img = logo_img.convert('RGB')
3603 logo_img = logo_img.resize((min(logo_img.width, 200), min(logo_img.height, 80)), Image.LANCZOS) # Resize constraints
3604 logo_photo = ImageTk.PhotoImage(logo_img)
3605 logo_label = tk.Label(logo_frame, image=logo_photo, bg=SIDEBAR_BG)
3606 logo_label.image = logo_photo # Keep a reference!
3607 logo_label.pack(pady=5)
3608 except Exception as e:
3609 print(f"Błąd ładowania lub wyświetlania logo: {e}")
3610 # Fallback if logo fails
3611 ttk.Label(logo_frame, text="Minecraft Launcher", bg=SIDEBAR_BG, fg=PRIMARY_FG, font=("Segoe UI", 12, "bold")).pack(pady=20)
3612
3613
3614 # Navigation buttons (Tabs)
3615 tabs = [
3616 ("Instancje", "📋"),
3617 ("Pobieranie", "⬇"),
3618 ("Modrinth", "📦"), # New tab for Modrinth
3619 ("Narzędzia", "🔧"), # New tab for tools (Import/Export etc.)
3620 ("Ustawienia", "⚙"),
3621 ("Konsola", "📜")
3622 ]
3623 tab_buttons = {}
3624 for tab_name, icon in tabs:
3625 btn = tk.Button(
3626 sidebar, text=f" {icon} {tab_name}", bg=SIDEBAR_BG, fg=PRIMARY_FG,
3627 activebackground=ACTIVE_TAB_COLOR, activeforeground=PRIMARY_FG, # Active state when clicked
3628 font=("Segoe UI", 11), relief="flat", anchor="w", padx=15, pady=12, # Increased padding
3629 command=lambda t=tab_name: switch_tab(t)
3630 )
3631 btn.pack(fill="x", pady=1) # Added small vertical padding
3632
3633 # Initial hover binding (will be unbound for the active tab)
3634 btn.bind("<Enter>", lambda e, b=btn: b.config(bg=HOVER_TAB_COLOR) if current_tab.get() != tab_name else None)
3635 btn.bind("<Leave>", lambda e, b=btn: b.config(bg=SIDEBAR_BG) if current_tab.get() != tab_name else None)
3636 tab_buttons[tab_name] = btn
3637
3638
3639 # Content area on the right
3640 content_frame = ttk.Frame(root, style="TFrame")
3641 content_frame.pack(side="left", fill="both", expand=True, padx=10, pady=10)
3642
3643 # --- Initial View ---
3644 switch_tab("Instancje") # Start on the instances tab
3645
3646 # Ensure console is initialized if the starting tab is not Console
3647 # log_to_console function now handles console=None gracefully,
3648 # but for the console tab itself, it needs to be created.
3649 # The switch_tab function now handles creating the console widget
3650 # when the Console tab is selected.
3651
3652 log_to_console(console, "Launcher uruchomiony.", "INFO")
3653
3654 root.mainloop()