import os
import sys
import threading
import time
import zipfile
import shutil
import subprocess
import json
import requests
import locale
import ctypes
import psutil
# ================= ССЫЛКИ НА АРХИВЫ (ОБЪЯВЛЕНЫ ПЕРВЫМИ) =================
URL_BASE_MC = "
"
URL_FABRIC_1214 = "
"
URL_FABRIC_1218 = "
"
F_API_1_21_4 = "
"
F_API_1_21_8 = "
"
# ================= СИСТЕМА =================
if sys.platform == "win32":
ctypes.windll.kernel32.SetConsoleOutputCP(65001)
try: locale.setlocale(locale.LC_ALL, 'ru_RU.UTF-8')
except: locale.setlocale(locale.LC_ALL, '')
def install_libs():
for lib in ["minecraft_launcher_lib", "pywebview"]:
try: __import__(lib.replace("-", "_"))
except ImportError: subprocess.check_call([sys.executable, "-m", "pip", "install", lib, "--quiet"])
install_libs()
import webview
import minecraft_launcher_lib
# ================= КЛИЕНТЫ =================
CHECKS = {
"soul": {"name": "SoulDLC", "tag": "OLD", "url": "
", "folder": "soul", "mode": "clone", "mc": "1.16.5"},
"venus": {"name": "VenusFree", "url": "
", "folder": "VenusFree", "mode": "clone", "mc": "1.16.5"},
"nightdlc113": {"name": "NightDLC 1.1.3", "url": "
", "folder": "nightdlc113", "mode": "clone", "mc": "1.16.5"},
"nightdlc": {"name": "NightDLC 1.1.2", "url": "
", "folder": "night", "mode": "clone", "mc": "1.16.5"},
"dimasik": {"name": "Dimasik", "tag": "OLD", "url": "
", "folder": "dimasik", "mode": "clone", "mc": "1.16.5"},
"neverlose": {"name": "NeverLose", "tag": "OLD", "url": "
", "folder": "NeverLose", "mode": "clone", "mc": "1.16.5"},
"javelin": {"name": "Javelin", "tag": "1.21.8", "url": "
", "folder": "Javelin", "mode": "fabric", "mc": "1.21.8", "api": F_API_1_21_8},
"rockstar": {"name": "RockStar", "tag": "1.21.4", "url": "
", "folder": "RockStar", "mode": "fabric", "mc": "1.21.4", "api": F_API_1_21_4},
"stellar": {"name": "Stellar", "tag": "1.21.4", "url": "
", "folder": "Stellar", "mode": "fabric", "mc": "1.21.4", "api": F_API_1_21_4},
"wild": {"name": "Wild", "tag": "1.21.4", "url": "
", "folder": "Wild", "mode": "fabric", "mc": "1.21.4", "api": F_API_1_21_4},
}
SETTINGS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "settings.json")
# ================= HTML =================
HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@import url('
:root { --bg: #0b0b0b; --panel: #141414; --border: #2a2a2a; --accent: #ffffff; --text: #e0e0e0; }
body.light { --bg: #f5f5f5; --panel: #ffffff; --border: #ddd; --text: #111; }
body { background: var(--bg); color: var(--text); font-family: 'Inter'; margin: 0; padding: 0; height: 100vh; display: flex; flex-direction: column; overflow: hidden; user-select: none; }
.head { height: 45px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 16px; -webkit-app-region: drag; background: var(--panel); }
.head-btns { display: flex; align-items: center; gap: 8px; -webkit-app-region: no-drag; }
.win-btn { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #666; transition: 0.2s; border-radius: 6px; }
.win-btn:hover { color: var(--accent); background: var(--border); }
.win-btn svg { width: 22px; height: 22px; fill: currentColor; }
.body { flex: 1; padding: 20px; display: flex; flex-direction: column; gap: 12px; -webkit-app-region: no-drag; box-sizing: border-box; }
.lbl { font-size: 11px; font-weight: 800; color: #666; text-transform: uppercase; margin-bottom: 2px; }
.custom-select { position: relative; width: 100%; box-sizing: border-box; }
.select-trigger { padding: 12px; border-radius: 8px; background: var(--bg); border: 1px solid var(--border); color: var(--text); font-family: 'JetBrains Mono'; font-size: 12px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; box-sizing: border-box; }
.select-options { position: absolute; top: 100%; left: 0; right: 0; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; margin-top: 5px; max-height: 180px; overflow-y: auto; display: none; z-index: 100; box-shadow: 0 5px 15px rgba(0,0,0,0.5); }
.select-options.show { display: block; }
.option { padding: 10px 12px; font-family: 'JetBrains Mono'; font-size: 12px; cursor: pointer; display: flex; align-items: center; }
.option:hover { background: var(--border); }
.option b, .select-trigger b { color: var(--accent) !important; font-weight: 900 !important; margin: 0 6px; text-transform: uppercase; }
.v-tag { color: #666; font-size: 10px; margin-left: auto; }
input { width: 100%; padding: 12px; border-radius: 8px; background: var(--bg); border: 1px solid var(--border); color: var(--text); font-family: 'JetBrains Mono'; font-size: 12px; outline: none; box-sizing: border-box; }
.ram-box { padding: 5px 0; -webkit-app-region: no-drag !important; }
input[type=range] { -webkit-appearance: none; width: 100%; height: 2px; background: var(--border); outline: none; -webkit-app-region: no-drag !important; }
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; background: var(--accent); border-radius: 50%; cursor: pointer; border: 4px solid var(--panel); -webkit-app-region: no-drag !important; }
.theme-row { display: flex; justify-content: space-between; align-items: center; -webkit-app-region: no-drag; }
.palette { display: flex; gap: 8px; align-items: center; }
.dot { width: 18px; height: 18px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; }
.dot.active { border-color: var(--text); transform: scale(1.1); }
.color-picker-btn { width: 18px; height: 18px; border-radius: 50%; background: linear-gradient(45deg,red,orange,yellow,green,cyan,blue,violet); cursor: pointer; border: 1px solid var(--border); overflow: hidden; position: relative; }
.color-picker-btn input { position: absolute; opacity: 0; width: 100%; height: 100%; cursor: pointer; top:0; left:0; }
.mode-sw { font-size: 10px; font-weight: 800; cursor: pointer; padding: 6px 12px; background: var(--border); border-radius: 6px; }
#launch { width: 100%; padding: 16px; border: none; border-radius: 10px; background: var(--accent); color: #000; font-weight: 800; font-size: 14px; cursor: pointer; margin-top: auto; -webkit-app-region: no-drag; }
.bar-con { height: 4px; background: var(--border); margin-top: 15px; border-radius: 2px; overflow: hidden; }
.bar { height: 100%; width: 0%; background: var(--accent); transition: 0.3s; }
.term { width: 100%; height: 100px; background: #080808; border: 1px solid var(--border); border-radius: 8px; padding: 10px; font-family: 'JetBrains Mono'; font-size: 10px; color: #bbb; overflow-y: auto; display: none; margin-top: 10px; box-sizing: border-box; word-break: break-all; white-space: pre-wrap; }
.term.show { display: block; }
</style>
</head>
<body onclick="closeAll()">
<div class="head">
<div style="font-weight:800; font-size:13px;">MYST LAUNCHER v7</div>
<div class="head-btns">
<div class="win-btn" onclick="pywebview.api.open_url('
')"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69a.2.2 0 00-.05-.18c-.06-.05-.14-.03-.21-.02-.09.02-1.49.95-4.22 2.79-.4.27-.76.41-1.08.4-.36-.01-1.04-.2-1.55-.37-.63-.2-1.12-.31-1.08-.66.02-.18.27-.36.74-.55 2.92-1.27 4.86-2.11 5.83-2.51 2.78-1.16 3.35-1.36 3.73-1.36.08 0 .27.02.39.12.1.08.13.19.14.27-.01.06.01.24 0 .38z"/></svg></div>
<div class="win-btn" onclick="pywebview.api.open_url('
')"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M19.73 4.87a18.2 18.2 0 0 0-4.6-1.44c-.21.4-.4.8-.58 1.21a17.1 17.1 0 0 0-5.1 0 13.5 13.5 0 0 0-.58-1.21 18.2 18.2 0 0 0-4.6 1.44A18.8 18.8 0 0 0 .12 18.05c2.42 1.78 4.76 2.86 7.07 3.58.54-.74 1.01-1.53 1.41-2.37a11.6 11.6 0 0 1-2.18-1.1c.14-.11.29-.22.43-.34 4.66 2.13 9.7 2.13 14.3 0 .14.11.29.23.43.34-.68.44-1.41.81-2.18 1.1.4.84 1.01 1.54 1.41 2.37 2.3-.71 4.65-1.8 7.07-3.58a18.8 18.8 0 0 0-3.66-13.18zM8.02 15.33c-1.18 0-2.16-1.08-2.16-2.42s.96-2.42 2.16-2.42c1.21 0 2.18 1.1 2.16 2.42 0 1.33-.96 2.42-2.16 2.42zm7.97 0c-1.18 0-2.16-1.08-2.16-2.42s.96-2.42 2.16-2.42c1.21 0 2.18 1.1 2.16 2.42 0 1.33-.95 2.42-2.16 2.42z"/></svg></div>
<div style="width:10px"></div>
<div class="win-btn" onclick="pywebview.api.min()">_</div>
<div class="win-btn" onclick="pywebview.api.close()">✕</div>
</div>
</div>
<div class="body">
<div><div class="lbl">Nickname</div><input id="nick" type="text" placeholder="Username" onchange="save()"></div>
<div><div class="lbl">Client Version</div><div class="custom-select" id="custom-ver"><div class="select-trigger" onclick="toggleSelect(event)"><span id="selected-text">Select Client</span><span>▼</span></div><div class="select-options" id="options-list"></div></div></div>
<div class="ram-box"><div class="lbl">RAM ALLOCATION <span id="ram-val" style="color:var(--accent); font-weight:900;">4 GB</span></div><input type="range" id="ram" min="1" max="16" step="1" oninput="updateRam(this.value)" onchange="save()"></div>
<div><div class="lbl">Install Path</div><div style="display:flex; gap:8px"><input id="dl_path" type="text" readonly><button style="padding:0 15px; border-radius:8px; border:1px solid var(--border); background:var(--panel); color:var(--text); cursor:pointer;" onclick="browse_dir()">...</button></div></div>
<div><div class="lbl">Java Path</div><div style="display:flex; gap:8px"><input id="java" type="text" readonly><button style="padding:0 15px; border-radius:8px; border:1px solid var(--border); background:var(--panel); color:var(--text); cursor:pointer;" onclick="browse_java()">...</button></div></div>
<div class="theme-row"><div class="palette"><div class="dot active" style="background:#ffffff" onclick="clr('#ffffff',this)"></div><div class="dot" style="background:#00ff9d" onclick="clr('#00ff9d',this)"></div><div class="dot" style="background:#ff4757" onclick="clr('#ff4757',this)"></div><div class="dot" style="background:#1e90ff" onclick="clr('#1e90ff',this)"></div><div class="dot" style="background:#bf55ec" onclick="clr('#bf55ec',this)"></div><div class="color-picker-btn"><input type="color" oninput="clr(this.value, null)"></div></div><div class="mode-sw" onclick="toggleTheme()">THEME</div></div>
<div style="margin-top:auto"><button id="launch" onclick="go()">LAUNCH CLIENT</button><div class="bar-con"><div class="bar" id="bar"></div></div><div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:5px; font-family:'JetBrains Mono';"><span id="st-txt">IDLE</span><span id="st-pct">0%</span></div></div>
<div class="term" id="term"></div>
</div>
<script>
let currentVer = "";
function toggleSelect(e) { e.stopPropagation(); document.getElementById('options-list').classList.toggle('show'); }
function closeAll() { document.getElementById('options-list').classList.remove('show'); }
function selectOpt(id, name, tag, mc) {
currentVer = id; let tagStr = tag ? ` <b style="color:var(--accent)">${tag}</b>` : "";
document.getElementById('selected-text').innerHTML = name + tagStr + ` <span class="v-tag">${mc}</span>`;
closeAll(); save();
}
function updateRam(v) { document.getElementById('ram-val').innerText = v + ' GB'; }
function toggleTheme() { document.body.classList.toggle('light'); save(); }
function clr(c, el) {
document.documentElement.style.setProperty('--accent', c);
document.querySelectorAll('.dot').forEach(d => d.classList.remove('active'));
if(el) el.classList.add('active'); save();
}
function browse_java() { pywebview.api.browse_java().then(p => { if(p) { document.getElementById('java').value = p; save(); }}); }
function browse_dir() { pywebview.api.browse_dir().then(p => { if(p) { document.getElementById('dl_path').value = p; save(); }}); }
function save() {
pywebview.api.save_cfg({nick: document.getElementById('nick').value, ver: currentVer, ram: document.getElementById('ram').value, java: document.getElementById('java').value, dl_path: document.getElementById('dl_path').value, theme: document.body.classList.contains('light')?'light':'dark', color: document.documentElement.style.getPropertyValue('--accent')});
}
function restore(d, versions, maxRam) {
const list = document.getElementById('options-list');
versions.forEach(v => {
const div = document.createElement('div'); div.className = 'option';
div.onclick = () => selectOpt(v.id, v.name, v.tag, v.mc);
div.innerHTML = `<span>${v.name}</span>${v.tag ? `<b style="color:var(--accent); font-weight:900;">${v.tag}</b>` : ""}<span class="v-tag">${v.mc}</span>`;
list.appendChild(div); if(d.ver === v.id) selectOpt(v.id, v.name, v.tag, v.mc);
});
document.getElementById('ram').max = maxRam;
if(d.nick) document.getElementById('nick').value = d.nick;
if(d.ram) { document.getElementById('ram').value = d.ram; updateRam(d.ram); }
if(d.java) document.getElementById('java').value = d.java;
if(d.dl_path) document.getElementById('dl_path').value = d.dl_path;
if(d.theme === 'light') document.body.classList.add('light');
if(d.color) clr(d.color, Array.from(document.querySelectorAll('.dot')).find(el => el.style.backgroundColor.includes(d.color)));
}
function go() { document.getElementById('launch').disabled = true; document.getElementById('term').classList.add('show'); document.getElementById('term').innerHTML = ''; pywebview.api.start(); }
function log(m, t) { const c = document.getElementById('term'); const d = document.createElement('div'); if(t==='err') d.style.color='#f55'; else if(t==='ok') d.style.color='#5f5'; d.innerText = '> ' + m; c.appendChild(d); c.scrollTop = c.scrollHeight; }
function prog(v, t) { document.getElementById('bar').style.width = v + '%'; document.getElementById('st-pct').innerText = Math.round(v) + '%'; if(t) document.getElementById('st-txt').innerText = t.toUpperCase(); }
function done() { document.getElementById('launch').disabled = false; }
</script>
</body>
</html>
"""
# ================= ЛОГИКА =================
class Api:
def init(self):
max_ram = int(psutil.virtual_memory().total / (1024**3))
self.cfg = self.load_settings()
if not self.cfg.get('java'): self.cfg['java'] = self.find_java()
if not self.cfg.get('dl_path'): self.cfg['dl_path'] = os.path.join(os.path.expanduser("~"), "Desktop")
v_list = [{"id": k, "name": v['name'], "tag": v.get('tag', ''), "mc": v['mc']} for k, v in CHECKS.items()]
window.evaluate_js(f'restore({json.dumps(self.cfg)}, {json.dumps(v_list)}, {max_ram})')
def load_settings(self):
if os.path.exists(SETTINGS_FILE):
try: return json.load(open(SETTINGS_FILE, 'r'))
except: return {}
return {}
def find_java(self):
for v in ["21", "17", "8"]:
p = f"C:\\Program Files\\Java\\jdk-{v}\\bin\\javaw.exe"
if os.path.exists(p): return p
return shutil.which("javaw") or ""
def save_cfg(self, d): self.cfg = d; open(SETTINGS_FILE, 'w').write(json.dumps(d))
def min(self): window.minimize()
def close(self): window.destroy()
def open_url(self, u): import webbrowser; webbrowser.open(u)
def browse_java(self):
res = window.create_file_dialog(webview.OPEN_DIALOG, file_types=('Executables (*.exe)',))
return res[0] if res else None
def browse_dir(self):
res = window.create_file_dialog(webview.FOLDER_DIALOG)
return res[0] if res else None
def log(self, m, t=''): window.evaluate_js(f'log({json.dumps(str(m))}, "{t}")')
def prog(self, v, t=None): window.evaluate_js(f'prog({v}, {json.dumps(t)})')
def _dl_v1(self, url, path, p_val, p_txt):
self.log(f"Downloading {p_txt}...")
self.prog(p_val, p_txt)
try:
r = requests.get(url, timeout=300)
if r.status_code == 200:
with open(path, 'wb') as f: f.write(r.content)
return True
return False
except Exception as e:
self.log(f"Error: {e}", "err"); return False
def _extract_v1(self, zip_path, target_dir, internal_folder=None):
"""Простая распаковка с опциональным переносом из внутренней папки."""
self.log(f"Extracting {os.path.basename(zip_path)}...")
temp_dir = os.path.join(target_dir, "_tmp_ext")
if os.path.exists(temp_dir): shutil.rmtree(temp_dir)
os.makedirs(temp_dir)
with zipfile.ZipFile(zip_path, 'r') as z:
z.extractall(temp_dir)
source = temp_dir
if internal_folder:
potential = os.path.join(temp_dir, internal_folder)
if os.path.exists(potential): source = potential
for item in os.listdir(source):
s, d = os.path.join(source, item), os.path.join(target_dir, item)
if os.path.isdir(s):
if os.path.exists(d): shutil.rmtree(d)
shutil.move(s, d)
else: shutil.move(s, d)
shutil.rmtree(temp_dir)
def start(self): threading.Thread(target=self._run, daemon=True).start()
def _run(self):
try:
info = CHECKS[self.cfg['ver']]
java, ram, nick = self.cfg['java'], self.cfg.get('ram', 4), self.cfg.get('nick', 'Player')
myst_dir = os.path.join(self.cfg['dl_path'], "MystLauncher")
os.makedirs(myst_dir, exist_ok=True)
if info['mode'] == 'fabric':
self._run_fabric(info, java, ram, nick, myst_dir)
else:
self._run_standard(info, java, ram, nick, myst_dir)
except Exception as e:
self.log(f"Launcher Error: {e}", "err")
window.evaluate_js('done()')
def _run_fabric(self, info, java, ram, nick, myst_dir):
# Папка чита: MystLauncher/Stellar/
cheat_dir = os.path.join(myst_dir, info['folder'])
os.makedirs(cheat_dir, exist_ok=True)
mc_dir = os.path.join(cheat_dir, ".minecraft")
os.makedirs(mc_dir, exist_ok=True)
# 1. Твоя база .minecraft.zip
if not os.path.exists(os.path.join(mc_dir, "libraries")):
zip_p = os.path.join(cheat_dir, "base.zip")
if self._dl_v1(URL_BASE_MC, zip_p, 10, "Base Minecraft"):
self._extract_v1(zip_p, cheat_dir, ".minecraft")
os.remove(zip_p)
# 2. Твой Fabric Zip
if not os.path.exists(os.path.join(mc_dir, "versions")):
f_url = URL_FABRIC_1218 if "1.21.8" in info['mc'] else URL_FABRIC_1214
f_zip = os.path.join(cheat_dir, "fabric.zip")
f_folder = f"Fabric {info['mc']}"
if self._dl_v1(f_url, f_zip, 40, "Fabric Components"):
self._extract_v1(f_zip, mc_dir, f_folder)
os.remove(f_zip)
# 3. Мод и API
mods_dir = os.path.join(mc_dir, "mods")
os.makedirs(mods_dir, exist_ok=True)
self._dl_v1(info['api'], os.path.join(mods_dir, "fabric-api.jar"), 70, "API")
self._dl_v1(info['url'], os.path.join(mods_dir, "client.jar"), 85, "Cheat Mod")
v_folder = os.path.join(mc_dir, "versions")
v_id = next(d for d in os.listdir(v_folder) if "fabric" in d.lower())
self._launch(v_id, java, ram, nick, mc_dir)
def _run_standard(self, info, java, ram, nick, myst_dir):
# 1.16.5 в общую папку MystLauncher/
mc_dir = os.path.join(myst_dir, ".minecraft")
os.makedirs(mc_dir, exist_ok=True)
if not os.path.exists(os.path.join(mc_dir, "libraries")):
zip_p = os.path.join(myst_dir, "base_legacy.zip")
if self._dl_v1(URL_BASE_MC, zip_p, 10, "Base Assets"):
self._extract_v1(zip_p, myst_dir, ".minecraft")
os.remove(zip_p)
target = os.path.join(mc_dir, "versions", info['folder'])
jar = os.path.join(target, f"{info['folder']}.jar")
if not os.path.exists(jar):
zip_p = os.path.join(myst_dir, "cheat_116.zip")
if self._dl_v1(info['url'], zip_p, 60, "Cheat Files"):
os.makedirs(target, exist_ok=True)
with zipfile.ZipFile(zip_p, 'r') as z: z.extractall(target)
os.remove(zip_p)
for r, d, files in os.walk(target):
for f in files:
if f.endswith(".jar"): shutil.move(os.path.join(r, f), jar); break
self._launch(info['folder'], java, ram, nick, mc_dir)
def _launch(self, v_id, java, ram, nick, g_dir):
self.prog(95, "Ready")
# Поиск библиотек в правильной папке
opts = {
"username": nick, "uuid": "0", "token": "0", "executablePath": java,
"gameDirectory": g_dir,
"jvmArguments": [f"-Xmx{ram}G", "-noverify", "-Dfile.encoding=UTF-8"]
}
try:
# Важно: minecraft_launcher_lib должна знать, где искать библиотеки
cmd = minecraft_launcher_lib.command.get_minecraft_command(v_id, g_dir, opts)
# Фикс для jopt-simple (для 1.16.5)
lib_j = os.path.join(g_dir, "libraries", "net", "sf", "jopt-simple", "jopt-simple", "5.0.4", "jopt-simple-5.0.4.jar")
if os.path.exists(lib_j):
cp_idx = cmd.index("-cp") + 1
if lib_j not in cmd[cp_idx]: cmd[cp_idx] += os.pathsep + lib_j
self.log(f"Launching {v_id}...", "ok"); self.prog(100, "Launched")
subprocess.Popen(cmd, cwd=g_dir)
except Exception as e:
self.log(f"Launch Error: {e}", "err")
if __name__ == '__main__':
api = Api()
window = webview.create_window("Myst Launcher v7", html=HTML, width=420, height=720, frameless=True, js_api=api)
webview.start(api.init)