# -*- coding: utf-8 -*- """Мастер первоначальной настройки — показывается при первом запуске. Окно живёт до построения главного окна (см. ui.run): пользователь выбирает язык, ник и объём ОЗУ. Результат пишется в settings, флаг onboarding_done не даёт мастеру появиться снова; апгрейд со старой версии считается уже настроенным (см. config.load_settings). """ import getpass import re import tkinter as tk from . import i18n from .i18n import T NICK_RE = re.compile(r"[A-Za-z0-9_]{3,16}") def total_ram_mb(): """ОЗУ системы в МБ (Windows — GlobalMemoryStatusEx, иначе 4096).""" try: import ctypes class _MemStatus(ctypes.Structure): _fields_ = [ ("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong), ("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong), ("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong), ("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong), ("ullAvailExtendedVirtual", ctypes.c_ulonglong), ] st = _MemStatus() st.dwLength = ctypes.sizeof(_MemStatus) if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(st)): return max(1024, st.ullTotalPhys // (1024 * 1024)) except Exception: # noqa: BLE001 pass return 4096 def default_ram_mb(): """Разумный дефолт: половина ОЗУ, в пределах 2-8 ГБ, кратно 512 МБ.""" mb = (total_ram_mb() // 2) // 512 * 512 return max(2048, min(8192, mb)) def _suggest_nick(): """Ник по умолчанию: имя пользователя Windows, если оно допустимо.""" try: user = getpass.getuser() except Exception: # noqa: BLE001 user = "" return user if NICK_RE.fullmatch(user) else "Player" class Wizard: """Окно мастера: язык -> ник -> ОЗУ -> готово.""" def __init__(self, root, settings): # палитру берём из ui (лениво: ui импортирует этот модуль в run()) from .ui import (BG, DIM, ERR, FAINT, GREEN, GREEN_HI, INPUT, INPUT_BD, PANEL, TEXT) self.pal = dict(BG=BG, DIM=DIM, ERR=ERR, FAINT=FAINT, GREEN=GREEN, GREEN_HI=GREEN_HI, INPUT=INPUT, INPUT_BD=INPUT_BD, PANEL=PANEL, TEXT=TEXT) self.root = root self.settings = settings self.step = 0 self.result = None # "done" | "skip" root.title("PixelLauncher") root.configure(bg=BG) root.geometry("560x430") root.resizable(False, False) root.protocol("WM_DELETE_WINDOW", self.skip) self.var_lang = tk.StringVar( value="en" if settings.get("lang") == "en" else "ru") self.var_nick = tk.StringVar(value=_suggest_nick()) self.var_auto = tk.BooleanVar(value=True) self.var_ram = tk.IntVar(value=default_ram_mb()) self.var_err = tk.StringVar(value="") self.lbl_ram = None self.scale_ram = None self._goto(0) self._center() # ------------------------------------------------------------ каркас def _goto(self, i): """Показать шаг i (окно перерисовывается целиком — тексты живые).""" self.step = i for w in self.root.winfo_children(): w.destroy() p = self.pal head = tk.Frame(self.root, bg=p["BG"]) head.pack(fill="x", padx=36, pady=(26, 0)) tk.Label(head, text="PixelLauncher", bg=p["BG"], fg=p["GREEN"], font=("Consolas", 13, "bold")).pack(side="left") tk.Label(head, text=T("Шаг {n} из {m}").format(n=i + 1, m=4), bg=p["BG"], fg=p["DIM"], font=("Segoe UI", 9)).pack(side="right") body = tk.Frame(self.root, bg=p["BG"]) body.pack(fill="both", expand=True, padx=36, pady=18) (self._step0 if i == 0 else self._step1 if i == 1 else self._step2 if i == 2 else self._step3)(body) foot = tk.Frame(self.root, bg=p["BG"]) foot.pack(fill="x", padx=36, pady=(0, 24)) if i > 0: self._btn(foot, T("Назад"), lambda: self._goto(i - 1)) self._btn(foot, T("Пропустить настройку"), self.skip, dim=True) label = T("Начать играть") if i == 3 else T("Далее") self._btn(foot, label, self._next, accent=True, right=True) def _btn(self, parent, text, cmd, accent=False, dim=False, right=False): p = self.pal bg = p["GREEN"] if accent else (p["BG"] if dim else p["PANEL"]) fg = "#0e1a08" if accent else (p["DIM"] if dim else p["TEXT"]) b = tk.Button(parent, text=text, command=cmd, bg=bg, fg=fg, activebackground=p["GREEN_HI"] if accent else bg, activeforeground=fg, relief="flat", cursor="hand2", font=("Segoe UI", 10, "bold" if accent else "normal"), padx=16, pady=6) b.pack(side="right" if right else "left", padx=(10, 0) if right else (0, 10)) return b def _center(self): self.root.update_idletasks() w, h = 560, 430 x = (self.root.winfo_screenwidth() - w) // 2 y = (self.root.winfo_screenheight() - h) // 2 - 30 self.root.geometry("%dx%d+%d+%d" % (w, h, max(0, x), max(0, y))) def _title(self, parent, text): p = self.pal tk.Label(parent, text=text, bg=p["BG"], fg=p["TEXT"], font=("Segoe UI", 15, "bold")).pack(anchor="w") # ------------------------------------------------------------ шаги def _step0(self, body): """Язык.""" p = self.pal self._title(body, T("Добро пожаловать в PixelLauncher!")) tk.Label(body, wraplength=470, justify="left", bg=p["BG"], fg=p["DIM"], font=("Segoe UI", 10), text=T("Первая настройка займёт минуту: язык, ник и " "память для игры.")).pack(anchor="w", pady=(10, 18)) row = tk.Frame(body, bg=p["BG"]) row.pack(anchor="w") for code, caption in (("ru", "🇷🇺 Русский"), ("en", "🇬🇧 English")): chosen = self.var_lang.get() == code b = tk.Button(row, text=caption, bg=p["GREEN"] if chosen else p["PANEL"], fg="#0e1a08" if chosen else p["TEXT"], activebackground=p["GREEN_HI"], relief="flat", cursor="hand2", font=("Segoe UI", 11, "bold"), padx=22, pady=10, command=lambda c=code: self._set_lang(c)) b.pack(side="left", padx=(0, 12)) tk.Label(body, wraplength=470, justify="left", bg=p["BG"], fg=p["FAINT"], font=("Segoe UI", 9), text=T("Язык можно поменять позже в настройках.") ).pack(anchor="w", pady=(18, 0)) def _set_lang(self, code): self.var_lang.set(code) i18n.set_language(code) self.settings["lang"] = code # сохранится даже при пропуске self._goto(self.step) def _step1(self, body): """Ник.""" p = self.pal self._title(body, T("Как тебя зовут?")) tk.Label(body, bg=p["BG"], fg=p["DIM"], font=("Segoe UI", 10), text=T("Ник игрока (виден в игре):") ).pack(anchor="w", pady=(12, 6)) e = tk.Entry(body, textvariable=self.var_nick, bg=p["INPUT"], fg=p["TEXT"], insertbackground=p["TEXT"], relief="flat", font=("Segoe UI", 13), width=24) e.pack(anchor="w", ipady=7) e.focus_set() e.bind("", lambda ev: self._next()) tk.Label(body, textvariable=self.var_err, bg=p["BG"], fg=p["ERR"], font=("Segoe UI", 9)).pack(anchor="w", pady=(8, 0)) tk.Label(body, wraplength=470, justify="left", bg=p["BG"], fg=p["DIM"], font=("Segoe UI", 9), text=T("Ник: 3-16 символов, латиница, цифры и _") ).pack(anchor="w", pady=(4, 0)) def _step2(self, body): """ОЗУ.""" p = self.pal self._title(body, T("Сколько памяти выделить игре?")) auto = tk.Checkbutton(body, text=T("Подобрать автоматически"), variable=self.var_auto, bg=p["BG"], fg=p["TEXT"], selectcolor=p["INPUT"], activebackground=p["BG"], activeforeground=p["TEXT"], relief="flat", font=("Segoe UI", 10), cursor="hand2", command=self._sync_ram) auto.pack(anchor="w", pady=(14, 8)) self.scale_ram = tk.Scale(body, from_=1024, to=16384, resolution=512, orient="horizontal", showvalue=0, bg=p["BG"], fg=p["TEXT"], troughcolor=p["INPUT"], highlightthickness=0, length=440, sliderrelief="flat", sliderlength=18, command=self._on_ram) self.scale_ram.set(self.var_ram.get()) self.scale_ram.pack(anchor="w") self.lbl_ram = tk.Label(body, text=self._ram_text(), bg=p["BG"], fg=p["GREEN"], font=("Segoe UI", 11, "bold")) self.lbl_ram.pack(anchor="w", pady=(4, 6)) tk.Label(body, wraplength=470, justify="left", bg=p["BG"], fg=p["DIM"], font=("Segoe UI", 9), text=T("Можно оставить авто — лаунчер подберёт значение " "при запуске.")).pack(anchor="w") self._sync_ram() def _on_ram(self, value): try: self.var_ram.set(int(float(value))) except (TypeError, ValueError): return if self.lbl_ram is not None: self.lbl_ram.config(text=self._ram_text()) def _ram_text(self): mb = int(self.var_ram.get()) return T("{mb} МБ ({gb} ГБ)").format(mb=mb, gb=int(round(mb / 1024))) def _sync_ram(self): """Галка «авто» включает/выключает ползунок.""" if self.scale_ram is not None: self.scale_ram.config( state="disabled" if self.var_auto.get() else "normal") def _step3(self, body): """Готово.""" p = self.pal self._title(body, T("Всё готово!")) lang = T("English") if self.var_lang.get() == "en" else "Русский" ram = T("авто") if self.var_auto.get() else self._ram_text() info = "\n".join([T("Ник: {nick}").format( nick=self.var_nick.get().strip() or "Player"), T("Язык: {lang}").format(lang=lang), T("Память: {ram}").format(ram=ram)]) tk.Label(body, justify="left", anchor="w", bg=p["PANEL"], fg=p["TEXT"], font=("Segoe UI", 11), padx=18, pady=14, text=info ).pack(anchor="w", pady=(14, 12)) tk.Label(body, wraplength=470, justify="left", bg=p["BG"], fg=p["DIM"], font=("Segoe UI", 9), text=T("Чтобы пройти настройку заново, удалите файл " "settings.json.")).pack(anchor="w") # ------------------------------------------------------------ действия def _next(self): if self.step == 1 and not NICK_RE.fullmatch( self.var_nick.get().strip()): self.var_err.set(T("Недопустимый ник: латиница, цифры и _, " "3-16 символов")) return if self.step == 3: self.finish() return self._goto(self.step + 1) def finish(self): """Применить выбор: язык, ник, ОЗУ — и закрыть мастер.""" nick = self.var_nick.get().strip() s = self.settings s["lang"] = "en" if self.var_lang.get() == "en" else "ru" i18n.set_language(s["lang"]) s["username"] = nick s["accounts"] = [nick] s["active_account"] = nick s["ram"] = int(self.var_ram.get()) s["auto_ram"] = bool(self.var_auto.get()) s["onboarding_done"] = True self.result = "done" self.root.destroy() def skip(self): """Настроить позже: только зафиксировать, что мастер показан.""" self.settings["onboarding_done"] = True self.result = "skip" self.root.destroy() def run(settings, _auto_close_ms=None): """Показать мастер и дождаться закрытия. settings меняется на месте.""" root = tk.Tk() Wizard(root, settings) if _auto_close_ms: # тестовый режим: просто мигнуть окном root.after(_auto_close_ms, root.destroy) root.mainloop()