From 300640844775f45143126e1396ab6b7f96844f10 Mon Sep 17 00:00:00 2001 From: SashegDev Date: Wed, 2 Sep 2026 00:57:43 +0000 Subject: [PATCH] =?UTF-8?q?site+server:=20add=20manual=20Zern=20Prologue?= =?UTF-8?q?=20pack=20variant=20(download=20zip=20+=20instructions)=20?= =?UTF-8?q?=E2=80=94=20site=20card=20with=20path=20%USERPROFILE%\.zernmc\i?= =?UTF-8?q?nstances\ZernPrologue,=20prism/multimc=20import,=20endpoint=20/?= =?UTF-8?q?pack/ZernPrologue/zip=20(requires=20pass)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zernmc/launcher/installer/Installer.java | 469 ------------------ server/main.py | 26 + server/site/index.html | 34 ++ 3 files changed, 60 insertions(+), 469 deletions(-) delete mode 100644 launcher/launcher/src/main/java/me/sashegdev/zernmc/launcher/installer/Installer.java diff --git a/launcher/launcher/src/main/java/me/sashegdev/zernmc/launcher/installer/Installer.java b/launcher/launcher/src/main/java/me/sashegdev/zernmc/launcher/installer/Installer.java deleted file mode 100644 index d8c5613..0000000 --- a/launcher/launcher/src/main/java/me/sashegdev/zernmc/launcher/installer/Installer.java +++ /dev/null @@ -1,469 +0,0 @@ -package me.sashegdev.zernmc.launcher.installer; - -import javax.swing.*; -import javax.swing.plaf.basic.BasicProgressBarUI; -import java.awt.*; -import java.awt.event.*; -import java.io.*; -import java.net.*; -import java.nio.file.*; -import java.security.MessageDigest; -import java.util.*; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import com.google.gson.*; - -public class Installer { - private static final String BASE_URL = "https://api.zern.cc"; - private static JFrame frame; - private static CardLayout cards; - private static JPanel mainPanel; - private static JProgressBar progressBar; - private static JLabel statusLabel, speedLabel, titleLabel; - private static JTextField dirField; - private static JCheckBox shortcutCheck; - private static String installDir = "C:\\ZernMC"; - private static boolean isOffline = false; - private static final Color bg = new Color(0x0c,0x0c,0x12); - private static final Color surface = new Color(0x16,0x16,0x1f); - private static final Color accent = new Color(0xe9,0x45,0x60); - private static final Color text = new Color(0xee,0xee,0xf0); - private static final Color muted = new Color(0x88,0x88,0x9a); - private static final Color sidebarBg = new Color(0x10,0x10,0x1e); - private static int currentStep = 0; - private static JLabel[] stepLabels; - - public static void main(String[] args) throws Exception { - for (String a: args) if (a.equals("--offline")) isOffline = true; - SwingUtilities.invokeLater(() -> createUI()); - } - - private static void createUI() { - try { UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); } catch(Exception ignored){} - frame = new JFrame("ZernMC Setup"); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.setSize(640, 420); - frame.setLocationRelativeTo(null); - frame.setResizable(false); - frame.setUndecorated(false); - - JPanel root = new JPanel(new BorderLayout()); - root.setBackground(bg); - - JPanel sidebar = new JPanel(); - sidebar.setBackground(sidebarBg); - sidebar.setPreferredSize(new Dimension(180, 420)); - sidebar.setLayout(new BoxLayout(sidebar, BoxLayout.Y_AXIS)); - sidebar.setBorder(BorderFactory.createEmptyBorder(24, 18, 24, 18)); - - JLabel logo = new JLabel("ZernMC"); - logo.setFont(new Font("Segoe UI", Font.BOLD, 22)); - logo.setForeground(text); - logo.setAlignmentX(Component.LEFT_ALIGNMENT); - sidebar.add(logo); - JLabel sub = new JLabel("Setup"); - sub.setFont(new Font("Segoe UI", Font.PLAIN, 12)); - sub.setForeground(accent); - sub.setAlignmentX(Component.LEFT_ALIGNMENT); - sidebar.add(sub); - sidebar.add(Box.createVerticalStrut(24)); - JSeparator sep = new JSeparator(); - sep.setMaximumSize(new Dimension(144,1)); - sep.setForeground(new Color(0x2a,0x2a,0x3a)); - sidebar.add(sep); - sidebar.add(Box.createVerticalStrut(18)); - - String[] steps = {"Приветствие","Папка установки","Установка","Готово"}; - stepLabels = new JLabel[steps.length]; - for (int i=0;i System.exit(0)); - JButton next = new JButton("Далее >"); - styleNav(next, true); - next.addActionListener(e -> onNext()); - next.putClientProperty("next", true); - nav.add(cancel); - nav.add(next); - content.add(nav, BorderLayout.SOUTH); - - root.add(content, BorderLayout.CENTER); - frame.setContentPane(root); - frame.setVisible(true); - } - - private static void styleNav(JButton b, boolean primary) { - b.setFont(new Font("Segoe UI", Font.PLAIN, 12)); - b.setFocusPainted(false); - b.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(primary?accent:new Color(0x3a,0x3a,0x4a),1), - BorderFactory.createEmptyBorder(6, 16, 6, 16))); - b.setBackground(primary?accent:surface); - b.setForeground(primary?Color.WHITE:text); - b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - } - - private static JPanel createWelcome() { - JPanel p = new JPanel(); - p.setBackground(surface); - p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); - JLabel t = new JLabel("Добро пожаловать в ZernMC"); - t.setFont(new Font("Segoe UI", Font.BOLD, 16)); - t.setForeground(text); - t.setAlignmentX(Component.LEFT_ALIGNMENT); - p.add(t); - p.add(Box.createVerticalStrut(10)); - JLabel d = new JLabel("Мастер установит ZernMC Launcher на ваш компьютер.
Рекомендуется закрыть другие приложения.

Нажмите «Далее», чтобы продолжить."); - d.setFont(new Font("Segoe UI", Font.PLAIN, 12)); - d.setForeground(muted); - d.setAlignmentX(Component.LEFT_ALIGNMENT); - p.add(d); - p.add(Box.createVerticalStrut(12)); - JLabel note = new JLabel("Онлайн — скачает 47М JRE + 50М лаунчер  |  Оффлайн — распакует встроенный архив"); - note.setFont(new Font("Segoe UI", Font.PLAIN, 11)); - note.setForeground(muted); - note.setAlignmentX(Component.LEFT_ALIGNMENT); - p.add(note); - return p; - } - - private static JPanel createDirPanel() { - JPanel p = new JPanel(); - p.setBackground(surface); - p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); - JLabel t = new JLabel("Выберите папку установки"); - t.setFont(new Font("Segoe UI", Font.BOLD, 14)); - t.setForeground(text); - t.setAlignmentX(Component.LEFT_ALIGNMENT); - p.add(t); - p.add(Box.createVerticalStrut(12)); - JPanel row = new JPanel(new BorderLayout(8,0)); - row.setBackground(surface); - row.setMaximumSize(new Dimension(420, 28)); - row.setAlignmentX(Component.LEFT_ALIGNMENT); - dirField = new JTextField(installDir); - dirField.setFont(new Font("Segoe UI", Font.PLAIN, 12)); - dirField.setBackground(bg); - dirField.setForeground(text); - dirField.setCaretColor(text); - dirField.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(new Color(0x2a,0x2a,0x3a)), BorderFactory.createEmptyBorder(4,6,4,6))); - JButton browse = new JButton("Обзор..."); - browse.setFont(new Font("Segoe UI", Font.PLAIN, 11)); - browse.addActionListener(e -> { - JFileChooser fc = new JFileChooser(); - fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - fc.setDialogTitle("Куда установить ZernMC"); - if (fc.showOpenDialog(frame)==JFileChooser.APPROVE_OPTION) { - String sel = fc.getSelectedFile().getAbsolutePath(); - if (!sel.toLowerCase().contains("zernmc")) sel = sel + File.separator + "ZernMC"; - dirField.setText(sel); - } - }); - row.add(dirField, BorderLayout.CENTER); - row.add(browse, BorderLayout.EAST); - p.add(row); - p.add(Box.createVerticalStrut(10)); - JLabel hint = new JLabel("По умолчанию: C:\\ZernMC • Данные в %USERPROFILE%\\.zernmc"); - hint.setFont(new Font("Segoe UI", Font.PLAIN, 10)); - hint.setForeground(muted); - hint.setAlignmentX(Component.LEFT_ALIGNMENT); - p.add(hint); - p.add(Box.createVerticalStrut(12)); - shortcutCheck = new JCheckBox("Создать ярлык на рабочем столе", true); - shortcutCheck.setBackground(surface); - shortcutCheck.setForeground(text); - shortcutCheck.setFont(new Font("Segoe UI", Font.PLAIN, 12)); - shortcutCheck.setAlignmentX(Component.LEFT_ALIGNMENT); - p.add(shortcutCheck); - return p; - } - - private static JPanel createProgressPanel() { - JPanel p = new JPanel(); - p.setBackground(surface); - p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); - titleLabel = new JLabel("Установка..."); - titleLabel.setFont(new Font("Segoe UI", Font.PLAIN, 12)); - titleLabel.setForeground(muted); - titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT); - p.add(titleLabel); - p.add(Box.createVerticalStrut(18)); - statusLabel = new JLabel("Подготовка..."); - statusLabel.setFont(new Font("Segoe UI", Font.PLAIN, 13)); - statusLabel.setForeground(text); - statusLabel.setAlignmentX(Component.CENTER_ALIGNMENT); - p.add(statusLabel); - p.add(Box.createVerticalStrut(12)); - progressBar = new JProgressBar(0,100); - progressBar.setPreferredSize(new Dimension(400,6)); - progressBar.setMaximumSize(new Dimension(400,6)); - progressBar.setAlignmentX(Component.CENTER_ALIGNMENT); - progressBar.setBackground(new Color(0x2a,0x2a,0x3a)); - progressBar.setForeground(accent); - progressBar.setBorderPainted(false); - progressBar.setUI(new BasicProgressBarUI(){ protected Color getSelectionBackground(){return accent;} protected Color getSelectionForeground(){return accent;} }); - p.add(progressBar); - p.add(Box.createVerticalStrut(6)); - speedLabel = new JLabel(" "); - speedLabel.setFont(new Font("Segoe UI", Font.PLAIN, 11)); - speedLabel.setForeground(muted); - speedLabel.setAlignmentX(Component.CENTER_ALIGNMENT); - p.add(speedLabel); - return p; - } - - private static JPanel createFinishPanel() { - JPanel p = new JPanel(); - p.setBackground(surface); - p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); - JLabel t = new JLabel("Установка завершена!"); - t.setFont(new Font("Segoe UI", Font.BOLD, 16)); - t.setForeground(text); - t.setAlignmentX(Component.CENTER_ALIGNMENT); - p.add(t); - p.add(Box.createVerticalStrut(10)); - JLabel d = new JLabel("
ZernMC Launcher установлен.
Запустите с ярлыка или из C:\\ZernMC\\zernmc.exe
Удаление: C:\\ZernMC\\uninstall.exe
"); - d.setFont(new Font("Segoe UI", Font.PLAIN, 12)); - d.setForeground(muted); - d.setAlignmentX(Component.CENTER_ALIGNMENT); - p.add(d); - p.add(Box.createVerticalStrut(18)); - JButton launch = new JButton("Запустить ZernMC"); - styleNav(launch, true); - launch.setAlignmentX(Component.CENTER_ALIGNMENT); - launch.addActionListener(e -> { - try { Runtime.getRuntime().exec(new String[]{dirField.getText()+File.separator+"zernmc.exe"}); } catch(Exception ignored){} - System.exit(0); - }); - p.add(launch); - return p; - } - - private static void onNext() { - if (currentStep==0) { currentStep=1; updateSteps(); cards.show(mainPanel,"dir"); } - else if (currentStep==1) { - installDir = dirField.getText().trim(); - if (installDir.isEmpty()) { JOptionPane.showMessageDialog(frame,"Укажите папку"); return; } - currentStep=2; updateSteps(); cards.show(mainPanel,"progress"); - // disable nav - new Thread(() -> { - try { doInstall(installDir, shortcutCheck.isSelected()); SwingUtilities.invokeLater(() -> { currentStep=3; updateSteps(); cards.show(mainPanel,"finish"); }); } - catch(Exception ex){ SwingUtilities.invokeLater(() -> { JOptionPane.showMessageDialog(frame,"Ошибка: "+ex.getMessage()); currentStep=1; updateSteps(); cards.show(mainPanel,"dir"); }); } - }).start(); - } else if (currentStep==3) System.exit(0); - } - - private static void updateSteps() { - for (int i=0;i0) { int toRead = (int)Math.min(buf.length, rem); int r = raf.read(buf,0,toRead); if(r<=0) break; out.write(buf,0,r); rem-=r; } - } - found = true; - } - } - } - } catch(Exception ignored){} - if (!found) { - // try adjacent ZernMC-win-*.zip next to exe - try { - String self = Installer.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath(); - Path exeDir = Paths.get(self).getParent(); - if (exeDir!=null) { - try (var stream = Files.list(exeDir)) { - for (Path cand : stream.toList()) { - String n = cand.getFileName().toString(); - if (n.startsWith("ZernMC-win-") && n.endsWith(".zip") || n.startsWith("ZernMC-Offline") && n.endsWith(".zip")) { - Files.copy(cand, tmp, StandardCopyOption.REPLACE_EXISTING); - found = true; break; - } - } - } - if (!found) { - // also check current dir - try (var s2 = Files.list(Paths.get("."))) { - for (Path cand : s2.toList()) { - String n = cand.getFileName().toString(); - if (n.startsWith("ZernMC-win-") && n.endsWith(".zip")) { Files.copy(cand, tmp, StandardCopyOption.REPLACE_EXISTING); found=true; break; } - } - } - } - } - } catch(Exception ignored){} - } - } - if (!found) throw new IOException("offline.zip не найден. Положите ZernMC-win-1.1.1.4.zip рядом с установщиком или соберите standalone offline"); - unzip(tmp, base); - Files.deleteIfExists(tmp); - } - setStatus("Создание ярлыка...", 90); - if (shortcut) createShortcut(dir); - createUninstaller(dir); - setStatus("Готово!",100); - } - - private static long[] findAppendedZipOffset(Path exe) throws Exception { - try (java.io.RandomAccessFile raf = new java.io.RandomAccessFile(exe.toFile(), "r")) { - long len = raf.length(); - long scan = Math.min(len, 1024*1024); - for (long pos = len - 22; pos >= len - scan; pos--) { - raf.seek(pos); - int b0 = raf.read(); int b1 = raf.read(); int b2 = raf.read(); int b3 = raf.read(); - if (b0==0x50 && b1==0x4b && b2==0x05 && b3==0x06) { - for (long s = pos; s >= Math.max(0, pos - 110*1024*1024); s--) { - raf.seek(s); - int a0=raf.read(); int a1=raf.read(); int a2=raf.read(); int a3=raf.read(); - if (a0==0x50 && a1==0x4b && a2==0x03 && a3==0x04) { return new long[]{s, len - s}; } - } - } - } - } - return null; - } - private static void setStatus(String s, int pct){ SwingUtilities.invokeLater(() -> { statusLabel.setText(s); progressBar.setValue(pct); titleLabel.setText(pct+"%"); }); } - private static JsonObject fetchMeta() throws Exception { - String ver = httpGet(BASE_URL+"/launcher/version"); - JsonObject jo = JsonParser.parseString(ver).getAsJsonObject(); - String v = jo.get("version").getAsString(); - String metaStr = httpGet(BASE_URL+"/launcher/meta/"+v); - JsonObject meta = JsonParser.parseString(metaStr).getAsJsonObject(); - if (!meta.has("version")) meta.addProperty("version", v); - return meta; - } - private static String httpGet(String url) throws Exception { - HttpURLConnection c=(HttpURLConnection)new URL(url).openConnection(); - c.setConnectTimeout(5000); c.setReadTimeout(10000); - try(BufferedReader br=new BufferedReader(new InputStreamReader(c.getInputStream()))){ StringBuilder sb=new StringBuilder(); String l; while((l=br.readLine())!=null) sb.append(l); return sb.toString(); } - } - private static void downloadFile(String url, Path dest, long expected) throws Exception { - HttpURLConnection c=(HttpURLConnection)new URL(url).openConnection(); - c.setConnectTimeout(8000); c.setReadTimeout(60000); - if (c.getResponseCode()!=200) throw new IOException("HTTP "+c.getResponseCode()+" "+url); - long total = expected>0?expected:c.getContentLengthLong(); - try(InputStream in=c.getInputStream(); OutputStream out=Files.newOutputStream(dest)){ byte[] buf=new byte[65536]; long done=0; long start=System.currentTimeMillis(); int n; while((n=in.read(buf))>0){ out.write(buf,0,n); done+=n; if(System.currentTimeMillis()-start>200){ double pct= total>0? done*100.0/total:0; speedLabel.setText(String.format("%.1f MB / %.1f MB", done/1024.0/1024, total/1024.0/1024)); } } } - } - private static void unzip(Path zip, Path dest) throws Exception { - try(ZipInputStream zis=new ZipInputStream(Files.newInputStream(zip))){ ZipEntry e; while((e=zis.getNextEntry())!=null){ Path p=dest.resolve(e.getName()); if (!p.normalize().startsWith(dest.normalize())) throw new IOException("ZipSlip"); if(e.isDirectory()) Files.createDirectories(p); else { Files.createDirectories(p.getParent()); try(OutputStream o=Files.newOutputStream(p)){ zis.transferTo(o); }} zis.closeEntry(); } } - } - private static String sha256(Path p) throws Exception { MessageDigest d=MessageDigest.getInstance("SHA-256"); try(InputStream in=Files.newInputStream(p)){ byte[] b=new byte[8192]; int n; while((n=in.read(b))>0) d.update(b,0,n);} StringBuilder sb=new StringBuilder(); for(byte x:d.digest()) sb.append(String.format("%02x",x)); return sb.toString(); } - private static void deleteRecursively(Path p) throws Exception { if(Files.isDirectory(p)) try(var s=Files.list(p)){ for(Path c: s.toList()) deleteRecursively(c);} Files.deleteIfExists(p);} - private static void createShortcut(String dir) throws Exception { - String desktop = System.getenv("USERPROFILE")+"\\Desktop"; - if(!Files.exists(Paths.get(desktop))) desktop = System.getenv("USERPROFILE")+"\\OneDrive\\Desktop"; - String lnk = desktop+"\\ZernMC Launcher.lnk"; - String target = dir+"\\zernmc.exe"; - String ps = "$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('"+lnk+"'); $Shortcut.TargetPath = '"+target+"'; $Shortcut.WorkingDirectory = '"+dir+"'; $Shortcut.IconLocation = '"+target+"'; $Shortcut.Save()"; - Path tmp = Paths.get(System.getProperty("java.io.tmpdir"),"sc.ps1"); - Files.writeString(tmp, ps); - new ProcessBuilder("powershell","-ExecutionPolicy","Bypass","-File",tmp.toString()).start().waitFor(); - Files.deleteIfExists(tmp); - } - private static void createUninstaller(String dir) throws Exception { - Path exe = Paths.get(dir,"uninstall.exe"); - // copy self as uninstall.exe - String self = Installer.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath(); - Path selfPath = Paths.get(new URI("file://"+self)); - if(Files.exists(selfPath) && !selfPath.equals(exe)) try{ Files.copy(selfPath, exe, StandardCopyOption.REPLACE_EXISTING);}catch(Exception ignored){} - Files.writeString(Paths.get(dir,"uninstall.bat"), "@echo off\ndel \"%USERPROFILE%\\Desktop\\ZernMC Launcher.lnk\"\n echo data in %USERPROFILE%\\.zernmc kept\n"); - } -} diff --git a/server/main.py b/server/main.py index 588ad5f..7c133d1 100644 --- a/server/main.py +++ b/server/main.py @@ -1364,6 +1364,32 @@ async def get_pack_file(pack_name: str, file_path: str, request: Request, curren return await send_file_async(full_path, request, cache=True) +@app.get("/pack/{pack_name}/zip") +async def download_pack_zip(pack_name: str, request: Request, current_user: dict = Depends(get_current_user)): + """Download whole pack as zip for manual installation (requires pass)""" + if not has_permission(current_user["role"], Permissions.DOWNLOAD_PACK): + raise HTTPException(403, "Requires active pass") + if is_pack_disabled(pack_name): + raise HTTPException(403, "Pack is disabled") + pack_path = PACKS_DIR / pack_name + if not pack_path.exists() or not pack_path.is_dir(): + raise HTTPException(404, "Pack not found") + import tempfile, zipfile + tmp = Path(tempfile.gettempdir()) / f"{pack_name}.zip" + # re-use if fresh (<5 min) + if tmp.exists() and (datetime.utcnow().timestamp() - tmp.stat().st_mtime) < 300: + return await send_file_async(tmp, request, content_type="application/zip", cache=False) + # create zip + def _zip(): + with zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) as zf: + for f in pack_path.rglob("*"): + if f.is_file(): + zf.write(f, f.relative_to(pack_path)) + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, _zip) + return await send_file_async(tmp, request, content_type="application/zip", cache=False) + + # ====================== ЭНДПОИНТЫ ДЛЯ ЛАУНЧЕРА ====================== def get_current_launcher_version() -> str: diff --git a/server/site/index.html b/server/site/index.html index 6cc7b5b..8cd48dd 100644 --- a/server/site/index.html +++ b/server/site/index.html @@ -438,6 +438,40 @@ git clone ssh://git@git.swe.zernmc.ru:2222/sasheg/launcher.git + + +
+
+
+ +
+
+

Zern Prologue — ручная установка

+

1.21.1 NeoForge · Create: Aeronautics · без лаунчера

+
+ v4 · 1.8 ГБ +
+

Скачай архив модпака и распакуй вручную. Подходит если лаунчер не качает или хочешь поставить на Prism / MultiMC / ATLauncher.

+
+ %USERPROFILE%\.zernmc\instances\ZernPrologue + · ~/.zernmc/instances/ZernPrologue на Linux +
+
+ + + Скачать Prologue.zip + + +
+
Инструкция +
    +
  1. Нажми «Скачать Prologue.zip» (требует проходку — войди в лаунчере хотя бы раз, иначе 403).
  2. +
  3. Распаковать в папку выше (создай если нет). Если там уже есть Prologue — замени.
  4. +
  5. Для Prism/MultiMC: Импорт → «Import from zip» или «Add instance → Import from folder» укажи распакованную папку, Java 21.
  6. +
  7. Запусти через ZernMC лаунчер или свой — вход на mc.zernmc.ru
  8. +
+
+

Без платных подписок и рекламы. Просто лаунчер. Системные требования: Windows 10+, 4 ГБ ОЗУ.