From 615ee5cadc888db359ba1baf91b966e8e7c5b1fc Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 08:19:13 +0800 Subject: [PATCH 01/88] fix: resolve cross-platform EPERM permissions errors modManager.js: - Switch from hardcoded 'junction' to dynamic symlink type based on OS (fixing Linux EPERM). - Add retry logic for directory removal to handle file locking race conditions. - Improve broken symlink detection during profile sync. gameManager.js: - Implement retry loop (3 attempts) for game directory removal in updateGameFiles to prevent EBUSY/EPERM errors on Windows. paths.js: - Prevent fs.mkdirSync failure in getModsPath by pre-checking for broken symbolic links. --- backend/core/paths.js | 13 ++++++++++-- backend/managers/gameManager.js | 16 ++++++++++++++- backend/managers/modManager.js | 35 +++++++++++++++++++++++++-------- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/backend/core/paths.js b/backend/core/paths.js index 17a7b92..b6aa309 100644 --- a/backend/core/paths.js +++ b/backend/core/paths.js @@ -179,8 +179,17 @@ async function getModsPath(customInstallPath = null) { const profilesPath = path.join(userDataPath, 'Profiles'); if (!fs.existsSync(modsPath)) { - // Ensure the Mods directory exists - fs.mkdirSync(modsPath, { recursive: true }); + // Check for broken symlink to avoid EEXIST/EPERM on mkdir + let isBrokenLink = false; + try { + const stats = fs.lstatSync(modsPath); + if (stats.isSymbolicLink()) isBrokenLink = true; + } catch (e) { /* ignore */ } + + if (!isBrokenLink) { + // Ensure the Mods directory exists + fs.mkdirSync(modsPath, { recursive: true }); + } } if (!fs.existsSync(disabledModsPath)) { fs.mkdirSync(disabledModsPath, { recursive: true }); diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index 2fb8b62..963bbdd 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -365,7 +365,21 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, if (fs.existsSync(gameDir)) { console.log('Removing old game files...'); - fs.rmSync(gameDir, { recursive: true, force: true }); + let retries = 3; + while (retries > 0) { + try { + fs.rmSync(gameDir, { recursive: true, force: true }); + break; + } catch (err) { + if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) { + retries--; + console.log(`[UpdateGameFiles] Removal failed with ${err.code}, retrying in 1s... (${retries} retries left)`); + await new Promise(resolve => setTimeout(resolve, 1000)); + } else { + throw err; + } + } + } } fs.renameSync(tempUpdateDir, gameDir); diff --git a/backend/managers/modManager.js b/backend/managers/modManager.js index 7929e8a..631db7f 100644 --- a/backend/managers/modManager.js +++ b/backend/managers/modManager.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const axios = require('axios'); +const { getOS } = require('../utils/platformUtils'); const { getModsPath, getProfilesDir } = require('../core/paths'); const { saveModsToConfig, loadModsFromConfig } = require('../core/config'); const profileManager = require('./profileManager'); @@ -307,11 +308,16 @@ async function syncModsForCurrentProfile() { // 2. Symlink / Migration Logic let needsLink = false; + let globalStats = null; + + try { + globalStats = fs.lstatSync(globalModsPath); + } catch (e) { + // Path doesn't exist + } - if (fs.existsSync(globalModsPath)) { - const stats = fs.lstatSync(globalModsPath); - - if (stats.isSymbolicLink()) { + if (globalStats) { + if (globalStats.isSymbolicLink()) { const linkTarget = fs.readlinkSync(globalModsPath); // Normalize paths for comparison if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) { @@ -319,7 +325,7 @@ async function syncModsForCurrentProfile() { fs.unlinkSync(globalModsPath); needsLink = true; } - } else if (stats.isDirectory()) { + } else if (globalStats.isDirectory()) { // MIGRATION: It's a real directory. Move contents to profile. console.log('[ModManager] Migrating global mods folder to profile folder...'); const files = fs.readdirSync(globalModsPath); @@ -349,7 +355,20 @@ async function syncModsForCurrentProfile() { // Remove the directory so we can link it try { - fs.rmSync(globalModsPath, { recursive: true, force: true }); + let retries = 3; + while (retries > 0) { + try { + fs.rmSync(globalModsPath, { recursive: true, force: true }); + break; + } catch (err) { + if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) { + retries--; + await new Promise(resolve => setTimeout(resolve, 500)); + } else { + throw err; + } + } + } needsLink = true; } catch (e) { console.error('Failed to remove global mods dir:', e); @@ -364,8 +383,8 @@ async function syncModsForCurrentProfile() { if (needsLink) { console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`); try { - // 'junction' is key for Windows without admin - fs.symlinkSync(profileModsPath, globalModsPath, 'junction'); + const symlinkType = getOS() === 'windows' ? 'junction' : 'dir'; + fs.symlinkSync(profileModsPath, globalModsPath, symlinkType); } catch (err) { // If we can't create the symlink, try creating the directory first console.error('[ModManager] Failed to create symlink. Falling back to direct folder mode.'); From b99b22e8bfd9bcf7388cd0b726b0c899538f7905 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 09:23:15 +0800 Subject: [PATCH 02/88] fix: missing pacman builds --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04eabb7..d643760 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,6 +35,7 @@ jobs: dist/*.AppImage.blockmap dist/*.deb dist/*.rpm + dist/*.pacman dist/*.pkg.tar.zst dist/latest-linux.yml From 17e15c17f049711bcd3f6d0eb42ceba976003e6a Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 09:34:16 +0800 Subject: [PATCH 03/88] prepare release for 2.1.1 minor fix for EPERM error permission --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b1fcf7..5937ba6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hytale-f2p-launcher", - "version": "2.1.0", + "version": "2.1.1", "description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support", "homepage": "https://github.com/amiayweb/Hytale-F2P", "main": "main.js", From 653d4429ed5e58d09fea8bcc3dd27e2c4fefe2d1 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 09:36:03 +0800 Subject: [PATCH 04/88] prepare release 2.1.1 minor fix EPERM permission error --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 12d6ac3..3a3df94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hytale-f2p-launcher", - "version": "2.1.0", + "version": "2.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hytale-f2p-launcher", - "version": "2.1.0", + "version": "2.1.1", "license": "MIT", "dependencies": { "adm-zip": "^0.5.10", From b668bdb45a7a735aaa27d6f919d4cc651cc56799 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 09:48:26 +0800 Subject: [PATCH 05/88] prepare release 2.1.1 --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3a7bcb2..1839f1a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@
-

🎮 Hytale F2P Launcher | Cross-Platform Multiplayer 🖥️

+

🎮 Hytale F2P Launcher 🚀

+

💻 Cross-Platform Multiplayer 🖥️

Available for Windows 🪟, macOS 🍎, and Linux 🐧

An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)

-![Version](https://img.shields.io/badge/Version-2.1.0-green?style=for-the-badge) +![Version](https://img.shields.io/badge/Version-2.1.1-green?style=for-the-badge) ![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-orange?style=for-the-badge) ![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge) @@ -17,10 +18,10 @@ ### ⚠️ **READ [QUICK START](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-quick-start) before Downloading & Installing the Launcher!** ⚠️ -🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑 +#### 🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑

- If you like the project, feel free to support us via Buy Me a Coffee! + 👍 If you like the project, feel free to support us via Buy Me a Coffee!
Any support is appreciated and helps keep the project going.

From 375b422c732aa635832c93354994f19141836cdf Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 11:33:00 +0800 Subject: [PATCH 06/88] Update README.md Windows Prequisites for ARM64 builds --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1839f1a..ac43863 100644 --- a/README.md +++ b/README.md @@ -161,9 +161,15 @@ ### 🪟 Windows Prequisites -* **Java JDK 25:** Download via [Adoptium](https://adoptium.net/temurin/releases/?version=25) or [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows) -* **Latest Visual Studio Redist:** Download via [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe) or [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/) -* **ENABLE MULTIPLAYER:** // TODO MULTIPLAYER GUIDE; FIREWALL GUIDE AND SUCH +* ** +* **Java JDK 25:** + * [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows), **no** support for Windows ARM64 in both version 25 and 21. + * [Adoptium](https://adoptium.net/temurin/releases/?version=25), has Windows ARM64 support in version 21 only. + * [Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download), has Windows ARM64 support in version 25. + * Download from any vendor if your OS is not Windows with ARM64 architecture. +* **Latest Visual Studio Redist:** + * Download via [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe) + * Or [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/) ### 🐧 Linux Prequisites From 20faf36b372317da8d58c8888603b626d907266b Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 12:01:46 +0800 Subject: [PATCH 07/88] fix: remove broken symlink after detected --- backend/core/paths.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/core/paths.js b/backend/core/paths.js index b6aa309..147bc46 100644 --- a/backend/core/paths.js +++ b/backend/core/paths.js @@ -187,9 +187,10 @@ async function getModsPath(customInstallPath = null) { } catch (e) { /* ignore */ } if (!isBrokenLink) { - // Ensure the Mods directory exists - fs.mkdirSync(modsPath, { recursive: true }); + fs.unlinkSync(modsPath); // Remove broken symlink } + // Ensure the Mods directory exists + fs.mkdirSync(modsPath, { recursive: true }); } if (!fs.existsSync(disabledModsPath)) { fs.mkdirSync(disabledModsPath, { recursive: true }); From 94d4586b97ae774190e50255a706dbc75278d133 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 12:09:48 +0800 Subject: [PATCH 08/88] fix: add pathexists for paths.js to check symlink --- backend/core/paths.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/backend/core/paths.js b/backend/core/paths.js index 147bc46..bd381b4 100644 --- a/backend/core/paths.js +++ b/backend/core/paths.js @@ -181,16 +181,26 @@ async function getModsPath(customInstallPath = null) { if (!fs.existsSync(modsPath)) { // Check for broken symlink to avoid EEXIST/EPERM on mkdir let isBrokenLink = false; + let pathExists = false; try { - const stats = fs.lstatSync(modsPath); - if (stats.isSymbolicLink()) isBrokenLink = true; - } catch (e) { /* ignore */ } + const stats = fs.lstatSync(modsPath); + pathExists = true; + if (stats.isSymbolicLink()) { + // Check if target exists + try { + fs.statSync(modsPath); + } catch { + isBrokenLink = true; + } + } + } catch (e) { /* path doesn't exist at all */ } if (!isBrokenLink) { fs.unlinkSync(modsPath); // Remove broken symlink } - // Ensure the Mods directory exists - fs.mkdirSync(modsPath, { recursive: true }); + if (!pathExists || isBrokenLink) { + fs.mkdirSync(modsPath, { recursive: true }); + } } if (!fs.existsSync(disabledModsPath)) { fs.mkdirSync(disabledModsPath, { recursive: true }); From eff6fcd520048d352bac70f181dc6b383567b2fc Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 12:24:24 +0800 Subject: [PATCH 09/88] fix: isbrokenlink should be true to remove the symlink --- backend/core/paths.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/core/paths.js b/backend/core/paths.js index bd381b4..78a5289 100644 --- a/backend/core/paths.js +++ b/backend/core/paths.js @@ -195,7 +195,7 @@ async function getModsPath(customInstallPath = null) { } } catch (e) { /* path doesn't exist at all */ } - if (!isBrokenLink) { + if (isBrokenLink) { fs.unlinkSync(modsPath); // Remove broken symlink } if (!pathExists || isBrokenLink) { From aed00cd0673da1f8b414733d4176d00bdf44f31c Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 13:52:18 +0800 Subject: [PATCH 10/88] add arch package .pkg.tar.zst for release --- .github/workflows/release.yml | 48 ++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d643760..1e2845b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,51 @@ on: workflow_dispatch: jobs: + build-arch: + runs-on: ubuntu-latest + + container: + image: archlinux:latest + + steps: + - name: Install base packages + run: | + pacman -Syu --noconfirm + pacman -S --noconfirm \ + base-devel \ + git \ + nodejs \ + npm \ + rpm-tools \ + libxcrypt-compat + + - name: Create build user + run: | + useradd -m builder + echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fix permissions + run: chown -R builder:builder . + + - name: Build Arch Package + run: | + sudo -u builder bash << 'EOF' + set -e + makepkg -s --noconfirm + EOF + - uses: actions/upload-artifact@v4 + with: + name: arch-package + path: | + *.pkg.tar.zst + *.src.tar.zst + .SRCINFO + build-linux: runs-on: ubuntu-latest steps: @@ -36,7 +81,6 @@ jobs: dist/*.deb dist/*.rpm dist/*.pacman - dist/*.pkg.tar.zst dist/latest-linux.yml build-windows: @@ -115,6 +159,8 @@ jobs: # If it's the 'release' branch, use 'v2.0.2-beta.r42' # name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}-beta.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }} files: | + artifacts/arch-package/*.pkg.tar.zst + artifacts/arch-package/*.src.tar.zst artifacts/linux-builds/**/* artifacts/windows-builds/**/* artifacts/macos-builds/**/* From b39877f5612a2bbd8cecda3b107cc79f02d5b456 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 17:46:40 +0800 Subject: [PATCH 11/88] fix: release workflow for build-arch and build-linux * build-arch job now only build arch .pkg.tar.zst package instead of the whole generic linux. * build-linux job now exclude .pacman package since its deprecated and should not be used. --- .github/workflows/release.yml | 154 +++++++++++++++++----------------- 1 file changed, 78 insertions(+), 76 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e2845b..ee466e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,80 +9,6 @@ on: workflow_dispatch: jobs: - build-arch: - runs-on: ubuntu-latest - - container: - image: archlinux:latest - - steps: - - name: Install base packages - run: | - pacman -Syu --noconfirm - pacman -S --noconfirm \ - base-devel \ - git \ - nodejs \ - npm \ - rpm-tools \ - libxcrypt-compat - - - name: Create build user - run: | - useradd -m builder - echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers - - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Fix permissions - run: chown -R builder:builder . - - - name: Build Arch Package - run: | - sudo -u builder bash << 'EOF' - set -e - makepkg -s --noconfirm - EOF - - uses: actions/upload-artifact@v4 - with: - name: arch-package - path: | - *.pkg.tar.zst - *.src.tar.zst - .SRCINFO - - build-linux: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install build dependencies - run: | - sudo apt-get update - sudo apt-get install -y libarchive-tools - - - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - - run: npm ci - - - name: Build Linux Packages - run: | - npx electron-builder --linux --x64 --arm64 --publish never - - uses: actions/upload-artifact@v4 - with: - name: linux-builds - path: | - dist/*.AppImage - dist/*.AppImage.blockmap - dist/*.deb - dist/*.rpm - dist/*.pacman - dist/latest-linux.yml - build-windows: runs-on: windows-latest steps: @@ -123,8 +49,82 @@ jobs: dist/*.zip dist/latest-mac.yml + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y libarchive-tools + + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + - run: npm ci + + - name: Build Linux Packages + run: | + npx electron-builder --linux AppImage deb rpm --x64 --arm64 --publish never + - uses: actions/upload-artifact@v4 + with: + name: linux-builds + path: | + dist/*.AppImage + dist/*.AppImage.blockmap + dist/*.deb + dist/*.rpm + dist/latest-linux.yml + + build-arch: + runs-on: ubuntu-latest + container: + image: archlinux:latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install base packages + run: | + pacman -Syu --noconfirm + pacman -S --noconfirm \ + base-devel \ + git \ + nodejs \ + npm \ + rpm-tools \ + libxcrypt-compat + + - name: Create build user + run: | + useradd -m builder + echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + + - name: Fix Permissions + run: chown -R builder:builder . + + - name: Build Arch Package + run: | + sudo -u builder bash << 'EOF' + set -e + makepkg --printsrcinfo > .SRCINFO + makepkg -s --noconfirm + EOF + + - name: Upload Arch Package + uses: actions/upload-artifact@v4 + with: + name: arch-package + path: | + *.pkg.tar.zst + *.src.tar.zst + .SRCINFO + release: - needs: [build-linux, build-windows, build-macos] + needs: [build-windows, build-macos, build-linux, build-arch] runs-on: ubuntu-latest if: | startsWith(github.ref, 'refs/tags/v') || @@ -154,13 +154,15 @@ jobs: - name: Create Release uses: softprops/action-gh-release@v2 with: + tag_name: ${{ github.ref_name }} # If it's a tag, use the tag. - tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }} + # tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }} # If it's the 'release' branch, use 'v2.0.2-beta.r42' # name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}-beta.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }} files: | artifacts/arch-package/*.pkg.tar.zst artifacts/arch-package/*.src.tar.zst + artifacts/arch-package/.SRCINFO artifacts/linux-builds/**/* artifacts/windows-builds/**/* artifacts/macos-builds/**/* From 131de1dcd7210aa58c31c1940e25528e68aa0981 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 17:56:44 +0800 Subject: [PATCH 12/88] fix: removes pacman build as it replaced by tar.zst and adds build:arch shortcut for pkgbuild --- package.json | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 5937ba6..83a319b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,11 @@ "build:win": "electron-builder --win", "build:linux": "electron-builder --linux", "build:mac": "electron-builder --mac", - "build:all": "electron-builder --win --linux --mac" + "build:all": "electron-builder --win --linux --mac", + "build:arch": "electron-builder --linux dir", + "build:appimage": "electron-builder --linux AppImage --publish never", + "build:deb": "electron-builder --linux deb --publish never", + "build:rpm": "electron-builder --linux rpm --publish never" }, "keywords": [ "hytale", @@ -82,7 +86,7 @@ ] } ], - "icon": "icon.ico" + "icon": "build/icon.ico" }, "linux": { "target": [ @@ -106,13 +110,6 @@ "x64", "arm64" ] - }, - { - "target": "pacman", - "arch": [ - "x64", - "arm64" - ] } ], "icon": "build/icon.png", From 78f76afe0a2e0d6525f373c627768852b41c61ab Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 18:20:37 +0800 Subject: [PATCH 13/88] aur: add proper VCS (-git) PKGBUILD created clean VCS-based PKGBUILD following arch packaging conventions. this explicitly marked as a rolling (-git) build and derives its version dynamically from git tags and commit history via pkgver(). previous hybrid approach has been changed. key changes: - use -git suffix to clearly indicate rolling source builds - set pkgver=0 and compute the actual version via pkgver() - build only a directory layout using electron-builder (--dir) - avoid generating AppImage, deb, rpm, or pacman installers - align build and package steps with Arch packaging guidelines note: this PKGBUILD is intended for development and AUR use only and is not suitable for binary redistribution or release artifacts. --- PKGBUILD | 33 --------------------------------- PKGBUILD-git | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 33 deletions(-) delete mode 100644 PKGBUILD create mode 100644 PKGBUILD-git diff --git a/PKGBUILD b/PKGBUILD deleted file mode 100644 index 12f6707..0000000 --- a/PKGBUILD +++ /dev/null @@ -1,33 +0,0 @@ -# Maintainer: Terromur -pkgname=Hytale-F2P-git -_pkgname=Hytale-F2P -pkgver=2.0.12.r150.gb62ffc1 -pkgrel=1 -pkgdesc="Hytale-F2P - unofficial Hytale Launcher for free to play with multiplayer support" -arch=('x86_64') -url="https://github.com/amiayweb/Hytale-F2P" -license=('custom') -makedepends=('npm' 'git' 'rpm-tools' 'libxcrypt-compat') -source=("git+$url.git" "Hytale-F2P.desktop") -sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30') - -pkgver() { - cd "$_pkgname" - version=$(git describe --abbrev=0 --tags --match "v[0-9]*") - commits=$(git rev-list --count HEAD) - hash=$(git rev-parse --short HEAD) - printf "%s.r%s.g%s" "${version#v}" "$commits" "$hash" -} - -build() { - cd "$_pkgname" - npm ci - npm run build:linux -} - -package() { - mkdir -p "$pkgdir/opt/$_pkgname" - cp -r "$_pkgname/dist/linux-unpacked/"* "$pkgdir/opt/$_pkgname" - install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop" - install -Dm644 "$_pkgname/GUI/icon.png" "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png" -} diff --git a/PKGBUILD-git b/PKGBUILD-git new file mode 100644 index 0000000..26da67a --- /dev/null +++ b/PKGBUILD-git @@ -0,0 +1,33 @@ +# Maintainer: Terromur +pkgname=Hytale-F2P-git +_pkgname=Hytale-F2P +pkgver=0 +pkgrel=1 +pkgdesc="Hytale-F2P - Unofficial Hytale Launcher for free to play with multiplayer support (rolling git build)" +arch=('x86_64') +url="https://github.com/amiayweb/Hytale-F2P" +license=('custom') +depends=('gtk3' 'nss' 'libxcrypt-compat') +makedepends=('git' 'npm') +source=("git+$url.git" "$_pkgname.desktop") +sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30') + +pkgver() { + cd "$srcdir/$_pkgname" + git describe --tags --long | sed 's/^v//;s/-/.r/;s/-/./' +} + +build() { + cd "$srcdir/$_pkgname" + npm ci + npm run build:arch +} + +package() { + cd "$srcdir/$_pkgname" + install -d "$pkgdir/opt/$_pkgname" + cp -r "$_pkgname/dist/linux-unpacked/"* "$pkgdir/opt/$_pkgname" + + install -Dm644 "$srcdir/$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop" + install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png" +} From e4266906328574660ed65d9aa9b3e0330fc92ede Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 18:33:07 +0800 Subject: [PATCH 14/88] ci: add fixed-version PKGBUILD for Arch Linux releases this PKGBUILD intended for CI and GitHub release artifacts. targets tagged releases only and uses a fixed pkgver that matches the corresponding git tag. all of the VCS logic has been removed to PKGBUILD-git to ensure reproducible builds and stable versioning suitable for binary distribution. the build process relies on electron-builder directory output (--dir) and packages only the unpacked application into a standard Arch Linux package (.pkg.tar.zst). other distro format are excluded from this path and handled separately. this change establishes a clear separation between: - rolling AUR development builds (-git) - CI-generated, versioned Arch Linux release packages the result is predictable artifact naming, correct version alignment, and Arch-compliant packaging for downstream users. --- PKGBUILD | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 PKGBUILD diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..5eea283 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,27 @@ +# Maintainer: Fazri Gading +# This PKGBUILD is for Github Releases +pkgname=Hytale-F2P +pkgver=2.1.1 +pkgrel=1 +pkgdesc="Hytale-F2P - unofficial Hytale Launcher for free to play with multiplayer support" +arch=('x86_64') +url="https://github.com/amiayweb/Hytale-F2P" +license=('custom') +depends=('gtk3' 'nss' 'libxcrypt-compat') +makedepends=('npm') +source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop") +sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30') + +build() { + cd "$_pkgname-$pkgver" + npm ci + npm run build:arch +} + +package() { + cd "$_pkgname-$pkgver" + install -d "$pkgdir/opt/$_pkgname" + cp -r dist/linux-unpacked/* "$pkgdir/opt/$_pkgname" + install -Dm644 "$srcdir/$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop" + install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png" +} From 75a450c9ecd8057442895627ffecbe01bc4765c2 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 18:54:53 +0800 Subject: [PATCH 15/88] Update README.md adds information for Arch build --- README.md | 58 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ac43863..b172ce6 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Windows 10/11 (64-bit; X64/ARM64) | Linux (x64/ARM64) | macOS (Apple Silicon only)
- ⚠️ Note: macOS Intel (x86) is not yet supported 1 + ⚠️ Note: macOS Intel (x86) is not yet supported 1 @@ -131,7 +131,7 @@ 🧠 RAM - 8GB (Dedicated) / 12GB (iGPU) + 8GB (dGPU)2 /
12GB (iGPU)3 16 GB 32 GB @@ -156,7 +156,9 @@
-

1 Hytale did not provide game files for macOS Intel, yet.

+

Note 1 Hytale did not provide game files for macOS Intel, yet.

+

Note 2 Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum.

+

Note 3 Using Integrated GPU (dGPU) must have 12 GB RAM minimum.

@@ -209,24 +211,36 @@ 3. **Permissions & Execution:** * **AppImage:** Make the file executable and run it: ```bash - chmod +x Hytale-F2P-Launcher.AppImage - ./Hytale-F2P-Launcher.AppImage + chmod +x hytale-f2p-launcher.AppImage + ./hytale-f2p-launcher.AppImage ``` - * **Fedora (dnf):** Install the RPM: + * **Ubuntu/Debian-based or Fedora/RHEL-based:** Install the DEB/RPM: ```bash - sudo dnf install ./Hytale-F2P-Launcher.rpm - ``` - * **Debian/Ubuntu (apt):** Install the DEB: - ```bash - sudo apt install ./Hytale-F2P-Launcher.deb + # Fedora/RHEL-based + sudo dnf install ./hytale-f2p-launcher.rpm + # Debian/Ubuntu + sudo apt install ./hytale-f2p-launcher.deb ``` * **Arch Linux (pacman):** Install the package using: ```bash - sudo pacman -U /path/to/Hytale-F2P-Launcher.pkg.tar.zst + # Stable Build + sudo pacman -U hytale-f2p-launcher.pkg.tar.zst + # Development Build + yay -S hytale-f2p-git # or + paru -S hytale-f2p-git + # Manual Build + git clone https://aur.archlinux.org/hytale-f2p-git.git + cd hytale-f2p-git + makepkg -si ``` + +> [!NOTE] +> Make sure to adjust the filename correctly with the version and the architecture type. + 4. **Troubleshooting:** * **FUSE:** If the AppImage fails to launch on newer distributions, ensure `libfuse2` (or `fuse2` on Arch/Fedora) is installed. * **Desktop Entry:** After installing via `.rpm`, `.deb`, or `.pkg.tar.zst`, the launcher should automatically appear in your App Library/Grid. + * Missing libxcrypt.so.1: Install `libxcrypt-compat` using your package manager --- @@ -292,20 +306,20 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions. ## 📋 Changelog -### 🆕 v2.1.0 - -- 🚨 **Auto-Retry Downloads and Auto-Patch Files** — -- ⚡ **Hardware Acceleration** — -- 👨‍💻 **In-App Logging** — -- 🛠️ **Repair Button** — Y -- 🔎 **Browse CurseForge Mods** — Browsing mods now easier with our dedicated CurseForge API Key. -- 🌎 **Fixes and Release New Translation** — Fixed 🇪🇸 🇧🇷 and added more translation for current build. Turkish 🇹🇷 language now added. - - +### 🆕 v2.1.1 +- 🛠️ **Fix EPERM** Issue +- 🅰️ **Adds Better Arch Build** +-
Click here to see older Changelogs +### 🆕 v2.1.0 +- 🚨 **Auto-Retry Downloads and Auto-Patch Files** — +- ⚡ **Hardware Acceleration** — +- 🔎 **Browse CurseForge Mods** — Browsing mods now easier with our dedicated CurseForge API Key. +- 🌎 **Fixes and Release New Translation** — Fixed 🇪🇸 🇧🇷 and added more translation for current build. Turkish 🇹🇷 language now added. + ### 🆕 v2.0.2b *(Minor Update: Performance & Utilities)* - 🌎 **Language Translation** — A big welcome for Spanish 🇪🇸 and Portuguese (Brazil) 🇧🇷 players! **Language setting can be found in the bottom part of Settings pane.** - 💻 **Laptop/Hybrid GPU Performance Issue Fix** — Added automatic GPU detection system and options to choose which GPU will be used for the game, *specifically for Linux users*. From 081ac926e36aa490a51afe9b220bb46dbb45fae8 Mon Sep 17 00:00:00 2001 From: TalesAmaral <57869141+TalesAmaral@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:49:39 -0300 Subject: [PATCH 16/88] Update README.md BUILD.md location was changed and now this link is poiting to nothing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac43863..0b2036c 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,7 @@ The `.zip` version is useful for users who prefer a portable installation or nee ## 🛠️ Building from Source -See [BUILD.md](BUILD.md) for comprehensive build instructions. +See [BUILD.md](docs/BUILD.md) for comprehensive build instructions. --- From cc1c6c334c5ce0f01200a75b6483ce728c000303 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Tue, 27 Jan 2026 00:14:53 +0800 Subject: [PATCH 17/88] Update PKGBUILD --- PKGBUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/PKGBUILD b/PKGBUILD index 5eea283..1bcb516 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,3 +1,4 @@ +# Maintainer: Terromur # Maintainer: Fazri Gading # This PKGBUILD is for Github Releases pkgname=Hytale-F2P From 782845463128bfb7c76b3e0b11523a6bb3f3ff04 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Tue, 27 Jan 2026 00:15:25 +0800 Subject: [PATCH 18/88] Update PKGBUILD-git --- PKGBUILD-git | 1 + 1 file changed, 1 insertion(+) diff --git a/PKGBUILD-git b/PKGBUILD-git index 26da67a..d3e690d 100644 --- a/PKGBUILD-git +++ b/PKGBUILD-git @@ -1,4 +1,5 @@ # Maintainer: Terromur +# Maintainer: Fazri Gading pkgname=Hytale-F2P-git _pkgname=Hytale-F2P pkgver=0 From f4d966ee65b2bbd96a94dc0d107a52b6ff54cdb5 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Tue, 27 Jan 2026 02:16:01 +0800 Subject: [PATCH 19/88] chore: fix ubuntu/debian part in README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c85885c..b35c9e4 100644 --- a/README.md +++ b/README.md @@ -217,9 +217,10 @@ * **Ubuntu/Debian-based or Fedora/RHEL-based:** Install the DEB/RPM: ```bash # Fedora/RHEL-based - sudo dnf install ./hytale-f2p-launcher.rpm + sudo dnf install hytale-f2p-launcher.rpm # Debian/Ubuntu - sudo apt install ./hytale-f2p-launcher.deb + sudo apt install -y libasound2 libpng16-16 libpng-dev libicu76 + sudo dpkg -i hytale-f2p-launcher.deb ``` * **Arch Linux (pacman):** Install the package using: ```bash @@ -235,7 +236,7 @@ ``` > [!NOTE] -> Make sure to adjust the filename correctly with the version and the architecture type. +> Make sure to adjust the filename correctly with the version and the architecture type. TIP: Use `cd` command to the package location. 4. **Troubleshooting:** * **FUSE:** If the AppImage fails to launch on newer distributions, ensure `libfuse2` (or `fuse2` on Arch/Fedora) is installed. From e7023dcf95a57487afc7874591619f2277e7a9e1 Mon Sep 17 00:00:00 2001 From: walti0 <95646872+walti0@users.noreply.github.com> Date: Mon, 26 Jan 2026 20:06:16 +0100 Subject: [PATCH 20/88] Polish language support (#195) --- GUI/js/i18n.js | 3 +- GUI/locales/pl-PL.json | 234 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 GUI/locales/pl-PL.json diff --git a/GUI/js/i18n.js b/GUI/js/i18n.js index faf6847..b2ec9e0 100644 --- a/GUI/js/i18n.js +++ b/GUI/js/i18n.js @@ -6,7 +6,8 @@ const i18n = (() => { { code: 'en', name: 'English' }, { code: 'es-ES', name: 'Español (España)' }, { code: 'pt-BR', name: 'Portuguese (Brazil)' }, - { code: 'tr-TR', name: 'Turkish (Turkey)' } + { code: 'tr-TR', name: 'Turkish (Turkey)' }, + { code: 'pl-PL', name: 'Polish (Poland)' } ]; // Load single language file diff --git a/GUI/locales/pl-PL.json b/GUI/locales/pl-PL.json new file mode 100644 index 0000000..7d03edf --- /dev/null +++ b/GUI/locales/pl-PL.json @@ -0,0 +1,234 @@ +{ + "nav": { + "play": "Graj", + "mods": "Mody", + "news": "Wiadomości", + "chat": "Chat z graczami", + "settings": "Ustawienia", + "skins": "Skiny" + }, + "header": { + "playersLabel": "Graczy:", + "manageProfiles": "Zarządzaj Profilami", + "defaultProfile": "Domyślny", + "f2p": "FREE TO PLAY" + }, + "install": { + "title": "FREE TO PLAY LAUNCHER", + "playerName": "Nazwa Gracza", + "playerNamePlaceholder": "Wprowadź Nazwę", + "customInstallation": "Dostosuj Instalacje", + "installationFolder": "Folder docelowy", + "pathPlaceholder": "Domyślna lokalizacja", + "browse": "Przeglądaj", + "installButton": "ZAINSTALUJ HYTALE", + "installing": "INSTALOWANIE..." + }, + "play": { + "ready": "GOTOWE", + "subtitle": "Uruchom Hytale i rozpocznij przygodę", + "playButton": "GRAJ W HYTALE", + "latestNews": "NAJNOWSZE WIADOMOŚCI", + "viewAll": "ZOBACZ CAŁOŚĆ", + "checking": "SPRAWDZANIE...", + "play": "GRAJ" + }, + "mods": { + "searchPlaceholder": "Wyszukaj mody...", + "myMods": "MOJE MODY", + "previous": "POPRZEDNIA", + "next": "NASTĘPNA", + "page": "Strona", + "of": "z", + "modalTitle": "MOJE MODY", + "noModsFound": "Nie Znaleziono Modów", + "noModsFoundDesc": "Spróbuj dostosować wyszukiwanie", + "noModsInstalled": "Brak Zainstalowanych Modów", + "noModsInstalledDesc": "Dodaj mody z CurseForge lub zaimportuj lokalne pliki", + "view": "WIDOK", + "install": "ZAINSTALUJ", + "installed": "ZAINSTALOWANE", + "enable": "WŁĄCZ", + "disable": "WYŁĄCZ", + "active": "AKTYWNE", + "disabled": "WYŁĄCZONE", + "delete": "Usuń mod", + "noDescription": "Brak opisu", + "confirmDelete": "Czy na pewno chcesz usunąć \"{name}\"?", + "confirmDeleteDesc": "Tej czynności nie można cofnąć.", + "confirmDeletion": "Potwierdź" + }, + "news": { + "title": "WSZYSTKIE WIADOMOŚCI", + "readMore": "Zobacz Więcej" + }, + "chat": { + "title": "Chat z graczami", + "pickColor": "Kolor", + "inputPlaceholder": "Wprowadź swoją wiadomość...", + "send": "Wyślij", + "online": "online", + "charCounter": "{current}/{max}", + "secureChat": "Bezpieczny czat – Linki są ocenzurowane", + "joinChat": "Dołącz do Czatu", + "chooseUsername": "Wybierz nazwę użytkownika, aby dołączyć do Czatu z graczami", + "username": "Nazwa Gracza", + "usernamePlaceholder": "Wprowadź swoją nazwę...", + "usernameHint": "Między 3-20 znaków, tylko litery, cyfry i znaki - i _", + "joinButton": "Dołącz do Czatu", + "colorModal": { + "title": "Dostosuj Kolor Użytkownika", + "chooseSolid": "Wybierz jednolity kolor:", + "customColor": "Kolor niestandardowy:", + "preview": "Podgląd:", + "previewUsername": "Nazwa", + "apply": "Zastosuj Kolor" + } + }, + "settings": { + "title": "USTAWIENIA", + "java": "Środowisko Java", + "useCustomJava": "Użyj niestandardowej ścieżki Java", + "javaDescription": "Zastąp dołączone środowisko wykonawcze Java własnym", + "javaPath": "Ścieżka Wykonywalna Java", + "javaPathPlaceholder": "Wybierz ścieżkę Java...", + "javaBrowse": "Przeglądaj", + "javaHint": "Wybierz folder instalacyjny Java (obsługiwane Windows, Mac, Linux)", + "discord": "Integracja z Discordem", + "enableRPC": "Włącz Discord Rich Presence", + "discordDescription": "Pokaż swoją aktywność na Discordzie", + "game": "Opcje gry", + "playerName": "Nazwa Gracza", + "playerNamePlaceholder": "Wprowadź swoją nazwę", + "playerNameHint": "Ta nazwa będzie używana w grze (1-16 znaków)", + "openGameLocation": "Otwórz Lokalizację Gry", + "openGameLocationDesc": "Otwórz folder instalacyjny gry", + "account": "Zarządzanie identyfikatorami UUID gracza", + "currentUUID": "Obecny UUID", + "uuidPlaceholder": "Ładowanie UUID...", + "copyUUID": "Skopiuj UUID", + "regenerateUUID": "Generuj UUID", + "uuidHint": "Twój unikalny identyfikator gracza dla tej nazwy użytkownika", + "manageUUIDs": "Zarządzaj wszystkimi UUID", + "manageUUIDsDesc": "Wyświetl i zarządzaj wszystkimi identyfikatorami UUID graczy", + "language": "Język", + "selectLanguage": "Wybierz Język", + "repairGame": "Napraw Grę", + "reinstallGame": "Zainstaluj ponownie pliki gry (zachowuje dane)", + "gpuPreference": "Preferencje GPU", + "gpuHint": "Wybierz preferowany procesor graficzny (Linux: wpływa na DRI_PRIME)", + "gpuAuto": "Auto", + "gpuIntegrated": "Zintegrowana", + "gpuDedicated": "Dedykowana", + "logs": "SYSTEM LOGS", + "logsCopy": "Kopiuj", + "logsRefresh": "Odśwież", + "logsFolder": "Otwórz Folder", + "logsLoading": "Ładowanie logów..." + }, + "uuid": { + "modalTitle": "Zarządzanie UUID", + "currentUserUUID": "Aktualny UUID użytkownika", + "allPlayerUUIDs": "Wszystkie identyfikatory UUID graczy", + "generateNew": "Wygeneruj nowy UUID", + "loadingUUIDs": "Ładowanie UUID...", + "setCustomUUID": "Ustaw niestandardowy UUID", + "customPlaceholder": "Wprowadź niestandardowy UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)", + "setUUID": "Ustaw UUID", + "warning": "Ostrzeżenie: Ustawienie niestandardowego identyfikatora UUID spowoduje zmianę Twojego obecnego identyfikatora gracza", + "copyTooltip": "Kopiuj UUID", + "regenerateTooltip": "Wygeneruj nowy UUID" + }, + "profiles": { + "modalTitle": "Zarządzaj Profilami", + "newProfilePlaceholder": "Nowa Nazwa Profilu", + "createProfile": "Utwórz Profil" + }, + "discord": { + "notificationText": "Dołącz do naszej społeczności Discord!", + "joinButton": "Dołącz Discord" + }, + "skins": { + "title": "Skiny", + "comingSoon": "Personalizacja skórek już wkrótce..." + }, + "common": { + "confirm": "Potwierdź", + "cancel": "Anuluj", + "save": "Zapisz", + "close": "Zamknij", + "delete": "Usuń", + "edit": "Edytuj", + "loading": "Ładowanie...", + "apply": "Zastosuj" + }, + "notifications": { + "gameDataNotFound": "Błąd: Nie znaleziono danych gry", + "gameUpdatedSuccess": "Gra została zaktualizowana pomyślnie! 🎉", + "updateFailed": "Aktualizacja nie powiodła się: {error}", + "updateError": "Błąd aktualizacji: {error}", + "discordEnabled": "Discord Rich Presence włączony", + "discordDisabled": "Discord Rich Presence wyłączony", + "discordSaveFailed": "Nie udało się zapisać ustawień Discorda", + "playerNameRequired": "Proszę podać prawidłową nazwę gracza", + "playerNameSaved": "Nazwa gracza została zapisana pomyślnie", + "playerNameSaveFailed": "Nie udało się zapisać nazwy gracza", + "uuidCopied": "Identyfikator UUID skopiowany do schowka!", + "uuidCopyFailed": "Nie udało się skopiować UUID", + "uuidRegenNotAvailable": "Ponowna gerowanie UUID niedostępne", + "uuidRegenFailed": "Nie udało się ponownie wygenerować UUID", + "uuidGenerated": "Nowy UUID został pomyślnie wygenerowany!", + "uuidGeneratedShort": "Wygenerowano nowy UUID!", + "uuidGenerateFailed": "Nie udało się wygenerować nowego UUID", + "uuidRequired": "Wprowadzić UUID", + "uuidInvalidFormat": "Nieprawidłowy format UUID", + "uuidSetFailed": "Nie udało się ustawić niestandardowego UUID", + "uuidSetSuccess": "Niestandardowy UUID został ustawiony pomyślnie!", + "uuidDeleteFailed": "Nie udało się usunąć UUID", + "uuidDeleteSuccess": "UUID został pomyślnie usunięty!", + "modsDownloading": "Pobieranie {name}...", + "modsTogglingMod": "Przełączanie moda...", + "modsDeletingMod": "Usuwanie moda...", + "modsLoadingMods": "Ładowanie modów z CurseForge...", + "modsInstalledSuccess": "{name} zainstalowany pomyślnie! 🎉", + "modsDeletedSuccess": "{name} usunięto pomyślnie", + "modsDownloadFailed": "Nie udało się pobrać moda: {error}", + "modsToggleFailed": "Nie udało się przełączyć moda: {error}", + "modsDeleteFailed": "Nie udało się usunąć moda: {error}", + "modsModNotFound": "Nie znaleziono informacji o modzie" + }, + "confirm": { + "defaultTitle": "Potwierdź działanie", + "regenerateUuidTitle": "Wygeneruj nowy UUID", + "regenerateUuidMessage": "Czy na pewno chcesz wygenerować nowy UUID? To spowoduje zmianę Twojego identyfikatora gracza.", + "regenerateUuidButton": "Generuj", + "setCustomUuidTitle": "Ustaw niestandardowy UUID", + "setCustomUuidMessage": "Czy na pewno chcesz ustawić ten UUID? To spowoduje zmianę Twojego identyfikatora gracza.", + "setCustomUuidButton": "Ustaw UUID", + "deleteUuidTitle": "Usuń UUID", + "deleteUuidMessage": "Czy na pewno chcesz usunąć UUID dla \"{username}\"? Tej czynności nie można cofnąć.", + "deleteUuidButton": "Usuń", + "uninstallGameTitle": "Odinstaluj grę", + "uninstallGameMessage": "Czy na pewno chcesz odinstalować Hytale? Wszystkie pliki gry zostaną usunięte.", + "uninstallGameButton": "Odinstaluj" + }, + "progress": { + "initializing": "Inicjalizacja...", + "downloading": "Pobieranie...", + "installing": "Instalowanie...", + "extracting": "Ekstraktowanie...", + "verifying": "Weryfikowanie...", + "switchingProfile": "Przełączanie profilu...", + "profileSwitched": "Profil zmieniony!", + "startingGame": "Uruchamianie gry...", + "launching": "URUCHAMIANIE...", + "uninstallingGame": "Odinstalowywanie gry...", + "gameUninstalled": "Gra została pomyślnie odinstalowana!", + "uninstallFailed": "Odinstalowanie nie powiodło się: {error}", + "startingUpdate": "Rozpoczynanie obowiązkowej aktualizacji gry...", + "installationComplete": "Instalacja zakończona pomyślnie!", + "installationFailed": "Instalacja nie powiodła się: {error}", + "installingGameFiles": "Instalowanie plików gry...", + "installComplete": "Instalacja zakończona!" + } +} From 6fa933fece1de0b3ec7f2e8afaf0a45a573f2047 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Tue, 27 Jan 2026 03:19:06 +0800 Subject: [PATCH 21/88] Update support_request.yml Added hardware specification --- .github/ISSUE_TEMPLATE/support_request.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/support_request.yml b/.github/ISSUE_TEMPLATE/support_request.yml index 71d98e1..53dfe8d 100644 --- a/.github/ISSUE_TEMPLATE/support_request.yml +++ b/.github/ISSUE_TEMPLATE/support_request.yml @@ -28,6 +28,15 @@ body: validations: required: true + - type: textarea + id: hardwarespec + attributes: + label: Hardware Specification + description: Tell us your CPU, iGPU, dGPU, VRAM, and RAM information. + placeholder: "CPU: Intel i9-14900K 6.0 GHz | GPU: NVIDIA RTX 4090 | VRAM: 24 GB | RAM: 32 GB" + validations: + required: true + - type: input id: version attributes: @@ -66,4 +75,4 @@ body: id: additional attributes: label: Additional Information - description: Any other information that might help us assist you. \ No newline at end of file + description: Any other information that might help us assist you. From 6b76eb365e16342e33502acafa5469d363ba853a Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Tue, 27 Jan 2026 03:21:47 +0800 Subject: [PATCH 22/88] Update bug_report.yml Add logs textfield to bug report --- .github/ISSUE_TEMPLATE/bug_report.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index dafdc62..c3bb5e6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -71,8 +71,17 @@ body: validations: required: true + - type: textarea + id: logs + attributes: + label: Logs or Error Messages + description: If applicable, paste any error messages or logs here. + render: shell + validations: + required: true + - type: textarea id: additional attributes: label: Additional context - description: Add any other context about the problem here. \ No newline at end of file + description: Add any other context about the problem here. From 639a2ab1b55330f23c3f5623a3c7acd02e8b2cd6 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Tue, 27 Jan 2026 03:38:20 +0800 Subject: [PATCH 23/88] chore: add changelog in README.md --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b35c9e4..7ff99b0 100644 --- a/README.md +++ b/README.md @@ -308,20 +308,21 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions. ## 📋 Changelog ### 🆕 v2.1.1 -- 🛠️ **Fix EPERM** Issue -- 🅰️ **Adds Better Arch Build** -- +- 🛠️ **Fix Bug EPERM**: EPERM or Error Permission in creating/removing process in reinstalling is now fixed. +- 🅰️ **Adds .pkg.tar.zst Build for Arch Users**: This Arch-package has been needed since the first release. +- ❎ **Removes .pacman Build for Arch**: Based on the established conventions within the Arch Linux community, the file extension .pacman should not be used for package files. +- 🌎 **New Translation**: New Polish 🇵🇱 Translation added to the Launcher.
Click here to see older Changelogs -### 🆕 v2.1.0 +### 🔄 v2.1.0 - 🚨 **Auto-Retry Downloads and Auto-Patch Files** — - ⚡ **Hardware Acceleration** — - 🔎 **Browse CurseForge Mods** — Browsing mods now easier with our dedicated CurseForge API Key. - 🌎 **Fixes and Release New Translation** — Fixed 🇪🇸 🇧🇷 and added more translation for current build. Turkish 🇹🇷 language now added. -### 🆕 v2.0.2b *(Minor Update: Performance & Utilities)* +### 🔄 v2.0.2b *(Minor Update: Performance & Utilities)* - 🌎 **Language Translation** — A big welcome for Spanish 🇪🇸 and Portuguese (Brazil) 🇧🇷 players! **Language setting can be found in the bottom part of Settings pane.** - 💻 **Laptop/Hybrid GPU Performance Issue Fix** — Added automatic GPU detection system and options to choose which GPU will be used for the game, *specifically for Linux users*. - 👨‍💻 **In-App Logging** — Reporting bugs and issues to `Github Issues` tab or `Open A Ticket` channel in our Discord Server has been made easier for players, no more finding logs file manually. From 01823729ec9867f5961c8f69bc98ac83bc698842 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Tue, 27 Jan 2026 03:40:22 +0800 Subject: [PATCH 24/88] fix screenshot input in feature_request.yml --- .github/ISSUE_TEMPLATE/feature_request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 6d1d5d4..44c8816 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -39,7 +39,7 @@ body: validations: required: true - - type: screenshots + - type: textarea id: screenshots attributes: label: Screenshots (Optional) @@ -49,4 +49,4 @@ body: id: additional attributes: label: Additional context - description: Add any other context or screenshots about the feature request here. \ No newline at end of file + description: Add any other context or screenshots about the feature request here. From 7d2672b684860e1670f33f8ffd7bf9d32d9e1b93 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Tue, 27 Jan 2026 03:41:26 +0800 Subject: [PATCH 25/88] add hardware spec input in bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c3bb5e6..f138ffc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -55,6 +55,15 @@ body: validations: required: true + - type: textarea + id: hardwarespec + attributes: + label: Hardware Specification + description: Tell us your CPU, iGPU, dGPU, VRAM, and RAM information. + placeholder: "CPU: Intel i9-14900K 6.0 GHz | GPU: NVIDIA RTX 4090 | VRAM: 24 GB | RAM: 32 GB" + validations: + required: true + - type: dropdown id: os attributes: From 3edee4b4eb632f5c87e7d052c9f60aa48ec6747c Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Tue, 27 Jan 2026 03:55:01 +0800 Subject: [PATCH 26/88] fix: PKGBUILD pkgname variable fix --- PKGBUILD | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 1bcb516..d5518ce 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -14,15 +14,15 @@ source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop") sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30') build() { - cd "$_pkgname-$pkgver" + cd "$pkgname-$pkgver" npm ci npm run build:arch } package() { - cd "$_pkgname-$pkgver" - install -d "$pkgdir/opt/$_pkgname" - cp -r dist/linux-unpacked/* "$pkgdir/opt/$_pkgname" - install -Dm644 "$srcdir/$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop" - install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png" + cd "$pkgname-$pkgver" + install -d "$pkgdir/opt/$pkgname" + cp -r dist/linux-unpacked/* "$pkgdir/opt/$pkgname" + install -Dm644 "$srcdir/$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop" + install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$pkgname.png" } From e56b12cd722b561e49974ce6fd1eb3fc74ea1879 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Tue, 27 Jan 2026 01:44:58 +0100 Subject: [PATCH 27/88] userdata migration [need review from other OS] --- backend/core/paths.js | 32 +++--- backend/managers/gameLauncher.js | 5 +- backend/managers/gameManager.js | 123 +++++---------------- backend/utils/userDataBackup.js | 6 +- backend/utils/userDataMigration.js | 172 +++++++++++++++++++++++++++++ main.js | 9 ++ package.json | 2 +- 7 files changed, 234 insertions(+), 115 deletions(-) create mode 100644 backend/utils/userDataMigration.js diff --git a/backend/core/paths.js b/backend/core/paths.js index 78a5289..ff82366 100644 --- a/backend/core/paths.js +++ b/backend/core/paths.js @@ -14,6 +14,21 @@ function getAppDir() { } } +/** + * Get centralized UserData saves directory (NEW in 2.1.2) + * UserData is now stored separately from game installation + */ +function getHytaleSavesDir() { + const home = os.homedir(); + if (process.platform === 'win32') { + return path.join(home, 'AppData', 'Local', 'HytaleSaves'); + } else if (process.platform === 'darwin') { + return path.join(home, 'Library', 'Application Support', 'HytaleSaves'); + } else { + return path.join(home, '.hytalesaves'); + } +} + const DEFAULT_APP_DIR = getAppDir(); function getResolvedAppDir(customPath) { @@ -218,20 +233,8 @@ async function getModsPath(customInstallPath = null) { function getProfilesDir(customInstallPath = null) { try { - // get UserData path - let installPath = customInstallPath; - if (!installPath) { - const configFile = path.join(DEFAULT_APP_DIR, 'config.json'); - if (fs.existsSync(configFile)) { - const config = JSON.parse(fs.readFileSync(configFile, 'utf8')); - installPath = config.installPath || ''; - } - } - if (!installPath) installPath = getAppDir(); - - const branch = loadVersionBranch(); - const gameLatest = path.join(installPath, branch, 'package', 'game', 'latest'); - const userDataPath = findUserDataPath(gameLatest); + // NEW 2.1.2: Use centralized UserData location + const userDataPath = getHytaleSavesDir(); const profilesDir = path.join(userDataPath, 'Profiles'); if (!fs.existsSync(profilesDir)) { @@ -247,6 +250,7 @@ function getProfilesDir(customInstallPath = null) { module.exports = { getAppDir, + getHytaleSavesDir, getResolvedAppDir, expandHome, APP_DIR, diff --git a/backend/managers/gameLauncher.js b/backend/managers/gameLauncher.js index 6a7a379..fddfa7f 100644 --- a/backend/managers/gameLauncher.js +++ b/backend/managers/gameLauncher.js @@ -12,6 +12,7 @@ const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA const { getLatestClientVersion } = require('../services/versionManager'); const { updateGameFiles } = require('./gameManager'); const { syncModsForCurrentProfile } = require('./modManager'); +const { getUserDataPath } = require('../utils/userDataMigration'); // Client patcher for custom auth server (sanasol.ws) let clientPatcher = null; @@ -106,7 +107,9 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr const customAppDir = getResolvedAppDir(installPathOverride); const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest'); const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest'); - const userDataDir = path.join(customGameDir, 'Client', 'UserData'); + + // NEW 2.1.2: Use centralized UserData location + const userDataDir = getUserDataPath(); const gameLatest = customGameDir; let clientPath = findClientPath(gameLatest); diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index 963bbdd..2f164bd 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -9,7 +9,7 @@ const { installButler } = require('./butlerManager'); const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager'); const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config'); const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager'); -const userDataBackup = require('../utils/userDataBackup'); +const { getUserDataPath, migrateUserDataToCentralized } = require('../utils/userDataMigration'); async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) { const osName = getOS(); @@ -308,31 +308,25 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, cacheDir = CACHE_DIR, branchOverride = null) { let tempUpdateDir; - let backupPath = null; const branch = branchOverride || loadVersionBranch(); const installPath = path.dirname(path.dirname(path.dirname(path.dirname(gameDir)))); - // Vérifier si on a version_client et version_branch dans config.json const config = loadConfig(); - const hasVersionConfig = !!(config.version_client && config.version_branch); - const oldBranch = config.version_branch || 'release'; // L'ancienne branche pour le backup - console.log(`[UpdateGameFiles] hasVersionConfig: ${hasVersionConfig}`); + const oldBranch = config.version_branch || 'release'; console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`); try { - if (progressCallback) { - progressCallback('Backing up user data...', 5, null, null, null); - } - - // Backup UserData AVANT de télécharger/installer (critical for same-branch updates) + // NEW 2.1.2: Ensure UserData migration to centralized location try { - console.log(`[UpdateGameFiles] Attempting to backup UserData from old branch: ${oldBranch}`); - backupPath = await userDataBackup.backupUserData(installPath, oldBranch, hasVersionConfig); - if (backupPath) { - console.log(`[UpdateGameFiles] ✓ UserData backed up from ${oldBranch}: ${backupPath}`); + console.log('[UpdateGameFiles] Ensuring UserData migration...'); + const migrationResult = await migrateUserDataToCentralized(); + if (migrationResult.migrated) { + console.log('[UpdateGameFiles] ✓ UserData migrated to centralized location'); + } else if (migrationResult.alreadyMigrated) { + console.log('[UpdateGameFiles] ✓ UserData already in centralized location'); } - } catch (backupError) { - console.warn('[UpdateGameFiles] ✗ UserData backup failed:', backupError.message); + } catch (migrationError) { + console.warn('[UpdateGameFiles] UserData migration warning:', migrationError.message); } if (progressCallback) { @@ -390,31 +384,9 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback); console.log('Logo@2x.png update result after update:', logoResult); - // Ensure UserData directory exists - const userDataDir = path.join(gameDir, 'Client', 'UserData'); - if (!fs.existsSync(userDataDir)) { - console.log(`[UpdateGameFiles] Creating UserData directory at: ${userDataDir}`); - fs.mkdirSync(userDataDir, { recursive: true }); - } - - if (progressCallback) { - progressCallback('Restoring user data...', 90, null, null, null); - } - - // Restore UserData using new system - if (backupPath) { - try { - console.log(`[UpdateGameFiles] Restoring UserData from ${oldBranch} to ${branch}`); - console.log(`[UpdateGameFiles] Source backup: ${backupPath}`); - await userDataBackup.restoreUserData(backupPath, installPath, branch); - await userDataBackup.cleanupBackup(backupPath); - console.log(`[UpdateGameFiles] ✓ UserData migrated successfully from ${oldBranch} to ${branch}`); - } catch (restoreError) { - console.warn('[UpdateGameFiles] ✗ UserData restore failed:', restoreError.message); - } - } else { - console.log('[UpdateGameFiles] No backup to restore, empty UserData folder created'); - } + // NEW 2.1.2: No longer create UserData in game installation + // UserData is now in centralized location (getUserDataPath()) + console.log('[UpdateGameFiles] UserData is managed in centralized location'); console.log(`Game files updated successfully to version: ${newVersion}`); @@ -434,15 +406,6 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, } catch (error) { console.error('Error updating game files:', error); - if (backupPath) { - try { - await userDataBackup.cleanupBackup(backupPath); - console.log('UserData backup cleaned up after error'); - } catch (cleanupError) { - console.warn('Could not clean up UserData backup:', cleanupError.message); - } - } - if (tempUpdateDir && fs.existsSync(tempUpdateDir)) { fs.rmSync(tempUpdateDir, { recursive: true, force: true }); } @@ -470,28 +433,18 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver const customToolsDir = path.join(customAppDir, 'butler'); const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest'); const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest'); - const userDataDir = path.join(customGameDir, 'Client', 'UserData'); - - // Vérifier si on a version_client et version_branch dans config.json - const config = loadConfig(); - const hasVersionConfig = !!(config.version_client && config.version_branch); - console.log(`[InstallGame] Configuration detected - version_client: ${config.version_client}, version_branch: ${config.version_branch}`); - console.log(`[InstallGame] hasVersionConfig: ${hasVersionConfig}`); - - // Backup UserData AVANT l'installation si nécessaire - let backupPath = null; - if (progressCallback) { - progressCallback('Checking for existing UserData...', 5, null, null, null); - } + // NEW 2.1.2: Ensure UserData migration to centralized location try { - console.log(`[InstallGame] Attempting UserData backup (hasVersionConfig: ${hasVersionConfig})...`); - backupPath = await userDataBackup.backupUserData(customAppDir, branch, hasVersionConfig); - if (backupPath) { - console.log(`[InstallGame] ✓ UserData backed up to: ${backupPath}`); + console.log('[InstallGame] Ensuring UserData migration...'); + const migrationResult = await migrateUserDataToCentralized(); + if (migrationResult.migrated) { + console.log('[InstallGame] ✓ UserData migrated to centralized location'); + } else if (migrationResult.alreadyMigrated) { + console.log('[InstallGame] ✓ UserData already in centralized location'); } - } catch (backupError) { - console.warn('[InstallGame] ✗ UserData backup failed:', backupError.message); + } catch (migrationError) { + console.warn('[InstallGame] UserData migration warning:', migrationError.message); } [customAppDir, customCacheDir, customToolsDir].forEach(dir => { @@ -500,10 +453,6 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver } }); - if (!fs.existsSync(userDataDir)) { - fs.mkdirSync(userDataDir, { recursive: true }); - } - saveUsername(playerName); if (installPathOverride) { saveInstallPath(installPathOverride); @@ -595,29 +544,9 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback); console.log('Logo@2x.png update result after installation:', logoResult); - // Ensure UserData directory exists - if (!fs.existsSync(userDataDir)) { - console.log(`[InstallGame] Creating UserData directory at: ${userDataDir}`); - fs.mkdirSync(userDataDir, { recursive: true }); - } - - // Restore UserData from backup if exists - if (backupPath) { - if (progressCallback) { - progressCallback('Restoring UserData...', 95, null, null, null); - } - - try { - console.log(`[InstallGame] Restoring UserData from: ${backupPath}`); - await userDataBackup.restoreUserData(backupPath, customAppDir, branch); - await userDataBackup.cleanupBackup(backupPath); - console.log('[InstallGame] ✓ UserData restored successfully'); - } catch (restoreError) { - console.warn('[InstallGame] ✗ UserData restore failed:', restoreError.message); - } - } else { - console.log('[InstallGame] No backup to restore, empty UserData folder created'); - } + // NEW 2.1.2: No longer create UserData in game installation + // UserData is managed in centralized location (getUserDataPath()) + console.log('[InstallGame] UserData is managed in centralized location'); if (progressCallback) { progressCallback('Installation complete', 100, null, null, null); diff --git a/backend/utils/userDataBackup.js b/backend/utils/userDataBackup.js index 0da8614..9380d1c 100644 --- a/backend/utils/userDataBackup.js +++ b/backend/utils/userDataBackup.js @@ -46,7 +46,8 @@ class UserDataBackup { console.log(`[UserDataBackup] Copying from ${userDataPath} to ${backupPath}...`); await fs.copy(userDataPath, backupPath, { overwrite: true, - errorOnExist: false + errorOnExist: false, + dereference: true // Follow symlinks to avoid EPERM errors on Windows }); console.log('[UserDataBackup] ✓ Backup completed successfully'); return backupPath; @@ -82,7 +83,8 @@ class UserDataBackup { await fs.copy(backupPath, userDataPath, { overwrite: true, - errorOnExist: false + errorOnExist: false, + dereference: true // Follow symlinks to avoid EPERM errors on Windows }); console.log('UserData restore completed successfully'); diff --git a/backend/utils/userDataMigration.js b/backend/utils/userDataMigration.js new file mode 100644 index 0000000..57e7332 --- /dev/null +++ b/backend/utils/userDataMigration.js @@ -0,0 +1,172 @@ +const fs = require('fs-extra'); +const path = require('path'); +const { getHytaleSavesDir, getResolvedAppDir } = require('../core/paths'); +const { loadConfig, saveConfig } = require('../core/config'); + +/** + * NEW SYSTEM (2.1.2+): UserData Migration to Centralized Location + * + * UserData is now stored in a centralized location instead of inside game installation: + * - Windows: %LOCALAPPDATA%\HytaleSaves\ + * - macOS: ~/Library/Application Support/HytaleSaves/ + * - Linux: ~/.hytalesaves/ + * + * This eliminates the need for backup/restore during updates. + */ + +/** + * Check if migration to centralized UserData has been completed + */ +function isMigrationCompleted() { + const config = loadConfig(); + return config.userDataMigrated === true; +} + +/** + * Mark migration as completed + */ +function markMigrationCompleted() { + saveConfig({ userDataMigrated: true }); + console.log('[UserDataMigration] Migration marked as completed in config'); +} + +/** + * Find old UserData location (pre-2.1.2) + * Searches in: installPath/branch/package/game/latest/Client/UserData + */ +function findOldUserDataPath() { + try { + const config = loadConfig(); + const installPath = getResolvedAppDir(); + const branch = config.version_branch || 'release'; + + console.log(`[UserDataMigration] Looking for old UserData...`); + console.log(`[UserDataMigration] Install path: ${installPath}`); + console.log(`[UserDataMigration] Branch: ${branch}`); + + // Old location + const oldPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData'); + console.log(`[UserDataMigration] Checking: ${oldPath}`); + console.log(`[UserDataMigration] Checking: ${oldPath}`); + + if (fs.existsSync(oldPath)) { + console.log(`[UserDataMigration] ✓ Found old UserData at: ${oldPath}`); + return oldPath; + } + + console.log(`[UserDataMigration] ✗ Not found at current branch location`); + + // Try other branch if current doesn't exist + const otherBranch = branch === 'release' ? 'pre-release' : 'release'; + const otherPath = path.join(installPath, otherBranch, 'package', 'game', 'latest', 'Client', 'UserData'); + console.log(`[UserDataMigration] Checking other branch: ${otherPath}`); + console.log(`[UserDataMigration] Checking other branch: ${otherPath}`); + + if (fs.existsSync(otherPath)) { + console.log(`[UserDataMigration] ✓ Found old UserData in other branch at: ${otherPath}`); + return otherPath; + } + + console.log('[UserDataMigration] ✗ No old UserData found in any branch'); + return null; + } catch (error) { + console.error('[UserDataMigration] Error finding old UserData:', error); + return null; + } +} + +/** + * Migrate UserData from old location to new centralized location + * One-time operation when upgrading to 2.1.2 + */ +async function migrateUserDataToCentralized() { + // Check if already migrated + if (isMigrationCompleted()) { + console.log('[UserDataMigration] Migration already completed, skipping'); + return { success: true, alreadyMigrated: true }; + } + + console.log('[UserDataMigration] === Starting UserData Migration to Centralized Location ==='); + + const newUserDataPath = getHytaleSavesDir(); + console.log(`[UserDataMigration] Target location: ${newUserDataPath}`); + + // Ensure new directory exists + if (!fs.existsSync(newUserDataPath)) { + fs.mkdirSync(newUserDataPath, { recursive: true }); + console.log('[UserDataMigration] Created new HytaleSaves directory'); + } + + // Find old UserData + const oldUserDataPath = findOldUserDataPath(); + + if (!oldUserDataPath) { + console.log('[UserDataMigration] No old UserData found - fresh install or already migrated'); + // Don't mark as migrated - let it check again next time in case game gets installed later + return { success: true, freshInstall: true }; + } + + // Check if new location already has data (shouldn't happen, but safety check) + const existingFiles = fs.readdirSync(newUserDataPath); + if (existingFiles.length > 0) { + console.warn('[UserDataMigration] New location already contains files, marking as migrated to avoid re-attempts'); + markMigrationCompleted(); + return { success: true, skipped: true, reason: 'target_not_empty' }; + } + + try { + console.log(`[UserDataMigration] Copying from ${oldUserDataPath} to ${newUserDataPath}...`); + + // Copy all UserData to new location + await fs.copy(oldUserDataPath, newUserDataPath, { + overwrite: false, + errorOnExist: false, + dereference: true // Follow symlinks to avoid EPERM errors on Windows + }); + + console.log('[UserDataMigration] ✓ UserData copied successfully'); + + // Mark migration as completed + markMigrationCompleted(); + + console.log('[UserDataMigration] === Migration Completed Successfully ==='); + return { + success: true, + migrated: true, + from: oldUserDataPath, + to: newUserDataPath + }; + + } catch (error) { + console.error('[UserDataMigration] ✗ Migration failed:', error); + return { + success: false, + error: error.message, + from: oldUserDataPath, + to: newUserDataPath + }; + } +} + +/** + * Get the centralized UserData path (always use this in 2.1.2+) + * Ensures directory exists + */ +function getUserDataPath() { + const userDataPath = getHytaleSavesDir(); + + // Ensure directory exists + if (!fs.existsSync(userDataPath)) { + fs.mkdirSync(userDataPath, { recursive: true }); + console.log(`[UserDataMigration] Created UserData directory: ${userDataPath}`); + } + + return userDataPath; +} + +module.exports = { + migrateUserDataToCentralized, + getUserDataPath, + isMigrationCompleted, + findOldUserDataPath +}; diff --git a/main.js b/main.js index 2d8c8f4..0aad11f 100644 --- a/main.js +++ b/main.js @@ -5,6 +5,7 @@ const { autoUpdater } = require('electron-updater'); const fs = require('fs'); const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); const { retryPWRDownload } = require('./backend/managers/gameManager'); +const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration'); // Handle Hardware Acceleration try { @@ -298,6 +299,14 @@ app.whenReady().then(async () => { // Initialize Profile Manager (runs migration if needed) profileManager.init(); + // Migrate UserData to centralized location (v2.1.2+) + console.log('[Startup] Checking UserData migration...'); + try { + await migrateUserDataToCentralized(); + } catch (error) { + console.error('[Startup] UserData migration failed:', error); + } + createSplashScreen(); setTimeout(async () => { diff --git a/package.json b/package.json index 83a319b..c496c76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hytale-f2p-launcher", - "version": "2.1.1", + "version": "2.1.2", "description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support", "homepage": "https://github.com/amiayweb/Hytale-F2P", "main": "main.js", From 34ee099ae2a2096c2b9bec7b9b9cff92e6df35fc Mon Sep 17 00:00:00 2001 From: AMIAY Date: Tue, 27 Jan 2026 03:26:43 +0100 Subject: [PATCH 28/88] french translate --- GUI/js/i18n.js | 1 + GUI/locales/fr.json | 235 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 GUI/locales/fr.json diff --git a/GUI/js/i18n.js b/GUI/js/i18n.js index b2ec9e0..a843b3a 100644 --- a/GUI/js/i18n.js +++ b/GUI/js/i18n.js @@ -4,6 +4,7 @@ const i18n = (() => { let translations = {}; const availableLanguages = [ { code: 'en', name: 'English' }, + { code: 'fr', name: 'Français' }, { code: 'es-ES', name: 'Español (España)' }, { code: 'pt-BR', name: 'Portuguese (Brazil)' }, { code: 'tr-TR', name: 'Turkish (Turkey)' }, diff --git a/GUI/locales/fr.json b/GUI/locales/fr.json new file mode 100644 index 0000000..4ad3316 --- /dev/null +++ b/GUI/locales/fr.json @@ -0,0 +1,235 @@ +{ + "nav": { + "play": "Jouer", + "mods": "Mods", + "news": "Actualités", + "chat": "Chat Joueurs", + "settings": "Paramètres" + }, + "header": { + "playersLabel": "Joueurs:", + "manageProfiles": "Gérer les Profils", + "defaultProfile": "Par défaut" + }, + "install": { + "title": "LAUNCHER GRATUIT", + "playerName": "Nom du Joueur", + "playerNamePlaceholder": "Entrez votre nom", + "gameBranch": "Version du Jeu", + "releaseVersion": "Release (Stable)", + "preReleaseVersion": "Pré-Release (Expérimental)", + "customInstallation": "Installation Personnalisée", + "installationFolder": "Dossier d'Installation", + "pathPlaceholder": "Emplacement par défaut", + "browse": "Parcourir", + "installButton": "INSTALLER HYTALE", + "installing": "INSTALLATION..." + }, + "play": { + "ready": "PRÊT À JOUER", + "subtitle": "Lancez Hytale et entrez dans l'aventure", + "playButton": "JOUER À HYTALE", + "latestNews": "DERNIÈRES ACTUALITÉS", + "viewAll": "VOIR TOUT", + "checking": "VÉRIFICATION...", + "play": "JOUER" + }, + "mods": { + "searchPlaceholder": "Rechercher des mods...", + "myMods": "MES MODS", + "previous": "PRÉCÉDENT", + "next": "SUIVANT", + "page": "Page", + "of": "sur", + "modalTitle": "MES MODS", + "noModsFound": "Aucun Mod Trouvé", + "noModsFoundDesc": "Essayez d'ajuster votre recherche", + "noModsInstalled": "Aucun Mod Installé", + "noModsInstalledDesc": "Ajoutez des mods depuis CurseForge ou importez des fichiers locaux", + "view": "VOIR", + "install": "INSTALLER", + "installed": "INSTALLÉ", + "enable": "ACTIVER", + "disable": "DÉSACTIVER", + "active": "ACTIF", + "disabled": "DÉSACTIVÉ", + "delete": "Supprimer le mod", + "noDescription": "Aucune description disponible", + "confirmDelete": "Êtes-vous sûr de vouloir supprimer \"{name}\" ?", + "confirmDeleteDesc": "Cette action est irréversible.", + "confirmDeletion": "Confirmer la Suppression", + "apiKeyRequired": "Clé API Requise", + "apiKeyRequiredDesc": "Une clé API CurseForge est nécessaire pour parcourir les mods" + }, + "news": { + "title": "TOUTES LES ACTUALITÉS", + "readMore": "Lire Plus" + }, + "chat": { + "title": "CHAT JOUEURS", + "pickColor": "Couleur", + "inputPlaceholder": "Tapez votre message...", + "send": "Envoyer", + "online": "en ligne", + "charCounter": "{current}/{max}", + "secureChat": "Chat sécurisé - Les liens sont censurés", + "joinChat": "Rejoindre le Chat", + "chooseUsername": "Choisissez un nom d'utilisateur pour rejoindre le Chat Joueurs", + "username": "Nom d'utilisateur", + "usernamePlaceholder": "Entrez votre nom d'utilisateur...", + "usernameHint": "3-20 caractères, lettres, chiffres, - et _ uniquement", + "joinButton": "Rejoindre le Chat", + "colorModal": { + "title": "Personnaliser la Couleur du Nom", + "chooseSolid": "Choisissez une couleur unie:", + "customColor": "Couleur personnalisée:", + "preview": "Aperçu:", + "previewUsername": "Nom d'utilisateur", + "apply": "Appliquer la Couleur" + } + }, + "settings": { + "title": "PARAMÈTRES", + "java": "Java Runtime", + "useCustomJava": "Utiliser un Chemin Java Personnalisé", + "javaDescription": "Remplacer le Java intégré par votre propre installation", + "javaPath": "Chemin de l'Exécutable Java", + "javaPathPlaceholder": "Sélectionnez le chemin Java...", + "javaBrowse": "Parcourir", + "javaHint": "Sélectionnez le dossier d'installation de Java (compatible Windows, Mac, Linux)", + "discord": "Intégration Discord", + "enableRPC": "Activer Discord Rich Presence", + "discordDescription": "Afficher votre activité du launcher sur Discord", + "game": "Options de Jeu", + "playerName": "Nom du Joueur", + "playerNamePlaceholder": "Entrez le nom du joueur", + "playerNameHint": "Ce nom sera utilisé en jeu (1-16 caractères)", + "openGameLocation": "Ouvrir l'Emplacement du Jeu", + "openGameLocationDesc": "Ouvrir le dossier d'installation du jeu", + "account": "Gestion UUID Joueur", + "currentUUID": "UUID Actuel", + "uuidPlaceholder": "Chargement UUID...", + "copyUUID": "Copier UUID", + "regenerateUUID": "Régénérer UUID", + "uuidHint": "Votre identifiant unique de joueur pour ce nom d'utilisateur", + "manageUUIDs": "Gérer Tous les UUIDs", + "manageUUIDsDesc": "Voir et gérer tous les UUIDs de joueurs", + "language": "Langue", + "selectLanguage": "Sélectionner la Langue", + "repairGame": "Réparer le Jeu", + "reinstallGame": "Réinstaller les fichiers du jeu (préserve les données)", + "gpuPreference": "Préférence GPU", + "gpuHint": "Sélectionnez votre GPU préféré (Linux: affecte DRI_PRIME)", + "gpuAuto": "Auto", + "gpuIntegrated": "Intégré", + "gpuDedicated": "Dédié", + "logs": "JOURNAUX SYSTÈME", + "logsCopy": "Copier", + "logsRefresh": "Actualiser", + "logsFolder": "Ouvrir le Dossier", + "logsLoading": "Chargement des journaux...", + "closeLauncher": "Comportement du Launcher", + "closeOnStart": "Fermer le Launcher au démarrage du jeu", + "closeOnStartDescription": "Fermer automatiquement le launcher après le lancement d'Hytale", + "hwAccel": "Accélération Matérielle", + "hwAccelDescription": "Activer l'accélération matérielle pour le launcher", + "gameBranch": "Branche du Jeu", + "branchRelease": "Release", + "branchPreRelease": "Pré-Release", + "branchHint": "Basculer entre la version stable release et la pré-release expérimentale", + "branchWarning": "Changer de branche téléchargera et installera une version différente du jeu", + "branchSwitching": "Passage à {branch}...", + "branchSwitched": "Passage à {branch} réussi!", + "installRequired": "Installation Requise", + "branchInstallConfirm": "Le jeu sera installé pour la branche {branch}. Continuer?" + }, + "uuid": { + "modalTitle": "Gestion UUID", + "currentUserUUID": "UUID Utilisateur Actuel", + "allPlayerUUIDs": "Tous les UUIDs Joueurs", + "generateNew": "Générer Nouvel UUID", + "loadingUUIDs": "Chargement des UUIDs...", + "setCustomUUID": "Définir UUID Personnalisé", + "customPlaceholder": "Entrez UUID personnalisé (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)", + "setUUID": "Définir UUID", + "warning": "Attention: Définir un UUID personnalisé changera votre identité de joueur actuelle", + "copyTooltip": "Copier UUID", + "regenerateTooltip": "Générer Nouvel UUID" + }, + "profiles": { + "modalTitle": "Gérer les Profils", + "newProfilePlaceholder": "Nom du Nouveau Profil", + "createProfile": "Créer un Profil" + }, + "discord": { + "notificationText": "Rejoignez notre communauté Discord!", + "joinButton": "Rejoindre Discord" + }, + "common": { + "confirm": "Confirmer", + "cancel": "Annuler", + "save": "Sauvegarder", + "close": "Fermer", + "delete": "Supprimer", + "edit": "Modifier", + "loading": "Chargement...", + "apply": "Appliquer", + "install": "Installer" + }, + "notifications": { + "gameDataNotFound": "Erreur: Données du jeu introuvables", + "gameUpdatedSuccess": "Jeu mis à jour avec succès! 🎉", + "updateFailed": "Mise à jour échouée: {error}", + "updateError": "Erreur de mise à jour: {error}", + "discordEnabled": "Discord Rich Presence activé", + "discordDisabled": "Discord Rich Presence désactivé", + "discordSaveFailed": "Échec de la sauvegarde des paramètres Discord", + "playerNameRequired": "Veuillez entrer un nom de joueur valide", + "playerNameSaved": "Nom du joueur sauvegardé avec succès", + "playerNameSaveFailed": "Échec de la sauvegarde du nom du joueur", + "uuidCopied": "UUID copié dans le presse-papiers!", + "uuidCopyFailed": "Échec de la copie de l'UUID", + "uuidRegenNotAvailable": "Régénération UUID non disponible", + "uuidRegenFailed": "Échec de la régénération de l'UUID", + "uuidGenerated": "Nouvel UUID généré avec succès!", + "uuidGeneratedShort": "Nouvel UUID généré!", + "uuidGenerateFailed": "Échec de la génération du nouvel UUID", + "uuidRequired": "Veuillez entrer un UUID", + "uuidInvalidFormat": "Format UUID invalide", + "uuidSetFailed": "Échec de la définition de l'UUID personnalisé", + "uuidSetSuccess": "UUID personnalisé défini avec succès!", + "javaPathCopied": "Chemin Java copié dans le presse-papiers!", + "javaPathCopyFailed": "Échec de la copie du chemin Java", + "javaPathSaved": "Chemin Java sauvegardé avec succès!", + "javaPathSaveFailed": "Échec de la sauvegarde du chemin Java", + "javaPathInvalid": "Chemin Java invalide", + "javaPathReset": "Chemin Java réinitialisé aux valeurs par défaut", + "gameLocationError": "Impossible d'ouvrir l'emplacement du jeu", + "launcherRestartRequired": "Redémarrage du launcher requis pour appliquer les modifications", + "gameRepairConfirm": "Êtes-vous sûr de vouloir réparer le jeu? Cela réinstallera tous les fichiers du jeu.", + "gameRepairInProgress": "Réparation du jeu en cours...", + "gameRepairSuccess": "Jeu réparé avec succès!", + "gameRepairFailed": "Échec de la réparation du jeu: {error}", + "invalidUsername": "Nom d'utilisateur invalide", + "usernameInUse": "Nom d'utilisateur déjà utilisé", + "chatJoinSuccess": "Vous avez rejoint le chat!", + "chatJoinFailed": "Échec de la connexion au chat", + "messageTooLong": "Message trop long", + "messageSent": "Message envoyé", + "messageSendFailed": "Échec de l'envoi du message", + "colorUpdated": "Couleur mise à jour!", + "colorUpdateFailed": "Échec de la mise à jour de la couleur", + "profileCreated": "Profil créé avec succès!", + "profileCreateFailed": "Échec de la création du profil", + "profileDeleted": "Profil supprimé", + "profileDeleteFailed": "Échec de la suppression du profil", + "profileSwitched": "Profil changé vers: {name}", + "profileSwitchFailed": "Échec du changement de profil", + "invalidProfileName": "Nom de profil invalide", + "profileNameExists": "Un profil avec ce nom existe déjà", + "noInternet": "Pas de connexion Internet", + "checkInternetConnection": "Vérifiez votre connexion Internet", + "serverError": "Erreur serveur. Veuillez réessayer plus tard.", + "unknownError": "Une erreur inconnue s'est produite" + } +} From 90258008203ddcb837605c3dc5e183d3d68d3e00 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Tue, 27 Jan 2026 04:29:01 +0100 Subject: [PATCH 29/88] Add German and Swedish translations Added de.json and sv.json locale files for German and Swedish language support. Updated i18n.js to register 'de' and 'sv' as available languages in the launcher. --- GUI/js/i18n.js | 2 + GUI/locales/de.json | 283 ++++++++++++++++++++++++++++++++++++++++++++ GUI/locales/sv.json | 283 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 568 insertions(+) create mode 100644 GUI/locales/de.json create mode 100644 GUI/locales/sv.json diff --git a/GUI/js/i18n.js b/GUI/js/i18n.js index a843b3a..6b93a8d 100644 --- a/GUI/js/i18n.js +++ b/GUI/js/i18n.js @@ -5,6 +5,8 @@ const i18n = (() => { const availableLanguages = [ { code: 'en', name: 'English' }, { code: 'fr', name: 'Français' }, + { code: 'de', name: 'Deutsch' }, + { code: 'sv', name: 'Svenska' }, { code: 'es-ES', name: 'Español (España)' }, { code: 'pt-BR', name: 'Portuguese (Brazil)' }, { code: 'tr-TR', name: 'Turkish (Turkey)' }, diff --git a/GUI/locales/de.json b/GUI/locales/de.json new file mode 100644 index 0000000..d509cdb --- /dev/null +++ b/GUI/locales/de.json @@ -0,0 +1,283 @@ +{ + "nav": { + "play": "Spielen", + "mods": "Mods", + "news": "Neuigkeiten", + "chat": "Spieler-Chat", + "settings": "Einstellungen" + }, + "header": { + "playersLabel": "Spieler:", + "manageProfiles": "Profile verwalten", + "defaultProfile": "Standard" + }, + "install": { + "title": "KOSTENLOSER LAUNCHER", + "playerName": "Spielername", + "playerNamePlaceholder": "Namen eingeben", + "gameBranch": "Spielversion", + "releaseVersion": "Release (Stabil)", + "preReleaseVersion": "Pre-Release (Experimentell)", + "customInstallation": "Benutzerdefinierte Installation", + "installationFolder": "Installationsordner", + "pathPlaceholder": "Standardspeicherort", + "browse": "Durchsuchen", + "installButton": "HYTALE INSTALLIEREN", + "installing": "INSTALLIERE..." + }, + "play": { + "ready": "BEREIT ZUM SPIELEN", + "subtitle": "Starte Hytale und beginne das Abenteuer", + "playButton": "HYTALE SPIELEN", + "latestNews": "NEUESTE NACHRICHTEN", + "viewAll": "ALLE ANZEIGEN", + "checking": "ÜBERPRÜFE...", + "play": "SPIELEN" + }, + "mods": { + "searchPlaceholder": "Mods suchen...", + "myMods": "MEINE MODS", + "previous": "ZURÜCK", + "next": "WEITER", + "page": "Seite", + "of": "von", + "modalTitle": "MEINE MODS", + "noModsFound": "Keine Mods gefunden", + "noModsFoundDesc": "Versuche deine Suche anzupassen", + "noModsInstalled": "Keine Mods installiert", + "noModsInstalledDesc": "Füge Mods von CurseForge hinzu oder importiere lokale Dateien", + "view": "ANZEIGEN", + "install": "INSTALLIEREN", + "installed": "INSTALLIERT", + "enable": "AKTIVIEREN", + "disable": "DEAKTIVIEREN", + "active": "AKTIV", + "disabled": "DEAKTIVIERT", + "delete": "Mod löschen", + "noDescription": "Keine Beschreibung verfügbar", + "confirmDelete": "Möchtest du \"{name}\" wirklich löschen?", + "confirmDeleteDesc": "Diese Aktion kann nicht rückgängig gemacht werden.", + "confirmDeletion": "Löschung bestätigen", + "apiKeyRequired": "API-Schlüssel erforderlich", + "apiKeyRequiredDesc": "CurseForge API-Schlüssel wird benötigt, um Mods zu durchsuchen" + }, + "news": { + "title": "ALLE NACHRICHTEN", + "readMore": "Mehr lesen" + }, + "chat": { + "title": "SPIELER-CHAT", + "pickColor": "Farbe", + "inputPlaceholder": "Nachricht eingeben...", + "send": "Senden", + "online": "online", + "charCounter": "{current}/{max}", + "secureChat": "Sicherer Chat - Links werden zensiert", + "joinChat": "Chat beitreten", + "chooseUsername": "Wähle einen Benutzernamen, um dem Spieler-Chat beizutreten", + "username": "Benutzername", + "usernamePlaceholder": "Benutzernamen eingeben...", + "usernameHint": "3-20 Zeichen, nur Buchstaben, Zahlen, - und _", + "joinButton": "Chat beitreten", + "colorModal": { + "title": "Benutzernamenfarbe anpassen", + "chooseSolid": "Wähle eine einfarbige Farbe:", + "customColor": "Benutzerdefinierte Farbe:", + "preview": "Vorschau:", + "previewUsername": "Benutzername", + "apply": "Farbe anwenden" + } + }, + "settings": { + "title": "EINSTELLUNGEN", + "java": "Java Runtime", + "useCustomJava": "Benutzerdefinierten Java-Pfad verwenden", + "javaDescription": "Ersetze die mitgelieferte Java-Installation durch deine eigene", + "javaPath": "Java-Ausführungsdatei-Pfad", + "javaPathPlaceholder": "Java-Pfad auswählen...", + "javaBrowse": "Durchsuchen", + "javaHint": "Wähle den Java-Installationsordner (unterstützt Windows, Mac, Linux)", + "discord": "Discord-Integration", + "enableRPC": "Discord Rich Presence aktivieren", + "discordDescription": "Zeige deine Launcher-Aktivität auf Discord", + "game": "Spieloptionen", + "playerName": "Spielername", + "playerNamePlaceholder": "Spielernamen eingeben", + "playerNameHint": "Dieser Name wird im Spiel verwendet (1-16 Zeichen)", + "openGameLocation": "Spielordner öffnen", + "openGameLocationDesc": "Öffne den Spielinstallationsordner", + "account": "Spieler-UUID-Verwaltung", + "currentUUID": "Aktuelle UUID", + "uuidPlaceholder": "UUID wird geladen...", + "copyUUID": "UUID kopieren", + "regenerateUUID": "UUID neu generieren", + "uuidHint": "Deine eindeutige Spielerkennung für diesen Benutzernamen", + "manageUUIDs": "Alle UUIDs verwalten", + "manageUUIDsDesc": "Alle Spieler-UUIDs anzeigen und verwalten", + "language": "Sprache", + "selectLanguage": "Sprache auswählen", + "repairGame": "Spiel reparieren", + "reinstallGame": "Spieldateien neu installieren (behält Daten)", + "gpuPreference": "GPU-Präferenz", + "gpuHint": "Wähle deine bevorzugte GPU (Linux: betrifft DRI_PRIME)", + "gpuAuto": "Auto", + "gpuIntegrated": "Integriert", + "gpuDedicated": "Dediziert", + "logs": "SYSTEMPROTOKOLLE", + "logsCopy": "Kopieren", + "logsRefresh": "Aktualisieren", + "logsFolder": "Ordner öffnen", + "logsLoading": "Protokolle werden geladen...", + "closeLauncher": "Launcher-Verhalten", + "closeOnStart": "Launcher beim Spielstart schließen", + "closeOnStartDescription": "Schließe den Launcher automatisch, nachdem Hytale gestartet wurde", + "hwAccel": "Hardware-Beschleunigung", + "hwAccelDescription": "Hardware-Beschleunigung für den Launcher aktivieren", + "gameBranch": "Spiel-Branch", + "branchRelease": "Release", + "branchPreRelease": "Pre-Release", + "branchHint": "Wechsel zwischen stabiler Release- und experimenteller Pre-Release-Version", + "branchWarning": "Das Ändern des Branches lädt eine andere Spielversion herunter und installiert sie", + "branchSwitching": "Wechsle zu {branch}...", + "branchSwitched": "Erfolgreich zu {branch} gewechselt!", + "installRequired": "Installation erforderlich", + "branchInstallConfirm": "Das Spiel wird für den {branch}-Branch installiert. Fortfahren?" + }, + "uuid": { + "modalTitle": "UUID-Verwaltung", + "currentUserUUID": "Aktuelle Benutzer-UUID", + "allPlayerUUIDs": "Alle Spieler-UUIDs", + "generateNew": "Neue UUID generieren", + "loadingUUIDs": "UUIDs werden geladen...", + "setCustomUUID": "Benutzerdefinierte UUID festlegen", + "customPlaceholder": "Benutzerdefinierte UUID eingeben (Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)", + "setUUID": "UUID festlegen", + "warning": "Warnung: Das Festlegen einer benutzerdefinierten UUID ändert deine aktuelle Spieleridentität", + "copyTooltip": "UUID kopieren", + "regenerateTooltip": "Neue UUID generieren" + }, + "profiles": { + "modalTitle": "Profile verwalten", + "newProfilePlaceholder": "Neuer Profilname", + "createProfile": "Profil erstellen" + }, + "discord": { + "notificationText": "Tritt unserer Discord-Community bei!", + "joinButton": "Discord beitreten" + }, + "common": { + "confirm": "Bestätigen", + "cancel": "Abbrechen", + "save": "Speichern", + "close": "Schließen", + "delete": "Löschen", + "edit": "Bearbeiten", + "loading": "Lädt...", + "apply": "Anwenden", + "install": "Installieren" + }, + "notifications": { + "gameDataNotFound": "Fehler: Spieldaten nicht gefunden", + "gameUpdatedSuccess": "Spiel erfolgreich aktualisiert! 🎉", + "updateFailed": "Update fehlgeschlagen: {error}", + "updateError": "Update-Fehler: {error}", + "discordEnabled": "Discord Rich Presence aktiviert", + "discordDisabled": "Discord Rich Presence deaktiviert", + "discordSaveFailed": "Discord-Einstellung konnte nicht gespeichert werden", + "playerNameRequired": "Bitte gib einen gültigen Spielernamen ein", + "playerNameSaved": "Spielername erfolgreich gespeichert", + "playerNameSaveFailed": "Spielername konnte nicht gespeichert werden", + "uuidCopied": "UUID in die Zwischenablage kopiert!", + "uuidCopyFailed": "UUID konnte nicht kopiert werden", + "uuidRegenNotAvailable": "UUID-Neugenerierung nicht verfügbar", + "uuidRegenFailed": "UUID konnte nicht neu generiert werden", + "uuidGenerated": "Neue UUID erfolgreich generiert!", + "uuidGeneratedShort": "Neue UUID generiert!", + "uuidGenerateFailed": "Neue UUID konnte nicht generiert werden", + "uuidRequired": "Bitte gib eine UUID ein", + "uuidInvalidFormat": "Ungültiges UUID-Format", + "uuidSetFailed": "Benutzerdefinierte UUID konnte nicht festgelegt werden", + "uuidSetSuccess": "Benutzerdefinierte UUID erfolgreich festgelegt!", + "uuidDeleteFailed": "UUID konnte nicht gelöscht werden", + "uuidDeleteSuccess": "UUID erfolgreich gelöscht!", + "modsDownloading": "{name} wird heruntergeladen...", + "modsTogglingMod": "Mod wird umgeschaltet...", + "modsDeletingMod": "Mod wird gelöscht...", + "modsLoadingMods": "Mods von CurseForge werden geladen...", + "modsInstalledSuccess": "{name} erfolgreich installiert! 🎉", + "modsDeletedSuccess": "{name} erfolgreich gelöscht", + "modsDownloadFailed": "Mod konnte nicht heruntergeladen werden: {error}", + "modsToggleFailed": "Mod konnte nicht umgeschaltet werden: {error}", + "modsDeleteFailed": "Mod konnte nicht gelöscht werden: {error}", + "modsModNotFound": "Mod-Informationen nicht gefunden", + "hwAccelSaved": "Hardware-Beschleunigungseinstellung gespeichert", + "hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden", + "javaPathCopied": "Java-Pfad in die Zwischenablage kopiert!", + "javaPathCopyFailed": "Java-Pfad konnte nicht kopiert werden", + "javaPathSaved": "Java-Pfad erfolgreich gespeichert!", + "javaPathSaveFailed": "Java-Pfad konnte nicht gespeichert werden", + "javaPathInvalid": "Ungültiger Java-Pfad", + "javaPathReset": "Java-Pfad auf Standardwerte zurückgesetzt", + "gameLocationError": "Spielordner konnte nicht geöffnet werden", + "launcherRestartRequired": "Launcher-Neustart erforderlich, um Änderungen anzuwenden", + "gameRepairConfirm": "Möchtest du das Spiel wirklich reparieren? Dies wird alle Spieldateien neu installieren.", + "gameRepairInProgress": "Spiel wird repariert...", + "gameRepairSuccess": "Spiel erfolgreich repariert!", + "gameRepairFailed": "Spielreparatur fehlgeschlagen: {error}", + "invalidUsername": "Ungültiger Benutzername", + "usernameInUse": "Benutzername bereits vergeben", + "chatJoinSuccess": "Du bist dem Chat beigetreten!", + "chatJoinFailed": "Chat-Beitritt fehlgeschlagen", + "messageTooLong": "Nachricht zu lang", + "messageSent": "Nachricht gesendet", + "messageSendFailed": "Nachricht konnte nicht gesendet werden", + "colorUpdated": "Farbe aktualisiert!", + "colorUpdateFailed": "Farbe konnte nicht aktualisiert werden", + "profileCreated": "Profil erfolgreich erstellt!", + "profileCreateFailed": "Profil konnte nicht erstellt werden", + "profileDeleted": "Profil gelöscht", + "profileDeleteFailed": "Profil konnte nicht gelöscht werden", + "profileSwitched": "Profil gewechselt zu: {name}", + "profileSwitchFailed": "Profilwechsel fehlgeschlagen", + "invalidProfileName": "Ungültiger Profilname", + "profileNameExists": "Ein Profil mit diesem Namen existiert bereits", + "noInternet": "Keine Internetverbindung", + "checkInternetConnection": "Überprüfe deine Internetverbindung", + "serverError": "Serverfehler. Bitte versuche es später erneut.", + "unknownError": "Ein unbekannter Fehler ist aufgetreten" + }, + "confirm": { + "defaultTitle": "Aktion bestätigen", + "regenerateUuidTitle": "Neue UUID generieren", + "regenerateUuidMessage": "Möchtest du wirklich eine neue UUID generieren? Dies ändert deine Spieleridentität.", + "regenerateUuidButton": "Generieren", + "setCustomUuidTitle": "Benutzerdefinierte UUID festlegen", + "setCustomUuidMessage": "Möchtest du wirklich diese benutzerdefinierte UUID festlegen? Dies ändert deine Spieleridentität.", + "setCustomUuidButton": "UUID festlegen", + "deleteUuidTitle": "UUID löschen", + "deleteUuidMessage": "Möchtest du wirklich die UUID für \"{username}\" löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "deleteUuidButton": "Löschen", + "uninstallGameTitle": "Spiel deinstallieren", + "uninstallGameMessage": "Möchtest du Hytale wirklich deinstallieren? Alle Spieldateien werden gelöscht.", + "uninstallGameButton": "Deinstallieren" + }, + "progress": { + "initializing": "Initialisiere...", + "downloading": "Lädt herunter...", + "installing": "Installiere...", + "extracting": "Entpacke...", + "verifying": "Überprüfe...", + "switchingProfile": "Profil wird gewechselt...", + "profileSwitched": "Profil gewechselt!", + "startingGame": "Spiel wird gestartet...", + "launching": "STARTET...", + "uninstallingGame": "Spiel wird deinstalliert...", + "gameUninstalled": "Spiel erfolgreich deinstalliert!", + "uninstallFailed": "Deinstallation fehlgeschlagen: {error}", + "startingUpdate": "Obligatorisches Spiel-Update wird gestartet...", + "installationComplete": "Installation erfolgreich abgeschlossen!", + "installationFailed": "Installation fehlgeschlagen: {error}", + "installingGameFiles": "Spieldateien werden installiert...", + "installComplete": "Installation abgeschlossen!" + } +} diff --git a/GUI/locales/sv.json b/GUI/locales/sv.json new file mode 100644 index 0000000..04a9773 --- /dev/null +++ b/GUI/locales/sv.json @@ -0,0 +1,283 @@ +{ + "nav": { + "play": "Spela", + "mods": "Moddar", + "news": "Nyheter", + "chat": "Spelarchatt", + "settings": "Inställningar" + }, + "header": { + "playersLabel": "Spelare:", + "manageProfiles": "Hantera profiler", + "defaultProfile": "Standard" + }, + "install": { + "title": "GRATIS LAUNCHER", + "playerName": "Spelarnamn", + "playerNamePlaceholder": "Ange ditt namn", + "gameBranch": "Spelversion", + "releaseVersion": "Release (Stabil)", + "preReleaseVersion": "Pre-Release (Experimentell)", + "customInstallation": "Anpassad installation", + "installationFolder": "Installationsmapp", + "pathPlaceholder": "Standardplats", + "browse": "Bläddra", + "installButton": "INSTALLERA HYTALE", + "installing": "INSTALLERAR..." + }, + "play": { + "ready": "REDO ATT SPELA", + "subtitle": "Starta Hytale och börja äventyret", + "playButton": "SPELA HYTALE", + "latestNews": "SENASTE NYHETERNA", + "viewAll": "VISA ALLA", + "checking": "KONTROLLERAR...", + "play": "SPELA" + }, + "mods": { + "searchPlaceholder": "Sök moddar...", + "myMods": "MINA MODDAR", + "previous": "FÖREGÅENDE", + "next": "NÄSTA", + "page": "Sida", + "of": "av", + "modalTitle": "MINA MODDAR", + "noModsFound": "Inga moddar hittades", + "noModsFoundDesc": "Försök justera din sökning", + "noModsInstalled": "Inga moddar installerade", + "noModsInstalledDesc": "Lägg till moddar från CurseForge eller importera lokala filer", + "view": "VISA", + "install": "INSTALLERA", + "installed": "INSTALLERAD", + "enable": "AKTIVERA", + "disable": "INAKTIVERA", + "active": "AKTIV", + "disabled": "INAKTIVERAD", + "delete": "Ta bort modd", + "noDescription": "Ingen beskrivning tillgänglig", + "confirmDelete": "Är du säker på att du vill ta bort \"{name}\"?", + "confirmDeleteDesc": "Denna åtgärd kan inte ångras.", + "confirmDeletion": "Bekräfta borttagning", + "apiKeyRequired": "API-nyckel krävs", + "apiKeyRequiredDesc": "CurseForge API-nyckel behövs för att bläddra bland moddar" + }, + "news": { + "title": "ALLA NYHETER", + "readMore": "Läs mer" + }, + "chat": { + "title": "SPELARCHATT", + "pickColor": "Färg", + "inputPlaceholder": "Skriv ditt meddelande...", + "send": "Skicka", + "online": "online", + "charCounter": "{current}/{max}", + "secureChat": "Säker chatt - Länkar är censurerade", + "joinChat": "Gå med i chatten", + "chooseUsername": "Välj ett användarnamn för att gå med i spelarchartten", + "username": "Användarnamn", + "usernamePlaceholder": "Ange ditt användarnamn...", + "usernameHint": "3-20 tecken, endast bokstäver, siffror, - och _", + "joinButton": "Gå med i chatten", + "colorModal": { + "title": "Anpassa användarnamnsfargen", + "chooseSolid": "Välj en enfärgad färg:", + "customColor": "Anpassad färg:", + "preview": "Förhandsvisning:", + "previewUsername": "Användarnamn", + "apply": "Använd färg" + } + }, + "settings": { + "title": "INSTÄLLNINGAR", + "java": "Java Runtime", + "useCustomJava": "Använd anpassad Java-sökväg", + "javaDescription": "Ersätt den medföljande Java-installationen med din egen", + "javaPath": "Java-körbar fil-sökväg", + "javaPathPlaceholder": "Välj Java-sökväg...", + "javaBrowse": "Bläddra", + "javaHint": "Välj Java-installationsmappen (stöder Windows, Mac, Linux)", + "discord": "Discord-integration", + "enableRPC": "Aktivera Discord Rich Presence", + "discordDescription": "Visa din launcher-aktivitet på Discord", + "game": "Spelalternativ", + "playerName": "Spelarnamn", + "playerNamePlaceholder": "Ange spelarnamn", + "playerNameHint": "Detta namn kommer att användas i spelet (1-16 tecken)", + "openGameLocation": "Öppna spelplats", + "openGameLocationDesc": "Öppna spelinstallationsmappen", + "account": "Spelare UUID-hantering", + "currentUUID": "Nuvarande UUID", + "uuidPlaceholder": "Laddar UUID...", + "copyUUID": "Kopiera UUID", + "regenerateUUID": "Återskapa UUID", + "uuidHint": "Din unika spelaridentifierare för detta användarnamn", + "manageUUIDs": "Hantera alla UUID:er", + "manageUUIDsDesc": "Visa och hantera alla spelare-UUID:er", + "language": "Språk", + "selectLanguage": "Välj språk", + "repairGame": "Reparera spel", + "reinstallGame": "Ominstallera spelfiler (bevarar data)", + "gpuPreference": "GPU-preferens", + "gpuHint": "Välj din föredragna GPU (Linux: påverkar DRI_PRIME)", + "gpuAuto": "Auto", + "gpuIntegrated": "Integrerad", + "gpuDedicated": "Dedikerad", + "logs": "SYSTEMLOGGAR", + "logsCopy": "Kopiera", + "logsRefresh": "Uppdatera", + "logsFolder": "Öppna mapp", + "logsLoading": "Laddar loggar...", + "closeLauncher": "Launcher-beteende", + "closeOnStart": "Stäng launcher vid spelstart", + "closeOnStartDescription": "Stäng automatiskt launcher efter att Hytale har startats", + "hwAccel": "Hårdvaruacceleration", + "hwAccelDescription": "Aktivera hårdvaruacceleration för launchern", + "gameBranch": "Spelgren", + "branchRelease": "Release", + "branchPreRelease": "Pre-Release", + "branchHint": "Växla mellan stabil release- och experimentell pre-release-version", + "branchWarning": "Att byta gren kommer att ladda ner och installera en annan spelversion", + "branchSwitching": "Byter till {branch}...", + "branchSwitched": "Bytte framgångsrikt till {branch}!", + "installRequired": "Installation krävs", + "branchInstallConfirm": "Spelet kommer att installeras för {branch}-grenen. Fortsätt?" + }, + "uuid": { + "modalTitle": "UUID-hantering", + "currentUserUUID": "Nuvarande användar-UUID", + "allPlayerUUIDs": "Alla spelare-UUID:er", + "generateNew": "Generera ny UUID", + "loadingUUIDs": "Laddar UUID:er...", + "setCustomUUID": "Ange anpassad UUID", + "customPlaceholder": "Ange anpassad UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)", + "setUUID": "Ange UUID", + "warning": "Varning: Att ange en anpassad UUID kommer att ändra din nuvarande spelaridentitet", + "copyTooltip": "Kopiera UUID", + "regenerateTooltip": "Generera ny UUID" + }, + "profiles": { + "modalTitle": "Hantera profiler", + "newProfilePlaceholder": "Nytt profilnamn", + "createProfile": "Skapa profil" + }, + "discord": { + "notificationText": "Gå med i vår Discord-gemenskap!", + "joinButton": "Gå med i Discord" + }, + "common": { + "confirm": "Bekräfta", + "cancel": "Avbryt", + "save": "Spara", + "close": "Stäng", + "delete": "Ta bort", + "edit": "Redigera", + "loading": "Laddar...", + "apply": "Verkställ", + "install": "Installera" + }, + "notifications": { + "gameDataNotFound": "Fel: Speldata hittades inte", + "gameUpdatedSuccess": "Spelet uppdaterades framgångsrikt! 🎉", + "updateFailed": "Uppdatering misslyckades: {error}", + "updateError": "Uppdateringsfel: {error}", + "discordEnabled": "Discord Rich Presence aktiverad", + "discordDisabled": "Discord Rich Presence inaktiverad", + "discordSaveFailed": "Misslyckades med att spara Discord-inställning", + "playerNameRequired": "Ange ett giltigt spelarnamn", + "playerNameSaved": "Spelarnamn sparat framgångsrikt", + "playerNameSaveFailed": "Misslyckades med att spara spelarnamn", + "uuidCopied": "UUID kopierad till urklipp!", + "uuidCopyFailed": "Misslyckades med att kopiera UUID", + "uuidRegenNotAvailable": "UUID-återgenerering ej tillgänglig", + "uuidRegenFailed": "Misslyckades med att återgenerera UUID", + "uuidGenerated": "Ny UUID genererad framgångsrikt!", + "uuidGeneratedShort": "Ny UUID genererad!", + "uuidGenerateFailed": "Misslyckades med att generera ny UUID", + "uuidRequired": "Ange en UUID", + "uuidInvalidFormat": "Ogiltigt UUID-format", + "uuidSetFailed": "Misslyckades med att ange anpassad UUID", + "uuidSetSuccess": "Anpassad UUID angiven framgångsrikt!", + "uuidDeleteFailed": "Misslyckades med att ta bort UUID", + "uuidDeleteSuccess": "UUID borttagen framgångsrikt!", + "modsDownloading": "Laddar ner {name}...", + "modsTogglingMod": "Växlar modd...", + "modsDeletingMod": "Tar bort modd...", + "modsLoadingMods": "Laddar moddar från CurseForge...", + "modsInstalledSuccess": "{name} installerad framgångsrikt! 🎉", + "modsDeletedSuccess": "{name} borttagen framgångsrikt", + "modsDownloadFailed": "Misslyckades med att ladda ner modd: {error}", + "modsToggleFailed": "Misslyckades med att växla modd: {error}", + "modsDeleteFailed": "Misslyckades med att ta bort modd: {error}", + "modsModNotFound": "Moddinformation hittades inte", + "hwAccelSaved": "Hårdvaruaccelerationsinställning sparad", + "hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning", + "javaPathCopied": "Java-sökväg kopierad till urklipp!", + "javaPathCopyFailed": "Misslyckades med att kopiera Java-sökväg", + "javaPathSaved": "Java-sökväg sparad framgångsrikt!", + "javaPathSaveFailed": "Misslyckades med att spara Java-sökväg", + "javaPathInvalid": "Ogiltig Java-sökväg", + "javaPathReset": "Java-sökväg återställd till standardvärden", + "gameLocationError": "Kunde inte öppna spelplats", + "launcherRestartRequired": "Launcher-omstart krävs för att tillämpa ändringar", + "gameRepairConfirm": "Är du säker på att du vill reparera spelet? Detta kommer att ominstallera alla spelfiler.", + "gameRepairInProgress": "Reparerar spel...", + "gameRepairSuccess": "Spel reparerat framgångsrikt!", + "gameRepairFailed": "Spelreparation misslyckades: {error}", + "invalidUsername": "Ogiltigt användarnamn", + "usernameInUse": "Användarnamn upptaget", + "chatJoinSuccess": "Du har gått med i chatten!", + "chatJoinFailed": "Misslyckades med att gå med i chatten", + "messageTooLong": "Meddelande för långt", + "messageSent": "Meddelande skickat", + "messageSendFailed": "Misslyckades med att skicka meddelande", + "colorUpdated": "Färg uppdaterad!", + "colorUpdateFailed": "Misslyckades med att uppdatera färg", + "profileCreated": "Profil skapad framgångsrikt!", + "profileCreateFailed": "Misslyckades med att skapa profil", + "profileDeleted": "Profil borttagen", + "profileDeleteFailed": "Misslyckades med att ta bort profil", + "profileSwitched": "Bytte profil till: {name}", + "profileSwitchFailed": "Profilbyte misslyckades", + "invalidProfileName": "Ogiltigt profilnamn", + "profileNameExists": "En profil med detta namn finns redan", + "noInternet": "Ingen internetanslutning", + "checkInternetConnection": "Kontrollera din internetanslutning", + "serverError": "Serverfel. Försök igen senare.", + "unknownError": "Ett okänt fel inträffade" + }, + "confirm": { + "defaultTitle": "Bekräfta åtgärd", + "regenerateUuidTitle": "Generera ny UUID", + "regenerateUuidMessage": "Är du säker på att du vill generera en ny UUID? Detta kommer att ändra din spelaridentitet.", + "regenerateUuidButton": "Generera", + "setCustomUuidTitle": "Ange anpassad UUID", + "setCustomUuidMessage": "Är du säker på att du vill ange denna anpassade UUID? Detta kommer att ändra din spelaridentitet.", + "setCustomUuidButton": "Ange UUID", + "deleteUuidTitle": "Ta bort UUID", + "deleteUuidMessage": "Är du säker på att du vill ta bort UUID:n för \"{username}\"? Denna åtgärd kan inte ångras.", + "deleteUuidButton": "Ta bort", + "uninstallGameTitle": "Avinstallera spel", + "uninstallGameMessage": "Är du säker på att du vill avinstallera Hytale? Alla spelfiler kommer att tas bort.", + "uninstallGameButton": "Avinstallera" + }, + "progress": { + "initializing": "Initierar...", + "downloading": "Laddar ner...", + "installing": "Installerar...", + "extracting": "Extraherar...", + "verifying": "Verifierar...", + "switchingProfile": "Byter profil...", + "profileSwitched": "Profil bytt!", + "startingGame": "Startar spel...", + "launching": "STARTAR...", + "uninstallingGame": "Avinstallerar spel...", + "gameUninstalled": "Spel avinstallerat framgångsrikt!", + "uninstallFailed": "Avinstallation misslyckades: {error}", + "startingUpdate": "Startar obligatorisk speluppdatering...", + "installationComplete": "Installation slutförd framgångsrikt!", + "installationFailed": "Installation misslyckades: {error}", + "installingGameFiles": "Installerar spelfiler...", + "installComplete": "Installation slutförd!" + } +} From 661a0c9eed114b569e3fb7112b8889ae0056b803 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Tue, 27 Jan 2026 17:38:33 +0800 Subject: [PATCH 30/88] Update README.md --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7ff99b0..6a1dc4b 100644 --- a/README.md +++ b/README.md @@ -160,10 +160,12 @@

Note 2 Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum.

Note 3 Using Integrated GPU (dGPU) must have 12 GB RAM minimum.

+> [!NOTE] +> Warning +--- ### 🪟 Windows Prequisites -* ** * **Java JDK 25:** * [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows), **no** support for Windows ARM64 in both version 25 and 21. * [Adoptium](https://adoptium.net/temurin/releases/?version=25), has Windows ARM64 support in version 21 only. @@ -178,8 +180,8 @@ > [!WARNING] > Ubuntu-based Distro like ZorinOS or Pop!_OS or Linux Mint would encounter issues due to UbuntuLTS environment, [check this Discord post](https://discord.com/channels/1462260103951421493/1463662398501027973). -* Make sure you have already installed newest **GPU driver**, consult your distro docs or wiki. - +* Make sure you have already installed newest **GPU driver** especially proprietary NVIDIA, consult your distro docs or wiki. + * Also make sure that your GPU can be connected to EGL, try checking it first (again, consult your distro docs or wiki) before installing Hytale game via our launcher. * Install `libpng` package to avoid SDL3_Image error: * `libpng16-16 libpng-dev` for Ubuntu/Debian-based Distro * `libpng libpng-devel` for Fedora/RHEL-based Distro @@ -197,6 +199,10 @@ * Click **More info**. * Click **Run anyway**. 4. **Launch:** Once installed, you can launch the app directly from your Desktop or the Start menu. +5. **Whitelist in Windows Firewall to Avoid "Server Failed to Boot" Error** [#192](https://github.com/amiayweb/Hytale-F2P/issues/192#issuecomment-3803042908) + * Open the Windows Start Menu and search for `Allow an app through Windows Firewall` + * Click "Change settings" (you may need Admin privileges) and Locate `HytaleClient.exe` in the list. + * Ensure both the Private and Public checkboxes are checked. Click OK to save. --- @@ -286,7 +292,7 @@ The `.zip` version is useful for users who prefer a portable installation or nee ## Dedicated Server > [!NOTE] -> If you have already `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server. +> If you already have the patched `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server. > [!TIP] > Use services like Playit.gg, Tailscale, Radmin VPN to share UDP connection if setting up router as an admin is not possible. @@ -295,7 +301,7 @@ The `.zip` version is useful for users who prefer a portable installation or nee > `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting). > [!IMPORTANT] -> See detailed information of setting up a server here: [SERVER.md](SERVER.md) +> See detailed information of setting up a server here: [SERVER.md](SERVER.md). Download the latest patched JAR, the patched RAR, or the SH/BAT scripts from channel `#open-public-server` in our Discord Server. --- From a5c931b26d3e60e0ebfbddef95a871e34240a969 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Tue, 27 Jan 2026 18:45:55 +0800 Subject: [PATCH 31/88] chore: add offline-mode warning to the README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6a1dc4b..964b01e 100644 --- a/README.md +++ b/README.md @@ -160,8 +160,9 @@

Note 2 Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum.

Note 3 Using Integrated GPU (dGPU) must have 12 GB RAM minimum.

-> [!NOTE] -> Warning +> [!WARNING] +> Our launcher has **not yet** supported Offline Mode (playing Hytale without internet). +> We will surely add the feature as soon as possible. Kindly wait for the update. --- From ee18455b4be86aedfdc2f793f05814fad3cf6a1e Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Tue, 27 Jan 2026 21:14:53 +0800 Subject: [PATCH 32/88] chore: add downloads counter in README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 964b01e..2e66c94 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@

An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)

+![GitHub Downloads](https://img.shields.io/github/downloads/amiayweb/Hytale-F2P/total?style=for-the-badge) ![Version](https://img.shields.io/badge/Version-2.1.1-green?style=for-the-badge) ![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-orange?style=for-the-badge) ![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge) From 9ec97f9d33399eeb9fadb284af63f5d2753503f4 Mon Sep 17 00:00:00 2001 From: sanasol Date: Tue, 27 Jan 2026 19:40:42 +0100 Subject: [PATCH 33/88] fix: Steam Deck/Ubuntu crash - use system libzstd.so The bundled libzstd.so is incompatible with glibc 2.41's stricter heap validation, causing "free(): invalid pointer" crashes. Solution: Automatically replace bundled libzstd.so with system version on Linux. The launcher detects and symlinks to /usr/lib/libzstd.so.1. - Auto-detect system libzstd at common paths (Arch, Debian, Fedora) - Backup bundled version as libzstd.so.bundled - Create symlink to system version - Add HYTALE_NO_LIBZSTD_FIX=1 to disable if needed Co-Authored-By: Claude Opus 4.5 --- backend/managers/gameLauncher.js | 49 ++++++++++ docs/STEAMDECK_CRASH_INVESTIGATION.md | 123 ++++++++++++++++++++++++++ docs/STEAMDECK_DEBUG_COMMANDS.md | 65 ++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 docs/STEAMDECK_CRASH_INVESTIGATION.md create mode 100644 docs/STEAMDECK_DEBUG_COMMANDS.md diff --git a/backend/managers/gameLauncher.js b/backend/managers/gameLauncher.js index fddfa7f..0ac187d 100644 --- a/backend/managers/gameLauncher.js +++ b/backend/managers/gameLauncher.js @@ -285,6 +285,55 @@ exec "$REAL_JAVA" "\${ARGS[@]}" const gpuEnv = setupGpuEnvironment(gpuPreference); Object.assign(env, gpuEnv); + // Linux: Replace bundled libzstd.so with system version to fix glibc 2.41+ crash + // The bundled libzstd causes "free(): invalid pointer" on Steam Deck / Ubuntu LTS + if (process.platform === 'linux' && process.env.HYTALE_NO_LIBZSTD_FIX !== '1') { + const clientDir = path.dirname(clientPath); + const bundledLibzstd = path.join(clientDir, 'libzstd.so'); + const backupLibzstd = path.join(clientDir, 'libzstd.so.bundled'); + + // Common system libzstd paths + const systemLibzstdPaths = [ + '/usr/lib/libzstd.so.1', // Arch Linux, Steam Deck + '/usr/lib/x86_64-linux-gnu/libzstd.so.1', // Debian/Ubuntu + '/usr/lib64/libzstd.so.1' // Fedora/RHEL + ]; + + let systemLibzstd = null; + for (const p of systemLibzstdPaths) { + if (fs.existsSync(p)) { + systemLibzstd = p; + break; + } + } + + if (systemLibzstd && fs.existsSync(bundledLibzstd)) { + try { + const stats = fs.lstatSync(bundledLibzstd); + + // Only replace if it's not already a symlink to system version + if (!stats.isSymbolicLink()) { + // Backup bundled version + if (!fs.existsSync(backupLibzstd)) { + fs.renameSync(bundledLibzstd, backupLibzstd); + console.log(`Linux: Backed up bundled libzstd.so`); + } else { + fs.unlinkSync(bundledLibzstd); + } + + // Create symlink to system version + fs.symlinkSync(systemLibzstd, bundledLibzstd); + console.log(`Linux: Linked libzstd.so to system version (${systemLibzstd}) for glibc 2.41+ compatibility`); + } else { + const linkTarget = fs.readlinkSync(bundledLibzstd); + console.log(`Linux: libzstd.so already linked to ${linkTarget}`); + } + } catch (libzstdError) { + console.warn(`Linux: Could not replace libzstd.so: ${libzstdError.message}`); + } + } + } + try { let spawnOptions = { stdio: ['ignore', 'pipe', 'pipe'], diff --git a/docs/STEAMDECK_CRASH_INVESTIGATION.md b/docs/STEAMDECK_CRASH_INVESTIGATION.md new file mode 100644 index 0000000..a7c95c9 --- /dev/null +++ b/docs/STEAMDECK_CRASH_INVESTIGATION.md @@ -0,0 +1,123 @@ +# Steam Deck / Ubuntu LTS Crash Investigation + +## Status: SOLVED + +**Last updated:** 2026-01-27 + +**Solution:** Replace bundled `libzstd.so` with system version. + +--- + +## Problem Summary + +The Hytale F2P launcher's client patcher causes crashes on Steam Deck and Ubuntu LTS with the error: +``` +free(): invalid pointer +``` +or +``` +SIGSEGV (Segmentation fault) +``` + +The crash occurs after successful authentication, specifically right after "Finished handling RequiredAssets". + +**Affected Systems:** +- Steam Deck (glibc 2.41) +- Ubuntu LTS + +**Working Systems:** +- macOS +- Windows +- Older Arch Linux (glibc < 2.41) + +--- + +## Root Cause + +The **bundled `libzstd.so`** in the game client is incompatible with glibc 2.41's stricter heap validation. When the game decompresses assets using this library, it triggers heap corruption detected by glibc 2.41. + +The crash occurs in `libzstd.so` during `free()` after "Finished handling RequiredAssets" (asset decompression). + +--- + +## Solution + +Replace the bundled `libzstd.so` with the system's `libzstd.so.1`. + +### Automatic (Launcher) + +The launcher automatically detects and replaces `libzstd.so` on Linux systems. No manual action needed. + +### Manual + +```bash +cd ~/.hytalef2p/release/package/game/latest/Client + +# Backup bundled version +mv libzstd.so libzstd.so.bundled + +# Link to system version +# Steam Deck / Arch Linux: +ln -s /usr/lib/libzstd.so.1 libzstd.so + +# Debian / Ubuntu: +ln -s /usr/lib/x86_64-linux-gnu/libzstd.so.1 libzstd.so + +# Fedora / RHEL: +ln -s /usr/lib64/libzstd.so.1 libzstd.so +``` + +### Restore Original + +```bash +cd ~/.hytalef2p/release/package/game/latest/Client +rm libzstd.so +mv libzstd.so.bundled libzstd.so +``` + +--- + +## Why This Works + +1. The bundled `libzstd.so` was likely compiled with different allocator settings or an older toolchain +2. glibc 2.41 has stricter heap validation that catches invalid memory operations +3. The system `libzstd.so.1` is compiled with the system's glibc and uses compatible memory allocation patterns +4. By using the system library, we avoid the incompatibility entirely + +--- + +## Previous Investigation (for reference) + +### What Was Tried Before Finding Solution + +| Approach | Result | +|----------|--------| +| jemalloc allocator | Worked ~30% of time, not stable | +| GLIBC_TUNABLES | No effect | +| taskset (CPU pinning) | Single core too slow | +| nice/chrt (scheduling) | No effect | +| Various patching approaches | All crashed | + +### Key Insight + +The crash was in `libzstd.so`, not in our patched code. The patching just changed timing enough to expose the libzstd incompatibility more frequently. + +--- + +## GDB Stack Trace (Historical) + +``` +#0 0x00007ffff7d3f5a4 in ?? () from /usr/lib/libc.so.6 +#1 raise () from /usr/lib/libc.so.6 +#2 abort () from /usr/lib/libc.so.6 +#3-#4 ?? () from /usr/lib/libc.so.6 +#5 free () from /usr/lib/libc.so.6 +#6 ?? () from libzstd.so <-- CRASH POINT (bundled library) +#7-#24 HytaleClient code (asset decompression) +``` + +--- + +## Branch + +`fix/steamdeck-libzstd` diff --git a/docs/STEAMDECK_DEBUG_COMMANDS.md b/docs/STEAMDECK_DEBUG_COMMANDS.md new file mode 100644 index 0000000..a727301 --- /dev/null +++ b/docs/STEAMDECK_DEBUG_COMMANDS.md @@ -0,0 +1,65 @@ +# Steam Deck / Linux Crash Fix + +## SOLUTION: Use system libzstd + +The crash is caused by the bundled `libzstd.so` being incompatible with glibc 2.41's stricter heap validation. + +### Automatic Fix + +The launcher automatically replaces `libzstd.so` with the system version. No manual action needed. + +### Manual Fix + +```bash +cd ~/.hytalef2p/release/package/game/latest/Client + +# Backup and replace +mv libzstd.so libzstd.so.bundled +ln -s /usr/lib/libzstd.so.1 libzstd.so +``` + +### Restore Original + +```bash +cd ~/.hytalef2p/release/package/game/latest/Client +rm libzstd.so +mv libzstd.so.bundled libzstd.so +``` + +--- + +## Debug Commands (for troubleshooting) + +### Check libzstd Status + +```bash +# Check if symlinked +ls -la ~/.hytalef2p/release/package/game/latest/Client/libzstd.so + +# Find system libzstd +find /usr/lib -name "libzstd.so*" +``` + +### Binary Validation + +```bash +file ~/.hytalef2p/release/package/game/latest/Client/HytaleClient +ldd ~/.hytalef2p/release/package/game/latest/Client/HytaleClient +``` + +### Restore Client Binary + +```bash +cd ~/.hytalef2p/release/package/game/latest/Client +cp HytaleClient.original HytaleClient +rm -f HytaleClient.patched_custom +``` + +--- + +## Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `HYTALE_AUTH_DOMAIN` | Custom auth domain | `auth.sanasol.ws` | +| `HYTALE_NO_LIBZSTD_FIX` | Disable libzstd replacement | `1` | From fbcbafb9b55b392ff12bd6462588864c076981a9 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Wed, 28 Jan 2026 04:26:42 +0800 Subject: [PATCH 34/88] chore: remove Windows and Linux ARM64 information on the README.md --- README.md | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 2e66c94..ec04a4c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

🎮 Hytale F2P Launcher 🚀

💻 Cross-Platform Multiplayer 🖥️

Available for Windows 🪟, macOS 🍎, and Linux 🐧

-

An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)

+

An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support!

![GitHub Downloads](https://img.shields.io/github/downloads/amiayweb/Hytale-F2P/total?style=for-the-badge) @@ -119,9 +119,9 @@ 🖥️ OS - Windows 10/11 (64-bit; X64/ARM64) | Linux (x64/ARM64) | macOS (Apple Silicon only) + Windows 10/11 (64-bit X64) | Linux (x64) | macOS (ARM64/Apple Silicon)
- ⚠️ Note: macOS Intel (x86) is not yet supported 1 + ⚠️ Note: ARM64 (Windows & Linux), macOS (x86/Intel) are not supported! ⚠️ @@ -132,7 +132,7 @@ 🧠 RAM - 8GB (dGPU)2 /
12GB (iGPU)3 + 8GB (dGPU) / 12GB (iGPU)1 16 GB 32 GB @@ -157,9 +157,7 @@ -

Note 1 Hytale did not provide game files for macOS Intel, yet.

-

Note 2 Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum.

-

Note 3 Using Integrated GPU (dGPU) must have 12 GB RAM minimum.

+

Note 1 Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum, while using Integrated GPU (iGPU) must have 12 GB RAM.

> [!WARNING] > Our launcher has **not yet** supported Offline Mode (playing Hytale without internet). @@ -169,10 +167,9 @@ ### 🪟 Windows Prequisites * **Java JDK 25:** - * [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows), **no** support for Windows ARM64 in both version 25 and 21. - * [Adoptium](https://adoptium.net/temurin/releases/?version=25), has Windows ARM64 support in version 21 only. + * [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows) + * [Adoptium](https://adoptium.net/temurin/releases/?version=25) * [Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download), has Windows ARM64 support in version 25. - * Download from any vendor if your OS is not Windows with ARM64 architecture. * **Latest Visual Studio Redist:** * Download via [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe) * Or [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/) @@ -184,7 +181,7 @@ * Make sure you have already installed newest **GPU driver** especially proprietary NVIDIA, consult your distro docs or wiki. * Also make sure that your GPU can be connected to EGL, try checking it first (again, consult your distro docs or wiki) before installing Hytale game via our launcher. -* Install `libpng` package to avoid SDL3_Image error: +* Install `libpng` package to avoid `SDL3_Image` error: * `libpng16-16 libpng-dev` for Ubuntu/Debian-based Distro * `libpng libpng-devel` for Fedora/RHEL-based Distro * `libpng` for Arch-based Distro @@ -198,16 +195,13 @@ 1. **Prerequisites:** Ensure you have installed all [**Windows Prerequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-windows-prequisites) listed above. 2. **Download:** Get the latest `Hytale-F2P-Launcher.exe` from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page. 3. **SmartScreen Note:** Since the executable is currently unsigned, Windows may show a "Windows protected your PC" popup. - * Click **More info**. - * Click **Run anyway**. + * Click **More info**, then click **Run anyway**. 4. **Launch:** Once installed, you can launch the app directly from your Desktop or the Start menu. -5. **Whitelist in Windows Firewall to Avoid "Server Failed to Boot" Error** [#192](https://github.com/amiayweb/Hytale-F2P/issues/192#issuecomment-3803042908) +5. **Whitelist in Windows Firewall** [#192](https://github.com/amiayweb/Hytale-F2P/issues/192#issuecomment-3803042908) * Open the Windows Start Menu and search for `Allow an app through Windows Firewall` * Click "Change settings" (you may need Admin privileges) and Locate `HytaleClient.exe` in the list. * Ensure both the Private and Public checkboxes are checked. Click OK to save. ---- - ### 🐧 Linux Installation 1. **Prerequisites:** Ensure you have installed all [**Linux Prerequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-linux-prequisites) above. @@ -251,8 +245,6 @@ * **Desktop Entry:** After installing via `.rpm`, `.deb`, or `.pkg.tar.zst`, the launcher should automatically appear in your App Library/Grid. * Missing libxcrypt.so.1: Install `libxcrypt-compat` using your package manager ---- - ### 🍎 macOS Installation > [!NOTE] @@ -280,9 +272,9 @@ The `.zip` version is useful for users who prefer a portable installation or nee --- -# How to Host a Server +# 📢 How to Host a Server -## Host your Singleplayer Server (Online-Play Feature) +## 🌐 Host your Singleplayer Server (Online-Play Feature) > [!NOTE] > You have to play the game to host the server. See Dedicated Server section below if you want to host it without you playing as the host. @@ -291,7 +283,7 @@ The `.zip` version is useful for users who prefer a portable installation or nee 2. Pause the game (Esc) > select Online Play > Turn on `Allow Other Players to Join` > Set password if needed > Press `Save`. 3. Check the status `Connected via STUN` or `Connected via UPnP`. -## Dedicated Server +## 🖧 Host a Dedicated Server > [!NOTE] > If you already have the patched `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server. @@ -300,14 +292,20 @@ The `.zip` version is useful for users who prefer a portable installation or nee > Use services like Playit.gg, Tailscale, Radmin VPN to share UDP connection if setting up router as an admin is not possible. > [!WARNING] -> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting). +> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting). Linux ARM64 is supported for server only. > [!IMPORTANT] > See detailed information of setting up a server here: [SERVER.md](SERVER.md). Download the latest patched JAR, the patched RAR, or the SH/BAT scripts from channel `#open-public-server` in our Discord Server. --- -## 🛠️ Building from Source +## 🔧 Troubleshooting + +See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed Troubleshooting guide. + +--- + +## 🔨 Building from Source See [BUILD.md](docs/BUILD.md) for comprehensive build instructions. From c4acb32fcdc4aabd83ca0549bb2cd356e90daa82 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Wed, 28 Jan 2026 05:16:00 +0800 Subject: [PATCH 35/88] Update support_request.yml --- .github/ISSUE_TEMPLATE/support_request.yml | 40 ++++++++++++++++------ 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/support_request.yml b/.github/ISSUE_TEMPLATE/support_request.yml index 53dfe8d..dfec503 100644 --- a/.github/ISSUE_TEMPLATE/support_request.yml +++ b/.github/ISSUE_TEMPLATE/support_request.yml @@ -1,8 +1,22 @@ name: Support Request description: Request help or support -title: "[SUPPORT] " +title: "[SUPPORT] " labels: ["support"] body: + - type: dropdown + id: acknowledge + attributes: + label: Checklist + options: + - label: I have read the README.md before asking Support Request. + required: true + - label: I have read the TROUBLESHOOTING.md before asking Support Request. + required: true + - label: I have added title before submitting this Support Request. + required: true + - label: I acknowledge that my Support Request will not be responded as quick as in Discord Open-A-Ticket, I prefer this way. + required: true + - type: markdown attributes: value: | @@ -24,7 +38,7 @@ body: attributes: label: Context description: Provide any relevant context or background information. - placeholder: "I've tried..., but got..." + placeholder: "I've tried these steps, but got..." validations: required: true @@ -37,12 +51,17 @@ body: validations: required: true - - type: input + - type: dropdown id: version attributes: label: Version description: What version are you using? - placeholder: "e.g. v2.0.11 stable/pre-release" + options: + - v2.1.2 + - v2.1.1 + - v2.1.0 + - v2.0.11 + - v2.0.2 validations: required: true @@ -52,13 +71,12 @@ body: label: Platform description: What platform are you using? options: - - Windows 10 - - Windows 11 - - macOS (Apple Silicon) - - macOS (Intel) - - Linux Ubuntu/Debian-based - - Linux Fedora/RHEL-based - - Linux Arch-based + - Windows 11 x64 + - Windows 10 x64 + - macOS ARM64 (Apple Silicon) + - Linux x64 Ubuntu/Debian-based + - Linux x64 Fedora/RHEL-based + - Linux x64 Arch-based validations: required: true From dd2dbc6f08c81db3c5e114e3f07e053d544c9b7a Mon Sep 17 00:00:00 2001 From: sanasol Date: Wed, 28 Jan 2026 01:48:58 +0100 Subject: [PATCH 36/88] fix: improve update system UX and macOS compatibility Update System Improvements: - Fix duplicate update popups by disabling legacy updater.js - Add skip button to update popup (shows after 30s, on error, or after download) - Add macOS-specific handling with manual download as primary option - Add missing open-download-page IPC handler - Add missing unblockInterface() method to properly clean up after popup close - Add quitAndInstallUpdate alias in preload for compatibility - Remove pulse animation when download completes - Fix manual download button to show correct status and close popup - Sync player name to settings input after first install Client Patcher Cleanup: - Remove server patching code (server uses pre-patched JAR from CDN) - Simplify to client-only patching - Remove unused imports (crypto, AdmZip, execSync, spawn, javaManager) - Remove unused methods (stringToUtf8, findAndReplaceDomainUtf8) - Move localhost dev code to backup file for reference Code Quality Fixes: - Fix duplicate DOMContentLoaded handlers in install.js - Fix duplicate checkForUpdates definition in preload.js - Fix redundant if/else in onProgressUpdate callback - Fix typo "Harwadre" -> "Hardware" in preload.js Co-Authored-By: Claude Opus 4.5 --- GUI/index.html | 2 +- GUI/js/install.js | 12 +- GUI/js/update.js | 216 +++++++++- backend/utils/clientPatcher.js | 741 +++++++++++---------------------- main.js | 66 ++- preload.js | 13 +- 6 files changed, 514 insertions(+), 536 deletions(-) diff --git a/GUI/index.html b/GUI/index.html index 5b4d13d..8e8b4e1 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -882,7 +882,7 @@ - + diff --git a/GUI/js/install.js b/GUI/js/install.js index 4208a5e..a20d1ef 100644 --- a/GUI/js/install.js +++ b/GUI/js/install.js @@ -72,8 +72,11 @@ export async function installGame() { setTimeout(() => { window.LauncherUI.hideProgress(); window.LauncherUI.showLauncherOrInstall(true); + // Sync player name to both launcher and settings inputs const playerNameInput = document.getElementById('playerName'); if (playerNameInput) playerNameInput.value = playerName; + const settingsPlayerName = document.getElementById('settingsPlayerName'); + if (settingsPlayerName) settingsPlayerName.value = playerName; resetInstallButton(); }, 2000); } @@ -125,8 +128,11 @@ function simulateInstallation(playerName) { setTimeout(() => { window.LauncherUI.hideProgress(); window.LauncherUI.showLauncherOrInstall(true); + // Sync player name to both launcher and settings inputs const playerNameInput = document.getElementById('playerName'); if (playerNameInput) playerNameInput.value = playerName; + const settingsPlayerName = document.getElementById('settingsPlayerName'); + if (settingsPlayerName) settingsPlayerName.value = playerName; resetInstallButton(); }, 2000); } @@ -246,9 +252,3 @@ document.addEventListener('DOMContentLoaded', async () => { setupInstallation(); await checkGameStatusAndShowInterface(); }); -window.browseInstallPath = browseInstallPath; - -document.addEventListener('DOMContentLoaded', async () => { - setupInstallation(); - await checkGameStatusAndShowInterface(); -}); diff --git a/GUI/js/update.js b/GUI/js/update.js index aa44277..4059f29 100644 --- a/GUI/js/update.js +++ b/GUI/js/update.js @@ -6,12 +6,12 @@ class ClientUpdateManager { } init() { - window.electronAPI.onUpdatePopup((updateInfo) => { - this.showUpdatePopup(updateInfo); - }); + console.log('🔧 ClientUpdateManager initializing...'); - // Listen for electron-updater events + // Listen for electron-updater events from main.js + // This is the primary update trigger - main.js checks for updates on startup window.electronAPI.onUpdateAvailable((updateInfo) => { + console.log('📥 update-available event received:', updateInfo); this.showUpdatePopup(updateInfo); }); @@ -20,18 +20,30 @@ class ClientUpdateManager { }); window.electronAPI.onUpdateDownloaded((updateInfo) => { + console.log('📦 update-downloaded event received:', updateInfo); this.showUpdateDownloaded(updateInfo); }); window.electronAPI.onUpdateError((errorInfo) => { + console.log('❌ update-error event received:', errorInfo); this.handleUpdateError(errorInfo); }); - this.checkForUpdatesOnDemand(); + console.log('✅ ClientUpdateManager initialized'); + + // Note: Don't call checkForUpdatesOnDemand() here - main.js already checks + // for updates after 3 seconds and sends 'update-available' event. + // Calling it here would cause duplicate popups. } showUpdatePopup(updateInfo) { - if (this.updatePopupVisible) return; + console.log('🔔 showUpdatePopup called, updatePopupVisible:', this.updatePopupVisible); + + // Check if popup already exists in DOM (extra safety) + if (this.updatePopupVisible || document.getElementById('update-popup-overlay')) { + console.log('⚠️ Update popup already visible, skipping'); + return; + } this.updatePopupVisible = true; @@ -92,7 +104,10 @@ class ClientUpdateManager { @@ -113,16 +128,43 @@ class ClientUpdateManager { installBtn.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); - + installBtn.disabled = true; installBtn.innerHTML = 'Installing...'; - + try { await window.electronAPI.quitAndInstallUpdate(); + + // If we're still here after 5 seconds, the install probably failed + setTimeout(() => { + console.log('⚠️ Install may have failed - showing skip option'); + installBtn.disabled = false; + installBtn.innerHTML = 'Try Again'; + + // Show skip button + const skipBtn = document.getElementById('update-skip-btn'); + const footerText = document.getElementById('update-footer-text'); + if (skipBtn) { + skipBtn.style.display = 'inline-block'; + if (footerText) { + footerText.textContent = 'Install not working? Skip for now:'; + } + } + }, 5000); } catch (error) { console.error('❌ Error installing update:', error); installBtn.disabled = false; installBtn.innerHTML = 'Install & Restart'; + + // Show skip button on error + const skipBtn = document.getElementById('update-skip-btn'); + const footerText = document.getElementById('update-footer-text'); + if (skipBtn) { + skipBtn.style.display = 'inline-block'; + if (footerText) { + footerText.textContent = 'Install failed. Skip for now:'; + } + } } }); } @@ -138,10 +180,15 @@ class ClientUpdateManager { try { await window.electronAPI.openDownloadPage(); - console.log('✅ Download page opened, launcher will close...'); - - downloadBtn.innerHTML = 'Launcher closing...'; - + console.log('✅ Download page opened'); + + downloadBtn.innerHTML = 'Opened in browser'; + + // Close the popup after opening download page + setTimeout(() => { + this.closeUpdatePopup(); + }, 1500); + } catch (error) { console.error('❌ Error opening download page:', error); downloadBtn.disabled = false; @@ -161,9 +208,39 @@ class ClientUpdateManager { }); } + // Show skip button after 30 seconds as fallback (in case update is stuck) + setTimeout(() => { + const skipBtn = document.getElementById('update-skip-btn'); + const footerText = document.getElementById('update-footer-text'); + if (skipBtn) { + skipBtn.style.display = 'inline-block'; + if (footerText) { + footerText.textContent = 'Update taking too long?'; + } + } + }, 30000); + + const skipBtn = document.getElementById('update-skip-btn'); + if (skipBtn) { + skipBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.closeUpdatePopup(); + }); + } + console.log('🔔 Update popup displayed with new style'); } + closeUpdatePopup() { + const overlay = document.getElementById('update-popup-overlay'); + if (overlay) { + overlay.remove(); + } + this.updatePopupVisible = false; + this.unblockInterface(); + } + updateDownloadProgress(progress) { const progressBar = document.getElementById('update-progress-bar'); const progressPercent = document.getElementById('update-progress-percent'); @@ -197,35 +274,96 @@ class ClientUpdateManager { const statusText = document.getElementById('update-status-text'); const progressContainer = document.getElementById('update-progress-container'); const buttonsContainer = document.getElementById('update-buttons-container'); + const installBtn = document.getElementById('update-install-btn'); + const downloadBtn = document.getElementById('update-download-btn'); + const skipBtn = document.getElementById('update-skip-btn'); + const footerText = document.getElementById('update-footer-text'); + const popupContainer = document.querySelector('.update-popup-container'); - if (statusText) { - statusText.textContent = 'Update downloaded! Ready to install.'; + // Remove breathing/pulse animation when download is complete + if (popupContainer) { + popupContainer.classList.remove('update-popup-pulse'); } if (progressContainer) { progressContainer.style.display = 'none'; } + // Use platform info from main process if available, fallback to browser detection + const autoInstallSupported = updateInfo.autoInstallSupported !== undefined + ? updateInfo.autoInstallSupported + : navigator.platform.toUpperCase().indexOf('MAC') < 0; + + if (!autoInstallSupported) { + // macOS: Show manual download as primary since auto-update doesn't work + if (statusText) { + statusText.textContent = 'Update downloaded but auto-install may not work on macOS.'; + } + + if (installBtn) { + // Still show install button but as secondary option + installBtn.classList.add('update-download-btn-secondary'); + installBtn.innerHTML = 'Try Install & Restart'; + } + + if (downloadBtn) { + // Make manual download primary + downloadBtn.classList.remove('update-download-btn-secondary'); + downloadBtn.innerHTML = 'Download Manually (Recommended)'; + } + + if (footerText) { + footerText.textContent = 'Auto-install often fails on macOS:'; + } + } else { + // Windows/Linux: Auto-install should work + if (statusText) { + statusText.textContent = 'Update downloaded! Ready to install.'; + } + + if (footerText) { + footerText.textContent = 'Click to install the update:'; + } + } + if (buttonsContainer) { buttonsContainer.style.display = 'block'; } - console.log('✅ Update downloaded, ready to install'); + // Always show skip button in downloaded state + if (skipBtn) { + skipBtn.style.display = 'inline-block'; + console.log('✅ Skip button made visible'); + } else { + console.error('❌ Skip button not found in DOM!'); + } + + console.log('✅ Update downloaded, ready to install. autoInstallSupported:', autoInstallSupported); } handleUpdateError(errorInfo) { console.error('Update error:', errorInfo); - + + // Show skip button immediately on any error + const skipBtn = document.getElementById('update-skip-btn'); + const footerText = document.getElementById('update-footer-text'); + if (skipBtn) { + skipBtn.style.display = 'inline-block'; + if (footerText) { + footerText.textContent = 'Update failed. You can skip for now.'; + } + } + // If manual download is required, update the UI (this will handle status text) if (errorInfo.requiresManualDownload) { this.showManualDownloadRequired(errorInfo); return; // Don't do anything else, showManualDownloadRequired handles everything } - + // For non-critical errors, just show error message without changing status const errorMessage = document.getElementById('update-error-message'); const errorText = document.getElementById('update-error-text'); - + if (errorMessage && errorText) { let message = errorInfo.message || 'An error occurred during the update process.'; if (errorInfo.isMacSigningError) { @@ -289,6 +427,16 @@ class ClientUpdateManager { buttonsContainer.style.display = 'block'; } + // Show skip button for manual download errors + const skipBtn = document.getElementById('update-skip-btn'); + const footerText = document.getElementById('update-footer-text'); + if (skipBtn) { + skipBtn.style.display = 'inline-block'; + if (footerText) { + footerText.textContent = 'Or continue without updating:'; + } + } + console.log('⚠️ Manual download required due to update error'); } @@ -300,13 +448,35 @@ class ClientUpdateManager { document.body.classList.add('no-select'); - document.addEventListener('keydown', this.blockKeyEvents.bind(this), true); - - document.addEventListener('contextmenu', this.blockContextMenu.bind(this), true); - + // Store bound functions so we can remove them later + this._boundBlockKeyEvents = this.blockKeyEvents.bind(this); + this._boundBlockContextMenu = this.blockContextMenu.bind(this); + + document.addEventListener('keydown', this._boundBlockKeyEvents, true); + document.addEventListener('contextmenu', this._boundBlockContextMenu, true); + console.log('🚫 Interface blocked for update'); } + unblockInterface() { + const mainContent = document.querySelector('.flex.w-full.h-screen'); + if (mainContent) { + mainContent.classList.remove('interface-blocked'); + } + + document.body.classList.remove('no-select'); + + // Remove event listeners + if (this._boundBlockKeyEvents) { + document.removeEventListener('keydown', this._boundBlockKeyEvents, true); + } + if (this._boundBlockContextMenu) { + document.removeEventListener('contextmenu', this._boundBlockContextMenu, true); + } + + console.log('✅ Interface unblocked'); + } + blockKeyEvents(event) { if (event.target.closest('#update-popup-overlay')) { if ((event.key === 'Enter' || event.key === ' ') && diff --git a/backend/utils/clientPatcher.js b/backend/utils/clientPatcher.js index 3446fed..4f3dd10 100644 --- a/backend/utils/clientPatcher.js +++ b/backend/utils/clientPatcher.js @@ -1,10 +1,5 @@ const fs = require('fs'); const path = require('path'); -const crypto = require('crypto'); -const AdmZip = require('adm-zip'); -const { execSync, spawn } = require('child_process'); -const { getJavaExec, getBundledJavaPath } = require('../managers/javaManager'); -const { JRE_DIR } = require('../core/paths'); // Domain configuration const ORIGINAL_DOMAIN = 'hytale.com'; @@ -26,15 +21,13 @@ function getTargetDomain() { const DEFAULT_NEW_DOMAIN = 'auth.sanasol.ws'; /** - * Patches HytaleClient and HytaleServer binaries to replace hytale.com with custom domain - * This allows the game to connect to a custom authentication server + * Patches HytaleClient binary to replace hytale.com with custom domain + * Server patching is done via pre-patched JAR download from CDN * * Supports domains from 4 to 16 characters: * - All F2P traffic routes to single endpoint: https://{domain} (no subdomains) * - Domains <= 10 chars: Direct replacement, subdomains stripped * - Domains 11-16 chars: Split mode - first 6 chars replace subdomain prefix, rest replaces domain - * - * Official hytale.com keeps original subdomain behavior (sessions., account-data., etc.) */ class ClientPatcher { constructor() { @@ -61,19 +54,16 @@ class ClientPatcher { /** * Calculate the domain patching strategy based on length - * @returns {object} Strategy with mainDomain and subdomainPrefix */ getDomainStrategy(domain) { if (domain.length <= 10) { - // Direct replacement - subdomains will be stripped return { mode: 'direct', mainDomain: domain, - subdomainPrefix: '', // Empty = subdomains stripped + subdomainPrefix: '', description: `Direct replacement: hytale.com -> ${domain}` }; } else { - // Split mode: first 6 chars become subdomain prefix, rest replaces hytale.com const prefix = domain.slice(0, 6); const suffix = domain.slice(6); return { @@ -87,21 +77,15 @@ class ClientPatcher { /** * Convert a string to the length-prefixed byte format used by the client - * Format: [length byte] [00 00 00 padding] [char1] [00] [char2] [00] ... [lastChar] - * Note: No null byte after the last character */ stringToLengthPrefixed(str) { const length = str.length; - const result = Buffer.alloc(4 + length + (length - 1)); // length byte + padding + chars + separators - - // Length byte + const result = Buffer.alloc(4 + length + (length - 1)); result[0] = length; - // Padding: 00 00 00 result[1] = 0x00; result[2] = 0x00; result[3] = 0x00; - // Characters with null separators (no separator after last char) let pos = 4; for (let i = 0; i < length; i++) { result[pos++] = str.charCodeAt(i); @@ -109,7 +93,6 @@ class ClientPatcher { result[pos++] = 0x00; } } - return result; } @@ -124,13 +107,6 @@ class ClientPatcher { return buf; } - /** - * Convert a string to UTF-8 bytes (how Java stores strings) - */ - stringToUtf8(str) { - return Buffer.from(str, 'utf8'); - } - /** * Find all occurrences of a pattern in a buffer */ @@ -148,7 +124,6 @@ class ClientPatcher { /** * Replace bytes in buffer - only overwrites the length of new bytes - * Prevents offset corruption by not expanding the replacement */ replaceBytes(buffer, oldBytes, newBytes) { let count = 0; @@ -160,9 +135,7 @@ class ClientPatcher { } const positions = this.findAllOccurrences(result, oldBytes); - for (const pos of positions) { - // Only overwrite the length of the new bytes newBytes.copy(result, pos); count++; } @@ -171,32 +144,7 @@ class ClientPatcher { } /** - * UTF-8 domain replacement for Java JAR files. - * Java stores strings in UTF-8 format in the constant pool. - */ - findAndReplaceDomainUtf8(data, oldDomain, newDomain) { - let count = 0; - const result = Buffer.from(data); - - const oldUtf8 = this.stringToUtf8(oldDomain); - const newUtf8 = this.stringToUtf8(newDomain); - - const positions = this.findAllOccurrences(result, oldUtf8); - - for (const pos of positions) { - newUtf8.copy(result, pos); - count++; - console.log(` Patched UTF-8 occurrence at offset 0x${pos.toString(16)}`); - } - - return { buffer: result, count }; - } - - /** - * Smart domain replacement that handles both null-terminated and non-null-terminated strings. - * .NET AOT stores some strings in various formats: - * - Standard UTF-16LE (each char is 2 bytes with \x00 high byte) - * - Length-prefixed where last char may have metadata byte instead of \x00 + * Smart domain replacement that handles both null-terminated and non-null-terminated strings */ findAndReplaceDomainSmart(data, oldDomain, newDomain) { let count = 0; @@ -218,7 +166,6 @@ class ClientPatcher { if (lastCharFirstByte === oldLastCharByte) { newUtf16NoLast.copy(result, pos); - result[lastCharPos] = newLastCharByte; if (lastCharPos + 1 < result.length) { @@ -238,7 +185,6 @@ class ClientPatcher { /** * Apply all domain patches using length-prefixed format - * This is the main patching method for variable-length domains */ applyDomainPatches(data, domain, protocol = 'https://') { let result = Buffer.from(data); @@ -253,9 +199,9 @@ class ClientPatcher { console.log(` Patching sentry: ${oldSentry.slice(0, 30)}... -> ${newSentry}`); const sentryResult = this.replaceBytes( - result, - this.stringToLengthPrefixed(oldSentry), - this.stringToLengthPrefixed(newSentry) + result, + this.stringToLengthPrefixed(oldSentry), + this.stringToLengthPrefixed(newSentry) ); result = sentryResult.buffer; if (sentryResult.count > 0) { @@ -263,12 +209,12 @@ class ClientPatcher { totalCount += sentryResult.count; } - // 2. Patch main domain (hytale.com -> mainDomain) + // 2. Patch main domain console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`); const domainResult = this.replaceBytes( - result, - this.stringToLengthPrefixed(ORIGINAL_DOMAIN), - this.stringToLengthPrefixed(strategy.mainDomain) + result, + this.stringToLengthPrefixed(ORIGINAL_DOMAIN), + this.stringToLengthPrefixed(strategy.mainDomain) ); result = domainResult.buffer; if (domainResult.count > 0) { @@ -283,9 +229,9 @@ class ClientPatcher { for (const sub of subdomains) { console.log(` Patching subdomain: ${sub} -> ${newSubdomainPrefix}`); const subResult = this.replaceBytes( - result, - this.stringToLengthPrefixed(sub), - this.stringToLengthPrefixed(newSubdomainPrefix) + result, + this.stringToLengthPrefixed(sub), + this.stringToLengthPrefixed(newSubdomainPrefix) ); result = subResult.buffer; if (subResult.count > 0) { @@ -298,7 +244,7 @@ class ClientPatcher { } /** - * Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7 + * Patch Discord invite URLs */ patchDiscordUrl(data) { let count = 0; @@ -307,11 +253,10 @@ class ClientPatcher { const oldUrl = '.gg/hytale'; const newUrl = '.gg/MHkEjepMQ7'; - // Try length-prefixed format first const lpResult = this.replaceBytes( - result, - this.stringToLengthPrefixed(oldUrl), - this.stringToLengthPrefixed(newUrl) + result, + this.stringToLengthPrefixed(oldUrl), + this.stringToLengthPrefixed(newUrl) ); if (lpResult.count > 0) { @@ -323,7 +268,6 @@ class ClientPatcher { const newUtf16 = this.stringToUtf16LE(newUrl); const positions = this.findAllOccurrences(result, oldUtf16); - for (const pos of positions) { newUtf16.copy(result, pos); count++; @@ -333,39 +277,66 @@ class ClientPatcher { } /** - * Check if the client binary has already been patched - * Also verifies the binary actually contains the patched domain + * Check patch status of client binary */ - isPatchedAlready(clientPath) { + getPatchStatus(clientPath) { const newDomain = this.getNewDomain(); const patchFlagFile = clientPath + this.patchedFlag; - // First check flag file if (fs.existsSync(patchFlagFile)) { try { const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8')); - if (flagData.targetDomain === newDomain) { - // Verify the binary actually contains the patched domain + const currentDomain = flagData.targetDomain; + + if (currentDomain === newDomain) { const data = fs.readFileSync(clientPath); const strategy = this.getDomainStrategy(newDomain); const domainPattern = this.stringToLengthPrefixed(strategy.mainDomain); if (data.includes(domainPattern)) { - return true; + return { patched: true, currentDomain, needsRestore: false }; } else { - console.log(' Flag exists but binary not patched (was updated?), re-patching...'); - return false; + console.log(' Flag exists but binary not patched (was updated?), needs re-patching...'); + return { patched: false, currentDomain: null, needsRestore: false }; } + } else { + console.log(` Currently patched for "${currentDomain}", need to change to "${newDomain}"`); + return { patched: false, currentDomain, needsRestore: true }; } } catch (e) { - // Flag file corrupt or unreadable + // Flag file corrupt } } + return { patched: false, currentDomain: null, needsRestore: false }; + } + + /** + * Check if client is already patched (backward compat) + */ + isPatchedAlready(clientPath) { + return this.getPatchStatus(clientPath).patched; + } + + /** + * Restore client from backup + */ + restoreFromBackup(clientPath) { + const backupPath = clientPath + '.original'; + if (fs.existsSync(backupPath)) { + console.log(' Restoring original binary from backup for re-patching...'); + fs.copyFileSync(backupPath, clientPath); + const patchFlagFile = clientPath + this.patchedFlag; + if (fs.existsSync(patchFlagFile)) { + fs.unlinkSync(patchFlagFile); + } + return true; + } + console.warn(' No backup found to restore - will try patching anyway'); return false; } /** - * Mark the client as patched + * Mark client as patched */ markAsPatched(clientPath) { const newDomain = this.getNewDomain(); @@ -378,43 +349,46 @@ class ClientPatcher { patchMode: strategy.mode, mainDomain: strategy.mainDomain, subdomainPrefix: strategy.subdomainPrefix, - patcherVersion: '2.0.0', + patcherVersion: '2.1.0', verified: 'binary_contents' }; fs.writeFileSync(patchFlagFile, JSON.stringify(flagData, null, 2)); } /** - * Create a backup of the original client binary + * Create backup of original client binary */ backupClient(clientPath) { const backupPath = clientPath + '.original'; - if (!fs.existsSync(backupPath)) { - console.log(` Creating backup at ${path.basename(backupPath)}`); - fs.copyFileSync(clientPath, backupPath); + try { + if (!fs.existsSync(backupPath)) { + console.log(` Creating backup at ${path.basename(backupPath)}`); + fs.copyFileSync(clientPath, backupPath); + return backupPath; + } + + const currentSize = fs.statSync(clientPath).size; + const backupSize = fs.statSync(backupPath).size; + + if (currentSize !== backupSize) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + const oldBackupPath = `${clientPath}.original.${timestamp}`; + console.log(` File updated, archiving old backup to ${path.basename(oldBackupPath)}`); + fs.renameSync(backupPath, oldBackupPath); + fs.copyFileSync(clientPath, backupPath); + return backupPath; + } + + console.log(' Backup already exists'); return backupPath; + } catch (e) { + console.error(` Failed to create backup: ${e.message}`); + return null; } - - // Check if current file differs from backup (might have been updated) - const currentSize = fs.statSync(clientPath).size; - const backupSize = fs.statSync(backupPath).size; - - if (currentSize !== backupSize) { - // File was updated, create timestamped backup of old backup - const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); - const oldBackupPath = `${clientPath}.original.${timestamp}`; - console.log(` File updated, archiving old backup to ${path.basename(oldBackupPath)}`); - fs.renameSync(backupPath, oldBackupPath); - fs.copyFileSync(clientPath, backupPath); - return backupPath; - } - - console.log(' Backup already exists'); - return backupPath; } /** - * Restore the original client binary from backup + * Restore original client binary */ restoreClient(clientPath) { const backupPath = clientPath + '.original'; @@ -433,15 +407,12 @@ class ClientPatcher { /** * Patch the client binary to use the custom domain - * @param {string} clientPath - Path to the HytaleClient binary - * @param {function} progressCallback - Optional callback for progress updates - * @returns {object} Result object with success status and details */ async patchClient(clientPath, progressCallback) { const newDomain = this.getNewDomain(); const strategy = this.getDomainStrategy(newDomain); - console.log('=== Client Patcher v2.0 ==='); + console.log('=== Client Patcher v2.1 ==='); console.log(`Target: ${clientPath}`); console.log(`Domain: ${newDomain} (${newDomain.length} chars)`); console.log(`Mode: ${strategy.mode}`); @@ -456,32 +427,34 @@ class ClientPatcher { return { success: false, error }; } - if (this.isPatchedAlready(clientPath)) { + const patchStatus = this.getPatchStatus(clientPath); + + if (patchStatus.patched) { console.log(`Client already patched for ${newDomain}, skipping`); - if (progressCallback) { - progressCallback('Client already patched', 100); - } + if (progressCallback) progressCallback('Client already patched', 100); return { success: true, alreadyPatched: true, patchCount: 0 }; } - if (progressCallback) { - progressCallback('Preparing to patch client...', 10); + if (patchStatus.needsRestore) { + if (progressCallback) progressCallback('Restoring original for domain change...', 5); + this.restoreFromBackup(clientPath); } + if (progressCallback) progressCallback('Preparing to patch client...', 10); + console.log('Creating backup...'); - this.backupClient(clientPath); - - if (progressCallback) { - progressCallback('Reading client binary...', 20); + const backupResult = this.backupClient(clientPath); + if (!backupResult) { + console.warn(' Could not create backup - proceeding without backup'); } + if (progressCallback) progressCallback('Reading client binary...', 20); + console.log('Reading client binary...'); const data = fs.readFileSync(clientPath); console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`); - if (progressCallback) { - progressCallback('Patching domain references...', 50); - } + if (progressCallback) progressCallback('Patching domain references...', 50); console.log('Applying domain patches (length-prefixed format)...'); const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain); @@ -492,7 +465,6 @@ class ClientPatcher { if (count === 0 && discordCount === 0) { console.log('No occurrences found - trying legacy UTF-16LE format...'); - // Fallback to legacy patching for older binary formats const legacyResult = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, strategy.mainDomain); if (legacyResult.count > 0) { console.log(`Found ${legacyResult.count} occurrences with legacy format`); @@ -505,18 +477,14 @@ class ClientPatcher { return { success: true, patchCount: 0, warning: 'No occurrences found' }; } - if (progressCallback) { - progressCallback('Writing patched binary...', 80); - } + if (progressCallback) progressCallback('Writing patched binary...', 80); console.log('Writing patched binary...'); fs.writeFileSync(clientPath, finalData); this.markAsPatched(clientPath); - if (progressCallback) { - progressCallback('Patching complete', 100); - } + if (progressCallback) progressCallback('Patching complete', 100); console.log(`Successfully patched ${count} domain occurrences and ${discordCount} Discord URLs`); console.log('=== Patching Complete ==='); @@ -525,16 +493,45 @@ class ClientPatcher { } /** - * Patch the server JAR by downloading pre-patched version - * @param {string} serverPath - Path to the HytaleServer.jar - * @param {function} progressCallback - Optional callback for progress updates - * @param {string} javaPath - Path to Java executable (unused, kept for compatibility) - * @returns {object} Result object with success status and details + * Check if server JAR contains DualAuth classes (was patched) */ - async patchServer(serverPath, progressCallback, javaPath = null) { + serverJarContainsDualAuth(serverPath) { + try { + const data = fs.readFileSync(serverPath); + // Check for DualAuthContext class signature in JAR + const signature = Buffer.from('DualAuthContext', 'utf8'); + return data.includes(signature); + } catch (e) { + return false; + } + } + + /** + * Validate downloaded file is not corrupt/partial + * Server JAR should be at least 50MB + */ + validateServerJarSize(serverPath) { + try { + const stats = fs.statSync(serverPath); + const minSize = 50 * 1024 * 1024; // 50MB minimum + if (stats.size < minSize) { + console.error(` Downloaded JAR too small: ${(stats.size / 1024 / 1024).toFixed(2)} MB (expected >50MB)`); + return false; + } + console.log(` Downloaded size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`); + return true; + } catch (e) { + return false; + } + } + + /** + * Patch server JAR by downloading pre-patched version from CDN + */ + async patchServer(serverPath, progressCallback) { const newDomain = this.getNewDomain(); - console.log('=== Server Patcher TEMP SYSTEM NEED TO BE FIXED ==='); + console.log('=== Server Patcher (Pre-patched Download) ==='); console.log(`Target: ${serverPath}`); console.log(`Domain: ${newDomain}`); @@ -546,82 +543,102 @@ class ClientPatcher { // Check if already patched const patchFlagFile = serverPath + '.dualauth_patched'; + let needsRestore = false; + if (fs.existsSync(patchFlagFile)) { try { const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8')); if (flagData.domain === newDomain) { - console.log(`Server already patched for ${newDomain}, skipping`); - if (progressCallback) progressCallback('Server already patched', 100); - return { success: true, alreadyPatched: true }; + // Verify JAR actually contains DualAuth classes (game may have auto-updated) + if (this.serverJarContainsDualAuth(serverPath)) { + console.log(`Server already patched for ${newDomain}, skipping`); + if (progressCallback) progressCallback('Server already patched', 100); + return { success: true, alreadyPatched: true }; + } else { + console.log(' Flag exists but JAR not patched (was auto-updated?), will re-download...'); + // Delete stale flag file + try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ } + } + } else { + console.log(`Server patched for "${flagData.domain}", need to change to "${newDomain}"`); + needsRestore = true; } } catch (e) { // Flag file corrupt, re-patch + console.log(' Flag file corrupt, will re-download'); + try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ } + } + } + + // Restore backup if patched for different domain + if (needsRestore) { + const backupPath = serverPath + '.original'; + if (fs.existsSync(backupPath)) { + if (progressCallback) progressCallback('Restoring original for domain change...', 5); + console.log('Restoring original JAR from backup for re-patching...'); + fs.copyFileSync(backupPath, serverPath); + if (fs.existsSync(patchFlagFile)) { + fs.unlinkSync(patchFlagFile); + } + } else { + console.warn(' No backup found to restore - will download fresh patched JAR'); } } // Create backup if (progressCallback) progressCallback('Creating backup...', 10); console.log('Creating backup...'); - this.backupClient(serverPath); + const backupResult = this.backupClient(serverPath); + if (!backupResult) { + console.warn(' Could not create backup - proceeding without backup'); + } + + // Only support standard domain (auth.sanasol.ws) via pre-patched download + if (newDomain !== 'auth.sanasol.ws' && newDomain !== 'sanasol.ws') { + console.error(`Domain "${newDomain}" requires DualAuthPatcher - only auth.sanasol.ws is supported via pre-patched download`); + return { success: false, error: `Unsupported domain: ${newDomain}. Only auth.sanasol.ws is supported.` }; + } // Download pre-patched JAR if (progressCallback) progressCallback('Downloading patched server JAR...', 30); - console.log('Downloading pre-patched HytaleServer.jar'); + console.log('Downloading pre-patched HytaleServer.jar...'); try { const https = require('https'); const url = 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar'; await new Promise((resolve, reject) => { - https.get(url, (response) => { + const handleResponse = (response) => { if (response.statusCode === 302 || response.statusCode === 301) { - // Follow redirect - https.get(response.headers.location, (redirectResponse) => { - if (redirectResponse.statusCode !== 200) { - reject(new Error(`Failed to download: HTTP ${redirectResponse.statusCode}`)); - return; - } - - const file = fs.createWriteStream(serverPath); - const totalSize = parseInt(redirectResponse.headers['content-length'], 10); - let downloaded = 0; - - redirectResponse.on('data', (chunk) => { - downloaded += chunk.length; - if (progressCallback && totalSize) { - const percent = 30 + Math.floor((downloaded / totalSize) * 60); - progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent); - } - }); - - redirectResponse.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - }).on('error', reject); - } else if (response.statusCode === 200) { - const file = fs.createWriteStream(serverPath); - const totalSize = parseInt(response.headers['content-length'], 10); - let downloaded = 0; - - response.on('data', (chunk) => { - downloaded += chunk.length; - if (progressCallback && totalSize) { - const percent = 30 + Math.floor((downloaded / totalSize) * 60); - progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent); - } - }); - - response.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - } else { - reject(new Error(`Failed to download: HTTP ${response.statusCode}`)); + https.get(response.headers.location, handleResponse).on('error', reject); + return; } - }).on('error', (err) => { + + if (response.statusCode !== 200) { + reject(new Error(`Failed to download: HTTP ${response.statusCode}`)); + return; + } + + const file = fs.createWriteStream(serverPath); + const totalSize = parseInt(response.headers['content-length'], 10); + let downloaded = 0; + + response.on('data', (chunk) => { + downloaded += chunk.length; + if (progressCallback && totalSize) { + const percent = 30 + Math.floor((downloaded / totalSize) * 60); + progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent); + } + }); + + response.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + }; + + https.get(url, handleResponse).on('error', (err) => { fs.unlink(serverPath, () => {}); reject(err); }); @@ -629,12 +646,42 @@ class ClientPatcher { console.log(' Download successful'); + // Verify downloaded JAR size and contents + if (progressCallback) progressCallback('Verifying downloaded JAR...', 95); + + if (!this.validateServerJarSize(serverPath)) { + console.error('Downloaded JAR appears corrupt or incomplete'); + + // Restore backup on verification failure + const backupPath = serverPath + '.original'; + if (fs.existsSync(backupPath)) { + fs.copyFileSync(backupPath, serverPath); + console.log('Restored backup after verification failure'); + } + + return { success: false, error: 'Downloaded JAR verification failed - file too small (corrupt/partial download)' }; + } + + if (!this.serverJarContainsDualAuth(serverPath)) { + console.error('Downloaded JAR does not contain DualAuth classes - invalid or corrupt download'); + + // Restore backup on verification failure + const backupPath = serverPath + '.original'; + if (fs.existsSync(backupPath)) { + fs.copyFileSync(backupPath, serverPath); + console.log('Restored backup after verification failure'); + } + + return { success: false, error: 'Downloaded JAR verification failed - missing DualAuth classes' }; + } + console.log(' Verification successful - DualAuth classes present'); + // Mark as patched fs.writeFileSync(patchFlagFile, JSON.stringify({ domain: newDomain, patchedAt: new Date().toISOString(), patcher: 'PrePatchedDownload', - source: 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar' + source: 'https://download.sanasol.ws/download/HytaleServer.jar' })); if (progressCallback) progressCallback('Server patching complete', 100); @@ -643,7 +690,7 @@ class ClientPatcher { } catch (downloadError) { console.error(`Failed to download patched JAR: ${downloadError.message}`); - + // Restore backup on failure const backupPath = serverPath + '.original'; if (fs.existsSync(backupPath)) { @@ -656,290 +703,7 @@ class ClientPatcher { } /** - * Find Java executable - uses bundled JRE first (same as game uses) - * Falls back to system Java if bundled not available - */ - findJava() { - // 1. Try bundled JRE first (comes with the game) - try { - const bundled = getBundledJavaPath(JRE_DIR); - if (bundled && fs.existsSync(bundled)) { - console.log(`Using bundled Java: ${bundled}`); - return bundled; - } - } catch (e) { - // Bundled not available - } - - // 2. Try javaManager's getJavaExec (handles all fallbacks) - try { - const javaExec = getJavaExec(JRE_DIR); - if (javaExec && fs.existsSync(javaExec)) { - console.log(`Using Java from javaManager: ${javaExec}`); - return javaExec; - } - } catch (e) { - // Not available - } - - // 3. Check JAVA_HOME - if (process.env.JAVA_HOME) { - const javaHome = process.env.JAVA_HOME; - const javaBin = path.join(javaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java'); - if (fs.existsSync(javaBin)) { - console.log(`Using Java from JAVA_HOME: ${javaBin}`); - return javaBin; - } - } - - // 4. Try 'java' from PATH - try { - execSync('java -version 2>&1', { encoding: 'utf8' }); - console.log('Using Java from PATH'); - return 'java'; - } catch (e) { - // Not in PATH - } - - return null; - } - - /** - * Download DualAuthPatcher from hytale-auth-server if not present - */ - async ensurePatcherDownloaded(patcherDir) { - const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java'); - const patcherUrl = 'https://raw.githubusercontent.com/sanasol/hytale-auth-server/master/patcher/DualAuthPatcher.java'; - - if (!fs.existsSync(patcherDir)) { - fs.mkdirSync(patcherDir, { recursive: true }); - } - - if (!fs.existsSync(patcherJava)) { - console.log('Downloading DualAuthPatcher from hytale-auth-server...'); - try { - const https = require('https'); - await new Promise((resolve, reject) => { - const file = fs.createWriteStream(patcherJava); - https.get(patcherUrl, (response) => { - if (response.statusCode === 302 || response.statusCode === 301) { - // Follow redirect - https.get(response.headers.location, (redirectResponse) => { - redirectResponse.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - }).on('error', reject); - } else { - response.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - } - }).on('error', (err) => { - fs.unlink(patcherJava, () => {}); - reject(err); - }); - }); - console.log(' Downloaded DualAuthPatcher.java'); - } catch (e) { - console.error(` Failed to download DualAuthPatcher: ${e.message}`); - throw e; - } - } - } - - /** - * Download ASM libraries if not present - */ - async ensureAsmLibraries(libDir) { - if (!fs.existsSync(libDir)) { - fs.mkdirSync(libDir, { recursive: true }); - } - - const libs = [ - { name: 'asm-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar' }, - { name: 'asm-tree-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar' }, - { name: 'asm-util-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-util/9.6/asm-util-9.6.jar' } - ]; - - for (const lib of libs) { - const libPath = path.join(libDir, lib.name); - if (!fs.existsSync(libPath)) { - console.log(`Downloading ${lib.name}...`); - try { - const https = require('https'); - await new Promise((resolve, reject) => { - const file = fs.createWriteStream(libPath); - https.get(lib.url, (response) => { - response.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - }).on('error', (err) => { - fs.unlink(libPath, () => {}); - reject(err); - }); - }); - console.log(` Downloaded ${lib.name}`); - } catch (e) { - console.error(` Failed to download ${lib.name}: ${e.message}`); - throw e; - } - } - } - } - - /** - * Compile DualAuthPatcher if needed - */ - async compileDualAuthPatcher(java, patcherDir, libDir) { - const patcherClass = path.join(patcherDir, 'DualAuthPatcher.class'); - const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java'); - - // Check if already compiled and up to date - if (fs.existsSync(patcherClass)) { - const classTime = fs.statSync(patcherClass).mtime; - const javaTime = fs.statSync(patcherJava).mtime; - if (classTime > javaTime) { - console.log('DualAuthPatcher already compiled'); - return { success: true }; - } - } - - console.log('Compiling DualAuthPatcher...'); - - const javac = java.replace(/java(\.exe)?$/, 'javac$1'); - const classpath = [ - path.join(libDir, 'asm-9.6.jar'), - path.join(libDir, 'asm-tree-9.6.jar'), - path.join(libDir, 'asm-util-9.6.jar') - ].join(process.platform === 'win32' ? ';' : ':'); - - try { - // Fix PATH for packaged Electron apps on Windows - const execOptions = { - stdio: 'pipe', - cwd: patcherDir, - env: { ...process.env } - }; - - // Add system32 to PATH for Windows to find cmd.exe - if (process.platform === 'win32') { - const systemRoot = process.env.SystemRoot || 'C:\\WINDOWS'; - const systemPath = `${systemRoot}\\system32;${systemRoot};${systemRoot}\\System32\\Wbem`; - execOptions.env.PATH = execOptions.env.PATH - ? `${systemPath};${execOptions.env.PATH}` - : systemPath; - execOptions.shell = true; - } - - execSync(`"${javac}" -cp "${classpath}" -d "${patcherDir}" "${patcherJava}"`, execOptions); - console.log(' Compilation successful'); - return { success: true }; - } catch (e) { - const error = `Failed to compile DualAuthPatcher: ${e.message}`; - console.error(error); - if (e.stderr) console.error(e.stderr.toString()); - return { success: false, error }; - } - } - - /** - * Run DualAuthPatcher on the server JAR - */ - async runDualAuthPatcher(java, classpath, serverPath, domain) { - return new Promise((resolve) => { - const args = ['-cp', classpath, 'DualAuthPatcher', serverPath]; - const env = { ...process.env, HYTALE_AUTH_DOMAIN: domain }; - - console.log(`Running: java ${args.join(' ')}`); - console.log(` HYTALE_AUTH_DOMAIN=${domain}`); - - const proc = spawn(java, args, { env, stdio: ['pipe', 'pipe', 'pipe'] }); - - let stdout = ''; - let stderr = ''; - - proc.stdout.on('data', (data) => { - const str = data.toString(); - stdout += str; - console.log(str.trim()); - }); - - proc.stderr.on('data', (data) => { - const str = data.toString(); - stderr += str; - console.error(str.trim()); - }); - - proc.on('close', (code) => { - if (code === 0) { - resolve({ success: true, stdout }); - } else { - resolve({ success: false, error: `Patcher exited with code ${code}: ${stderr}` }); - } - }); - - proc.on('error', (err) => { - resolve({ success: false, error: `Failed to run patcher: ${err.message}` }); - }); - }); - } - - /** - * Legacy server patcher (simple domain replacement, no dual auth) - * Use patchServer() for full dual auth support - */ - async patchServerLegacy(serverPath, progressCallback) { - const newDomain = this.getNewDomain(); - const strategy = this.getDomainStrategy(newDomain); - - console.log('=== Legacy Server Patcher ==='); - console.log(`Target: ${serverPath}`); - console.log(`Domain: ${newDomain} (${newDomain.length} chars)`); - - if (!fs.existsSync(serverPath)) { - return { success: false, error: `Server JAR not found: ${serverPath}` }; - } - - if (progressCallback) progressCallback('Patching server...', 20); - - console.log('Opening server JAR...'); - const zip = new AdmZip(serverPath); - const entries = zip.getEntries(); - - let totalCount = 0; - const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN); - - for (const entry of entries) { - const name = entry.entryName; - if (name.endsWith('.class') || name.endsWith('.properties') || - name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) { - const data = entry.getData(); - if (data.includes(oldUtf8)) { - const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, strategy.mainDomain); - if (count > 0) { - zip.updateFile(entry.entryName, patchedData); - totalCount += count; - } - } - } - } - - if (totalCount > 0) { - zip.writeZip(serverPath); - } - - if (progressCallback) progressCallback('Complete', 100); - return { success: true, patchCount: totalCount }; - } - - /** - * Find the client binary path based on platform + * Find client binary path based on platform */ findClientPath(gameDir) { const candidates = []; @@ -961,7 +725,9 @@ class ClientPatcher { return null; } - + /** + * Find server JAR path + */ findServerPath(gameDir) { const candidates = [ path.join(gameDir, 'Server', 'HytaleServer.jar'), @@ -978,9 +744,6 @@ class ClientPatcher { /** * Ensure both client and server are patched before launching - * @param {string} gameDir - Path to the game directory - * @param {function} progressCallback - Optional callback for progress updates - * @param {string} javaPath - Optional path to Java executable for server patching */ async ensureClientPatched(gameDir, progressCallback, javaPath = null) { const results = { @@ -991,9 +754,7 @@ class ClientPatcher { const clientPath = this.findClientPath(gameDir); if (clientPath) { - if (progressCallback) { - progressCallback('Patching client binary...', 10); - } + if (progressCallback) progressCallback('Patching client binary...', 10); results.client = await this.patchClient(clientPath, (msg, pct) => { if (progressCallback) { progressCallback(`Client: ${msg}`, pct ? pct / 2 : null); @@ -1006,14 +767,12 @@ class ClientPatcher { const serverPath = this.findServerPath(gameDir); if (serverPath) { - if (progressCallback) { - progressCallback('Patching server JAR...', 50); - } + if (progressCallback) progressCallback('Patching server JAR...', 50); results.server = await this.patchServer(serverPath, (msg, pct) => { if (progressCallback) { progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null); } - }, javaPath); + }); } else { console.warn('Could not find HytaleServer.jar'); results.server = { success: false, error: 'Server JAR not found' }; @@ -1023,9 +782,7 @@ class ClientPatcher { results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched); results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0); - if (progressCallback) { - progressCallback('Patching complete', 100); - } + if (progressCallback) progressCallback('Patching complete', 100); return results; } diff --git a/main.js b/main.js index 0aad11f..3f9f04b 100644 --- a/main.js +++ b/main.js @@ -176,7 +176,8 @@ function createWindow() { initDiscordRPC(); // Configure and initialize electron-updater - autoUpdater.autoDownload = false; + // Enable auto-download so updates start immediately when available + autoUpdater.autoDownload = true; autoUpdater.autoInstallOnAppQuit = true; autoUpdater.on('checking-for-update', () => { @@ -201,6 +202,20 @@ function createWindow() { autoUpdater.on('error', (err) => { console.error('Error in auto-updater:', err); + + // Handle macOS code signing errors - requires manual download + if (mainWindow && !mainWindow.isDestroyed()) { + const isMacSigningError = process.platform === 'darwin' && + (err.code === 'ERR_UPDATER_INVALID_SIGNATURE' || + err.message.includes('signature') || + err.message.includes('code sign')); + + mainWindow.webContents.send('update-error', { + message: err.message, + isMacSigningError: isMacSigningError, + requiresManualDownload: isMacSigningError || process.platform === 'darwin' + }); + } }); autoUpdater.on('download-progress', (progressObj) => { @@ -218,7 +233,10 @@ function createWindow() { console.log('Update downloaded:', info.version); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('update-downloaded', { - version: info.version + version: info.version, + platform: process.platform, + // macOS auto-install often fails on unsigned apps + autoInstallSupported: process.platform !== 'darwin' }); } }); @@ -859,6 +877,17 @@ ipcMain.handle('open-external', async (event, url) => { } }); +ipcMain.handle('open-download-page', async () => { + try { + // Open GitHub releases page for manual download + await shell.openExternal('https://github.com/amiayweb/Hytale-F2P/releases/latest'); + return { success: true }; + } catch (error) { + console.error('Failed to open download page:', error); + return { success: false, error: error.message }; + } +}); + ipcMain.handle('open-game-location', async () => { try { const { getResolvedAppDir, loadVersionBranch } = require('./backend/launcher'); @@ -1086,8 +1115,37 @@ ipcMain.handle('download-update', async () => { } }); -ipcMain.handle('install-update', () => { - autoUpdater.quitAndInstall(false, true); +ipcMain.handle('install-update', async () => { + console.log('[AutoUpdater] Installing update...'); + + // On macOS, quitAndInstall often fails silently + // Use a more aggressive approach + if (process.platform === 'darwin') { + console.log('[AutoUpdater] macOS detected, using force quit approach'); + // Give user feedback that something is happening + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('update-installing'); + } + + // Small delay to show the "Installing..." state + await new Promise(resolve => setTimeout(resolve, 500)); + + try { + autoUpdater.quitAndInstall(false, true); + } catch (err) { + console.error('[AutoUpdater] quitAndInstall failed:', err); + // Force quit the app - the update should install on next launch + app.exit(0); + } + + // If quitAndInstall didn't work, force exit after a delay + setTimeout(() => { + console.log('[AutoUpdater] Force exiting app...'); + app.exit(0); + }, 2000); + } else { + autoUpdater.quitAndInstall(false, true); + } }); ipcMain.handle('get-launcher-version', () => { diff --git a/preload.js b/preload.js index d79ca99..077f2ac 100644 --- a/preload.js +++ b/preload.js @@ -24,7 +24,7 @@ contextBridge.exposeInMainWorld('electronAPI', { saveCloseLauncher: (enabled) => ipcRenderer.invoke('save-close-launcher', enabled), loadCloseLauncher: () => ipcRenderer.invoke('load-close-launcher'), - // Harwadre Acceleration + // Hardware Acceleration saveLauncherHardwareAcceleration: (enabled) => ipcRenderer.invoke('save-launcher-hw-accel', enabled), loadLauncherHardwareAcceleration: () => ipcRenderer.invoke('load-launcher-hw-accel'), @@ -50,14 +50,7 @@ contextBridge.exposeInMainWorld('electronAPI', { selectModFiles: () => ipcRenderer.invoke('select-mod-files'), copyModFile: (sourcePath, modsPath) => ipcRenderer.invoke('copy-mod-file', sourcePath, modsPath), onProgressUpdate: (callback) => { - ipcRenderer.on('progress-update', (event, data) => { - // Ensure data includes retry state if available - if (data && typeof data === 'object') { - callback(data); - } else { - callback(data); - } - }); + ipcRenderer.on('progress-update', (event, data) => callback(data)); }, onProgressComplete: (callback) => { ipcRenderer.on('progress-complete', () => callback()); @@ -69,7 +62,6 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.on('installation-end', () => callback()); }, getUserId: () => ipcRenderer.invoke('get-user-id'), - checkForUpdates: () => ipcRenderer.invoke('check-for-updates'), openDownloadPage: () => ipcRenderer.invoke('open-download-page'), getUpdateInfo: () => ipcRenderer.invoke('get-update-info'), onUpdatePopup: (callback) => { @@ -126,6 +118,7 @@ contextBridge.exposeInMainWorld('electronAPI', { checkForUpdates: () => ipcRenderer.invoke('check-for-updates'), downloadUpdate: () => ipcRenderer.invoke('download-update'), installUpdate: () => ipcRenderer.invoke('install-update'), + quitAndInstallUpdate: () => ipcRenderer.invoke('install-update'), // Alias for update.js compatibility getLauncherVersion: () => ipcRenderer.invoke('get-launcher-version'), onUpdateAvailable: (callback) => { ipcRenderer.on('update-available', (event, data) => callback(data)); From e7110936d8017a3fdc44dcc65b586d19674f555b Mon Sep 17 00:00:00 2001 From: Zakhar Smokotov Date: Wed, 28 Jan 2026 16:27:48 +0300 Subject: [PATCH 37/88] Add Russian language support Added Russian (ru) to the list of available languages. --- GUI/js/i18n.js | 3 +- GUI/locales/ru.json | 250 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 3 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 GUI/locales/ru.json diff --git a/GUI/js/i18n.js b/GUI/js/i18n.js index 6b93a8d..d69e186 100644 --- a/GUI/js/i18n.js +++ b/GUI/js/i18n.js @@ -10,7 +10,8 @@ const i18n = (() => { { code: 'es-ES', name: 'Español (España)' }, { code: 'pt-BR', name: 'Portuguese (Brazil)' }, { code: 'tr-TR', name: 'Turkish (Turkey)' }, - { code: 'pl-PL', name: 'Polish (Poland)' } + { code: 'pl-PL', name: 'Polish (Poland)' }, + { code: 'ru', name: 'Русский' } ]; // Load single language file diff --git a/GUI/locales/ru.json b/GUI/locales/ru.json new file mode 100644 index 0000000..9174ce2 --- /dev/null +++ b/GUI/locales/ru.json @@ -0,0 +1,250 @@ +{ + "nav": { + "play": "Играть", + "mods": "Моды", + "news": "Новости", + "chat": "Чат игроков", + "settings": "Настройки" + }, + "header": { + "playersLabel": "Игроки:", + "manageProfiles": "Управлять профилями:", + "defaultProfile": "По умолчанию" + }, + "install": { + "title": "FREE TO PLAY LAUNCHER", + "playerName": "Ник игрока", + "playerNamePlaceholder": "Введите ваш ник", + "gameBranch": "Версия игры", + "releaseVersion": "Релиз (Стабильная)", + "preReleaseVersion": "Бета (Эксперементально)", + "customInstallation": "Модифицированная установка", + "installationFolder": "Папка установки", + "pathPlaceholder": "Путь по умолчанию", + "browse": "Browse", + "installButton": "УСТАНОВИТЬ HYTALE", + "installing": "УСТАНОВКА..." + }, + "play": { + "ready": "ГОТОВ К ИГРЕ", + "subtitle": "Запусти Hytale и приготовься к приключению!", + "playButton": "ЗАПУСТИТЬ HYTALE", + "latestNews": "ПОСЛЕДНИЕ НОВОСТИ", + "viewAll": "ПОСМОТРЕТЬ ВСЕ", + "checking": "ПРОВЕРКА...", + "play": "ЗАПУСТИТЬ" + }, + "mods": { + "searchPlaceholder": "Искать моды...", + "myMods": "Мои моды", + "previous": "Предыдущая", + "next": "Вперед", + "page": "Страница", + "of": "", + "modalTitle": "МОИ МОДЫ", + "noModsFound": "Моды не найдены", + "noModsFoundDesc": "Попробуйте изменить свой запрос", + "noModsInstalled": "Нет установленных модов", + "noModsInstalledDesc": "Добавьте моды с CurseForge или импортируйте свои!", + "view": "Посмотреть", + "install": "Установить", + "installed": "УСТАНОВЛЕННЫЕ", + "enable": "ВКЛЮЧИТЬ", + "disable": "ВЫКЛЮЧИТЬ", + "active": "ВКЛЮЧЕН", + "disabled": "ВЫКЛЮЧЕН", + "delete": "Удалить мод", + "noDescription": "Нет доступного описания", + "confirmDelete": "Вы точно уверены, что хотите удалить \"{name}\"?", + "confirmDeleteDesc": "Это действие не отменить.", + "confirmDeletion": "Подтвердите удаление", + "apiKeyRequired": "Требуется ключ API", + "apiKeyRequiredDesc": "Ключ CurseForge API требуется для просмотра модов" + }, + "news": { + "title": "ВСЕ НОВОСТИ", + "readMore": "Читать дальше" + }, + "chat": { + "title": "ЧАТ ИГРОКОВ", + "pickColor": "Цвет", + "inputPlaceholder": "Введите свое сообщение...", + "send": "Отправить", + "online": "онлайн", + "charCounter": "{current}/{max}", + "secureChat": "Безопасный чат - все ссылки зацензурены", + "joinChat": "Присоединиться к чату", + "chooseUsername": "Выберите имя пользователя для входа в чат игроков", + "username": "Ник", + "usernamePlaceholder": "Введите ваш ник...", + "usernameHint": "3-20 символов, букв, цифр, только - и _", + "joinButton": "Присоединиться к чату", + "colorModal": { + "title": "Выберите цвет ника", + "chooseSolid": "Choose a solid color:", + "customColor": "Модифицированный цвет:", + "preview": "Preview:", + "previewUsername": "Ник", + "apply": "Применить цвет" + } + }, + "settings": { + "title": "НАСТРОЙКИ", + "java": "Рантайм JAVA", + "useCustomJava": "Укажите свой путь Java", + "javaDescription": "Override the bundled Java runtime with your own installation", + "javaPath": "Путь исполняемого файла Java", + "javaPathPlaceholder": "Выберите путь Java...", + "javaBrowse": "Обзор", + "javaHint": "Выберите папку установки Java (поддерживается Windows, Mac, Linux)", + "discord": "Интеграция Discord", + "enableRPC": "Включить Discord Rich Presence", + "discordDescription": "Показывать вашу активность лаунчера в Discord", + "game": "Настройки игры", + "playerName": "Ник игрока", + "playerNamePlaceholder": "Введите ваш ник", + "playerNameHint": "Этот ник будет использован в игре (1-16 символов)", + "openGameLocation": "Открыть местоположение игры", + "openGameLocationDesc": "Открыть папку установки игры", + "account": "Управление UUID игрока", + "currentUUID": "Текущий UUID", + "uuidPlaceholder": "Загрузка UUID...", + "copyUUID": "Копировать UUID", + "regenerateUUID": "Перегенерировать UUID", + "uuidHint": "Уникальный идентификатор игрока для этого ника", + "manageUUIDs": "Управление всеми UUID", + "manageUUIDsDesc": "Смотреть и управлять всеми UUID игрока", + "language": "Язык", + "selectLanguage": "Выберите язык", + "repairGame": "Починить игру", + "reinstallGame": "Переустановить файлы игры (сохраняет данные)", + "gpuPreference": "Предпочтение GPU", + "gpuHint": "Выберите ваш предпочитаемый GPU (Linux: affects DRI_PRIME)", + "gpuAuto": "Автоматический выбор", + "gpuIntegrated": "Интегрированная видеокарта", + "gpuDedicated": "Дискретная видеокарта", + "logs": "ЛОГИ", + "logsCopy": "Копировать", + "logsRefresh": "Обновить", + "logsFolder": "Открыть папку", + "logsLoading": "Загрузка логов...", + "closeLauncher": "Поведение лаунчера", + "closeOnStart": "Закрыть лаунчер при старте игры", + "closeOnStartDescription": "Автоматически закрыть лаунчер после запуска Hytale", + "hwAccel": "Аппаратное ускорение", + "hwAccelDescription": "Включить аппаратное ускорение для лаунчера", + "gameBranch": "Ветка игры", + "branchRelease": "Релиз", + "branchPreRelease": "Бета", + "branchHint": "Переключает между релизом и бетой игры", + "branchWarning": "Изменение ветки скачает и установит другую версию игры", + "branchSwitching": "Переключение на {branch}...", + "branchSwitched": "Переключение на {branch} выполнено успешно!", + "installRequired": "Необходима установка", + "branchInstallConfirm": "Игры будет установлена для ветки {branch}. Продолжить?" + }, + "uuid": { + "modalTitle": "Управление UUID", + "currentUserUUID": "UUID текущего пользователя", + "allPlayerUUIDs": "UUID всех игроков", + "generateNew": "Сгенерировать новый UUID", + "loadingUUIDs": "Загрузка UUID...", + "setCustomUUID": "Установить кастомный UUID", + "customPlaceholder": "Ввести кастомный UUID (форматы: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)", + "setUUID": "Установить UUID", + "warning": "Внимание! Установка кастомного UUID изменит вашу текущую личность игрока!", + "copyTooltip": "Скопировать UUID", + "regenerateTooltip": "Сгенерировать новый UUID" + }, + "profiles": { + "modalTitle": "Управление профилями", + "newProfilePlaceholder": "Новое имя профиля", + "createProfile": "Создать профиль" + }, + "discord": { + "notificationText": "Join our Discord community!", + "joinButton": "Join Discord" + }, + "common": { + "confirm": "Подтвердить", + "cancel": "Отменить", + "save": "Сохранить", + "close": "Закрыть", + "delete": "Удалить", + "edit": "Редактировать", + "loading": "Загружается...", + "apply": "Применить", + "install": "Установить" + }, + "notifications": { + "gameDataNotFound": "Ошибка: данные игры не найдены", + "gameUpdatedSuccess": "Игра успешно обновлена! Ура! 🎉", + "updateFailed": "Обновление прервалось с ошибкой: {error}", + "updateError": "Ошибка обновления: {error}", + "discordEnabled": "Discord Rich Presence включен", + "discordDisabled": "Discord Rich Presence выключен", + "discordSaveFailed": "Не удалось сохранить настройку Discord", + "playerNameRequired": "Please enter a valid player name", + "playerNameSaved": "Имя игрока успешно сохранено!", + "playerNameSaveFailed": "Не удалось сохранить имя игрока", + "uuidCopied": "UUID скопирован в буфер обмена!", + "uuidCopyFailed": "Не удалось скопировать UUID", + "uuidRegenNotAvailable": "UUID перегенерация к сожалению не доступна", + "uuidRegenFailed": "Не удалось перегенерировать UUID", + "uuidGenerated": "Новый UUID сгенерирован успешно!", + "uuidGeneratedShort": "Новый UUID сгенерирован!", + "uuidGenerateFailed": "Не получилось сгенерировать новый UUID", + "uuidRequired": "Пожалуйста введите UUID", + "uuidInvalidFormat": "Неправильный формат UUID", + "uuidSetFailed": "Не удалось поставить кастомный UUID", + "uuidSetSuccess": "Кастомный UUID успешно установлен!", + "uuidDeleteFailed": "Не удалось удалить UUID", + "uuidDeleteSuccess": "Удаление UUID успешно!", + "modsDownloading": "Скачивание {name}...", + "modsTogglingMod": "Включение мода...", + "modsDeletingMod": "Удаление мода...", + "modsLoadingMods": "Загрузка модов с CurseForge...", + "modsInstalledSuccess": "{name} успешно установлен! 🎉", + "modsDeletedSuccess": "{name} удален успешно!", + "modsDownloadFailed": "Не получилось скачать мод: {error}", + "modsToggleFailed": "Не получилось включить мод: {error}", + "modsDeleteFailed": "Не получилось удалить мод: {error}", + "modsModNotFound": "Mod information not found", + "hwAccelSaved": "Настройка аппаратного ускорения сохранена!", + "hwAccelSaveFailed": "Не удалось сохранить настройку аппаратного ускорения" + }, + "confirm": { + "defaultTitle": "Подтвердить действие", + "regenerateUuidTitle": "Сгенерировать новый UUID", + "regenerateUuidMessage": "Вы уверены, что хотите сгенерировать новый UUID? Генерация новго UUID изменит вашу текущую личность игрока!", + "regenerateUuidButton": "Сгенерировать", + "setCustomUuidTitle": "Установить кастомный UUID", + "setCustomUuidMessage": "Вы уверены, что хотите установить кастомный UUID? Установка кастомного UUID изменит вашу текущую личность игрока!", + "setCustomUuidButton": "Установить UUID", + "deleteUuidTitle": "Удалить UUID", + "deleteUuidMessage": "Вы уверены, что хотите удалить UUID для \"{username}\"? Это действие необратимо!", + "deleteUuidButton": "Удалить", + "uninstallGameTitle": "Удалить игру", + "uninstallGameMessage": "Вы уверены, что хотите удалить Hytale? Все данные игры будут безвозвратно удалены!", + "uninstallGameButton": "Удалить" + }, + "progress": { + "initializing": "Инициализация...", + "downloading": "Скачивание...", + "installing": "Установка...", + "extracting": "Извлечение...", + "verifying": "Сверка...", + "switchingProfile": "Смена профиля...", + "profileSwitched": "Профиль сменен!", + "startingGame": "Запуск игры...", + "launching": "ЗАПУСК...", + "uninstallingGame": "Удаление игры...", + "gameUninstalled": "Игра успешно удалена!", + "uninstallFailed": "Удаление игры не удалось: {error}", + "startingUpdate": "Starting mandatory game update...", + "installationComplete": "Установка успешно завершена!", + "installationFailed": "Установка не удалась: {error}", + "installingGameFiles": "Установка файлов игры...", + "installComplete": "Установка завершена!" + } +} \ No newline at end of file diff --git a/README.md b/README.md index ec04a4c..4943cf2 100644 --- a/README.md +++ b/README.md @@ -415,6 +415,7 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions. - [**@Citeli-py**](https://github.com/Citeli-py) - *Issues fixer* - [**@Rahul-Sahani04**](https://github.com/Rahul-Sahani04) - *Issues fixer* - [**@xSamiVS**](https://github.com/xSamiVS) - *Language Translator* +- [**@BlackSystemCoder**](https://github.com/BlackSystemCoder) - *Language Translator* --- From 9cf504bbccd98451d028c216071b486fe17c7f59 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Wed, 28 Jan 2026 23:41:27 +0800 Subject: [PATCH 38/88] chore: drafting documentation on SERVER.md --- SERVER.md | 139 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 105 insertions(+), 34 deletions(-) diff --git a/SERVER.md b/SERVER.md index 67e7f29..0dbb27c 100644 --- a/SERVER.md +++ b/SERVER.md @@ -1,34 +1,79 @@ -# Hytale F2P Server Guide +# 🎮 Hytale F2P Server Guide Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup. -DOWNLOAD SERVER FILES HERE: https://discord.gg/MEyWUxt77m +### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/MEyWUxt77m** + +**Table of Contents** + +* [A. Host your Singleplayer World](SERVER.md#1-host-your-singleplayer-world-to-your-friends-online-play-feature) + * [1. Using In-Game Invite Code / Online Play Feature]() + * [Common Issues (UPnP/NAT/STUN) on Online Play](SERVER.md#common-issues-upnpnatstun-on-online-play) + * [2. Using Playit.gg \[Recommended\]]() + * [3. Using Radmin VPN]() +* [B. Dedicated Server] + * [1. ] +--- + +### "Server" Term and Definiton + +"HytaleServer.jar", which called as "Server", functions as the place of authentication of the client that supposed to go to Hytale Official Authentication System but we managed our way to redirect it on our service (sanasol.ws), handling approximately thousands of users to play this game for free to worldwide players. + +Kindly support us via [our Buy Me a Coffee link](https://buymeacoffee.com/hf2p) if you think our launcher took a big part of developing this Hytale community for the love of the game itself. +**We will always advertise, always pushing, and always asking, to every users of this launcher to purchase the original game to help the official development of Hytale**. + +### Server Directory Location + +Here are the directory locations of Server folder if you have installed +- **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server` +- **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server` +- **Linux:** `~/.hytalef2p/release/package/game/latest/Server` + +> [!NOTE] +> This location only exists if the user installed the game using our launcher. The `Server` folder needed to auth the HytaleClient to play Hytale online +> (for now; we planned to add offline mode in later version of our launcher). + +> [!IMPORTANT] +> Hosting a dedicated Hytale server will not need the exact similar tree. You can put it anywhere, as long as the directory has `Assets.zip` which +> can be acquired from our launcher via our `HytaleServer.rar` server file (which contains patched `HytaleServer.jar`, `Assets.zip`, and `run_server` scripts in `.sh & .bat`. --- -## Part 1: Playing with Friends (Online Play) +# Host + +## 1 - Host your Singleplayer World to your friends (Online Play Feature) The easiest way to play with friends - no manual server setup required! +*The game automatically handles networking using UPnP/STUN/NAT traversal.* -### How It Works - -1. **Start the game** via F2P Launcher -2. **Click "Online Play"** in the main menu -3. **Share the invite code** with your friends -4. Friends enter your invite code to join - -The game automatically handles networking using UPnP/STUN/NAT traversal. - -### Network Requirements - -For Online Play to work, you need: - +**For Online Play to work, you need:** - **UPnP enabled** on your router (most routers have this on by default) - **Public IP address** from your ISP (not behind CGNAT) -### Common Issues +> [!TIP] +> Hoster need to make sure that the router can use UPnP: read router docs, wiki, or watch Youtube tutorials. +> +> If you encounter any problem, check Common Issues section below -#### "NAT Type: Carrier-Grade NAT (CGNAT)" Warning +1. Press **Worlds** on the Main Menu. +2. Select which world you want to play with your friend. +3. Once you get in the world, press **ESC**/Pause the game. +4. Press **Online Play** in the Pause Menu. +5. Set option "Allow Other Players to Join" from OFF to **ON**. You can set Password if you want. +6. Press **Save**, the Invite Code will appear. +7. Press **Copy to Clipboard** and **Share the Invite Code** to your friends! +8. Friends: Press **Servers** in the Main Menu > Press **Join via Code** > Paste the Code > Join. + +> [!WARNING] +> If other players can't join the Hoster with error: `Failed to connect to any available address. The host may be offline or behind a strict firewall.` +> +> **AND ALSO** the Hoster "Online Play" menu shows `Connected to STUN - NAT Type: Restricted (No UPnP)`, +> +> this means the Online Play is **unavailable** on the Hoster machine, and it is neccessary to use services to host your world. **We recommend Playit.gg!** + + +### Common Issues (UPnP/NAT/STUN) on Online Play +
1) "NAT Type: Carrier-Grade NAT (CGNAT)" Warning If you see this message: ``` @@ -40,14 +85,13 @@ Warning: Your network configuration may prevent other players from connecting. **What this means:** Your ISP doesn't give you a public IP address. Multiple customers share one public IP, which blocks incoming connections. **Solutions:** - 1. **Contact your ISP** - Request a public/static IP address (may cost extra) 2. **Use a VPN with port forwarding** - Services like Mullvad, PIA, or AirVPN offer this -3. **Use Radmin VPN or Playit.gg** - Create a virtual LAN with friends (see below) +3. **Use Playit.gg / Tailscale / Radmin VPN** - Create a virtual LAN with friends (see below) 4. **Have a friend with public IP host instead** +
-#### "UPnP Failed" or "Port Mapping Failed" - +
2) "UPnP Failed" or "Port Mapping Failed" Warning **Check your router:** 1. Log into router admin panel (usually `192.168.1.1` or `192.168.0.1`) 2. Find UPnP settings (often under "Advanced" or "NAT") @@ -56,23 +100,49 @@ Warning: Your network configuration may prevent other players from connecting. **If UPnP isn't available:** - Manually forward **port 5520 UDP** to your computer's local IP -- See "Port Forwarding" section below - -#### "Strict NAT" or "Symmetric NAT" +- See "Port Forwarding" or "Workarounds or NAT/CGNAT" sections below +
+
3) "Strict NAT" or "Symmetric NAT" Warning Some routers have restrictive NAT that blocks peer connections. **Try:** 1. Enable "NAT Passthrough" or "NAT Filtering: Open" in router settings 2. Put your device in router's DMZ (temporary test only) -3. Use Radmin VPN as workaround +3. Use Playit.gg / Tailscale / Radmin VPN as workaround +
### Workarounds for NAT/CGNAT Issues -#### Option 1: playit.gg (Recommended) +#### Option 1: Playit.gg (Recommended) ✔️ Free tunneling service - only the host needs to install it: +1. Go to https://playit.gg/login and **Log In** with your existing account, **Create Account** if you don't have one +2. Press "Add a tunnel" > Select `UDP` > Tunnel description of `Hytale Server` > Port count `1` > and Local Port `5520` +3. Press **Start the tunnel** (or you can just run the Playit.gg.EXE if you already installed it on your machine) - You'll get a public address like `xx-xx.gl.at.ply.gg:5520` +4. Go to https://playit.gg/download : `Installer` (Windows) or `x86-64` (Linux) or follow `Debian Install Script` (Debian-based only) + * Windows: Install the `playit-windows.msi` + * Linux: + * Right-click file > Properties > Turn on 'Executable as a Program' | or just do `chmod +x playit-linux-amd64` on terminal + * Run by double-clicking the file or `./playit-linux-amd64` via terminal +5. Open Playit.gg > Copy (select the URL, then Right-Click | `Ctrl+Shift+C` for Linux) > Paste the prompted URL into your browser to link your created account +6. **WARNING: Do not close the terminal if you are still playing or hosting the server** +7. +8. Now you can use the public address that written in the playit.gg exe/you can check via browser [look at step 3] +9. Download the `run_server_with_tokens` script file (`.BAT` for Windows, `.SH` for Linux) from our Discord server > channel `#open-public-server` +10. Put the script file to the `Server` folder in `HytaleF2P` directory (`%localappdata%\HytaleF2P\release\package\game\latest\Server`) +11. Copy the `Assets.zip` from the `%localappdata%\HytaleF2P\release\package\game\latest\` folder to the `Server\` folder +12. double-click the .BAT file to host your server, wait until it shows like +``` +=================================================== +Hytale Server Booted! [Multiplayer, Fresh Universe] +=================================================== +``` +12. You connect to the server by go to `Servers` in your game client, and add server, type `localhost` in the address box, use any name for your server, `my server` for example. +13. Send the public address in step 3 to your friends, use `add server` also. +14. enjoy :smile: + 1. **Download [playit.gg](https://playit.gg/)** and run it - Connect your account from the terminal (do not close it when playing on the server) 2. **Add a tunnel** - Select "UDP", tunnel description of "Hytale Server", port count `1`, and local port `5520` 3. **Start the tunnel** - You'll get a public address like `xx-xx.gl.at.ply.gg:5520` @@ -98,13 +168,13 @@ It creates mesh VPN service that streamlines connecting devices and services sec [Once installed, Tailwind starts and live inside your hidden icon section in Windows, Mac and Linux] 2. Create a **common tailscale** account which will shared among your friends to log in. 3. Ask your **host to login in to thier tailscale client first**, then the other members. -##### Host - 1. Open your singleplayer world - 2. Go to Online Play settings - 3. Re-save your settings to generate a new share code -##### Friends - 1. Ensure Tailscale is connected -2. Use the new share code to connect + * Host + * Open your singleplayer world + * Go to Online Play settings + * Re-save your settings to generate a new share code + * Friends + * Ensure Tailscale is connected + * Use the new share code to connect [To test your connection, ping the host's ipv4 mentioned in tailwind] --- @@ -457,3 +527,4 @@ JVM_XMX=8G \ - Hytale F2P Project - [Hybrowse Docker Image](https://github.com/Hybrowse/hytale-server-docker) - Auth Server: sanasol.ws + From 89f981b586868b6988f8b74a20d8aaec1fbb1a9f Mon Sep 17 00:00:00 2001 From: Zakhar Smokotov Date: Wed, 28 Jan 2026 19:16:19 +0300 Subject: [PATCH 39/88] Some updates in Russian language localization file --- GUI/locales/ru.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/GUI/locales/ru.json b/GUI/locales/ru.json index 9174ce2..73967e3 100644 --- a/GUI/locales/ru.json +++ b/GUI/locales/ru.json @@ -17,11 +17,11 @@ "playerNamePlaceholder": "Введите ваш ник", "gameBranch": "Версия игры", "releaseVersion": "Релиз (Стабильная)", - "preReleaseVersion": "Бета (Эксперементально)", + "preReleaseVersion": "Бета (Экспериментально)", "customInstallation": "Модифицированная установка", "installationFolder": "Папка установки", "pathPlaceholder": "Путь по умолчанию", - "browse": "Browse", + "browse": "Обзор", "installButton": "УСТАНОВИТЬ HYTALE", "installing": "УСТАНОВКА..." }, @@ -30,7 +30,7 @@ "subtitle": "Запусти Hytale и приготовься к приключению!", "playButton": "ЗАПУСТИТЬ HYTALE", "latestNews": "ПОСЛЕДНИЕ НОВОСТИ", - "viewAll": "ПОСМОТРЕТЬ ВСЕ", + "viewAll": "ПОСМОТРЕТЬ ВСЁ", "checking": "ПРОВЕРКА...", "play": "ЗАПУСТИТЬ" }, @@ -38,7 +38,7 @@ "searchPlaceholder": "Искать моды...", "myMods": "Мои моды", "previous": "Предыдущая", - "next": "Вперед", + "next": "Вперёд", "page": "Страница", "of": "", "modalTitle": "МОИ МОДЫ", @@ -92,7 +92,7 @@ "title": "НАСТРОЙКИ", "java": "Рантайм JAVA", "useCustomJava": "Укажите свой путь Java", - "javaDescription": "Override the bundled Java runtime with your own installation", + "javaDescription": "Переопределить встроенный рантайм Java с вашей установкой", "javaPath": "Путь исполняемого файла Java", "javaPathPlaceholder": "Выберите путь Java...", "javaBrowse": "Обзор", @@ -162,8 +162,8 @@ "createProfile": "Создать профиль" }, "discord": { - "notificationText": "Join our Discord community!", - "joinButton": "Join Discord" + "notificationText": "Присоединитесь к нашему сообществу в Discord!", + "joinButton": "Присоединиться к Discord" }, "common": { "confirm": "Подтвердить", @@ -184,7 +184,7 @@ "discordEnabled": "Discord Rich Presence включен", "discordDisabled": "Discord Rich Presence выключен", "discordSaveFailed": "Не удалось сохранить настройку Discord", - "playerNameRequired": "Please enter a valid player name", + "playerNameRequired": "Пожалуйста, введите действительное имя игрока", "playerNameSaved": "Имя игрока успешно сохранено!", "playerNameSaveFailed": "Не удалось сохранить имя игрока", "uuidCopied": "UUID скопирован в буфер обмена!", @@ -209,7 +209,7 @@ "modsDownloadFailed": "Не получилось скачать мод: {error}", "modsToggleFailed": "Не получилось включить мод: {error}", "modsDeleteFailed": "Не получилось удалить мод: {error}", - "modsModNotFound": "Mod information not found", + "modsModNotFound": "Информация по моду не найдена", "hwAccelSaved": "Настройка аппаратного ускорения сохранена!", "hwAccelSaveFailed": "Не удалось сохранить настройку аппаратного ускорения" }, @@ -241,7 +241,7 @@ "uninstallingGame": "Удаление игры...", "gameUninstalled": "Игра успешно удалена!", "uninstallFailed": "Удаление игры не удалось: {error}", - "startingUpdate": "Starting mandatory game update...", + "startingUpdate": "Начало обязательного обновления игры...", "installationComplete": "Установка успешно завершена!", "installationFailed": "Установка не удалась: {error}", "installingGameFiles": "Установка файлов игры...", From e491bf1a8466be1ce026852b480558fb6cc15393 Mon Sep 17 00:00:00 2001 From: Zakhar Smokotov Date: Wed, 28 Jan 2026 19:17:37 +0300 Subject: [PATCH 40/88] fix --- GUI/locales/ru.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GUI/locales/ru.json b/GUI/locales/ru.json index 73967e3..27df745 100644 --- a/GUI/locales/ru.json +++ b/GUI/locales/ru.json @@ -81,9 +81,9 @@ "joinButton": "Присоединиться к чату", "colorModal": { "title": "Выберите цвет ника", - "chooseSolid": "Choose a solid color:", + "chooseSolid": "Выберите цвет:", "customColor": "Модифицированный цвет:", - "preview": "Preview:", + "preview": "Предварительный просмотр:", "previewUsername": "Ник", "apply": "Применить цвет" } From 721d287036bb9b5871e0e6154437f02b4b81a421 Mon Sep 17 00:00:00 2001 From: Zakhar Smokotov Date: Wed, 28 Jan 2026 19:33:36 +0300 Subject: [PATCH 41/88] Update ru.json --- GUI/locales/ru.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GUI/locales/ru.json b/GUI/locales/ru.json index 27df745..a1d4f50 100644 --- a/GUI/locales/ru.json +++ b/GUI/locales/ru.json @@ -51,7 +51,7 @@ "installed": "УСТАНОВЛЕННЫЕ", "enable": "ВКЛЮЧИТЬ", "disable": "ВЫКЛЮЧИТЬ", - "active": "ВКЛЮЧЕН", + "active": "ВКЛЮЧЁН", "disabled": "ВЫКЛЮЧЕН", "delete": "Удалить мод", "noDescription": "Нет доступного описания", @@ -68,7 +68,7 @@ "chat": { "title": "ЧАТ ИГРОКОВ", "pickColor": "Цвет", - "inputPlaceholder": "Введите свое сообщение...", + "inputPlaceholder": "Введите своё сообщение...", "send": "Отправить", "online": "онлайн", "charCounter": "{current}/{max}", From 4cd76bb96d1fb4845e15365279504b593876e4c2 Mon Sep 17 00:00:00 2001 From: Zakhar Smokotov Date: Wed, 28 Jan 2026 19:39:41 +0300 Subject: [PATCH 42/88] Fixed Java runtime name and fixed typo --- GUI/locales/ru.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GUI/locales/ru.json b/GUI/locales/ru.json index a1d4f50..13acb7b 100644 --- a/GUI/locales/ru.json +++ b/GUI/locales/ru.json @@ -90,9 +90,9 @@ }, "settings": { "title": "НАСТРОЙКИ", - "java": "Рантайм JAVA", + "java": "Java Runtime", "useCustomJava": "Укажите свой путь Java", - "javaDescription": "Переопределить встроенный рантайм Java с вашей установкой", + "javaDescription": "Переопределить встроенный Java Runtime с вашей установкой", "javaPath": "Путь исполняемого файла Java", "javaPathPlaceholder": "Выберите путь Java...", "javaBrowse": "Обзор", @@ -216,7 +216,7 @@ "confirm": { "defaultTitle": "Подтвердить действие", "regenerateUuidTitle": "Сгенерировать новый UUID", - "regenerateUuidMessage": "Вы уверены, что хотите сгенерировать новый UUID? Генерация новго UUID изменит вашу текущую личность игрока!", + "regenerateUuidMessage": "Вы уверены, что хотите сгенерировать новый UUID? Генерация нового UUID изменит вашу текущую личность игрока!", "regenerateUuidButton": "Сгенерировать", "setCustomUuidTitle": "Установить кастомный UUID", "setCustomUuidMessage": "Вы уверены, что хотите установить кастомный UUID? Установка кастомного UUID изменит вашу текущую личность игрока!", From 4fff87f2218a69307799ae2ca3fffa1bc5d087cd Mon Sep 17 00:00:00 2001 From: Zakhar Smokotov Date: Wed, 28 Jan 2026 19:40:39 +0300 Subject: [PATCH 43/88] fixed untranslated place --- GUI/locales/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUI/locales/ru.json b/GUI/locales/ru.json index 13acb7b..1f3a218 100644 --- a/GUI/locales/ru.json +++ b/GUI/locales/ru.json @@ -119,7 +119,7 @@ "repairGame": "Починить игру", "reinstallGame": "Переустановить файлы игры (сохраняет данные)", "gpuPreference": "Предпочтение GPU", - "gpuHint": "Выберите ваш предпочитаемый GPU (Linux: affects DRI_PRIME)", + "gpuHint": "Выберите ваш предпочитаемый GPU (Linux: влияет на DRI_PRIME)", "gpuAuto": "Автоматический выбор", "gpuIntegrated": "Интегрированная видеокарта", "gpuDedicated": "Дискретная видеокарта", From d69695e499f3961d04cc7fba141ac8ede2d69451 Mon Sep 17 00:00:00 2001 From: Zakhar Smokotov Date: Wed, 28 Jan 2026 19:45:29 +0300 Subject: [PATCH 44/88] Update ru.json --- GUI/locales/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUI/locales/ru.json b/GUI/locales/ru.json index 1f3a218..3479ede 100644 --- a/GUI/locales/ru.json +++ b/GUI/locales/ru.json @@ -141,7 +141,7 @@ "branchSwitching": "Переключение на {branch}...", "branchSwitched": "Переключение на {branch} выполнено успешно!", "installRequired": "Необходима установка", - "branchInstallConfirm": "Игры будет установлена для ветки {branch}. Продолжить?" + "branchInstallConfirm": "Игра будет установлена для ветки {branch}. Продолжить?" }, "uuid": { "modalTitle": "Управление UUID", From de193e991f47f5cf22367b0bd8bfda327c1a1cc4 Mon Sep 17 00:00:00 2001 From: Zakhar Smokotov Date: Wed, 28 Jan 2026 19:46:30 +0300 Subject: [PATCH 45/88] Update ru.json --- GUI/locales/ru.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GUI/locales/ru.json b/GUI/locales/ru.json index 3479ede..44cf831 100644 --- a/GUI/locales/ru.json +++ b/GUI/locales/ru.json @@ -17,7 +17,7 @@ "playerNamePlaceholder": "Введите ваш ник", "gameBranch": "Версия игры", "releaseVersion": "Релиз (Стабильная)", - "preReleaseVersion": "Бета (Экспериментально)", + "preReleaseVersion": "Пре-Релиз (Экспериментально)", "customInstallation": "Модифицированная установка", "installationFolder": "Папка установки", "pathPlaceholder": "Путь по умолчанию", @@ -135,7 +135,7 @@ "hwAccelDescription": "Включить аппаратное ускорение для лаунчера", "gameBranch": "Ветка игры", "branchRelease": "Релиз", - "branchPreRelease": "Бета", + "branchPreRelease": "Пре-Релиз", "branchHint": "Переключает между релизом и бетой игры", "branchWarning": "Изменение ветки скачает и установит другую версию игры", "branchSwitching": "Переключение на {branch}...", @@ -199,7 +199,7 @@ "uuidSetFailed": "Не удалось поставить кастомный UUID", "uuidSetSuccess": "Кастомный UUID успешно установлен!", "uuidDeleteFailed": "Не удалось удалить UUID", - "uuidDeleteSuccess": "Удаление UUID успешно!", + "uuidDeleteSuccess": "Удаление UUID успешно завершено!", "modsDownloading": "Скачивание {name}...", "modsTogglingMod": "Включение мода...", "modsDeletingMod": "Удаление мода...", From 4fc4d77415d6f013620171b5073eef8fa66dccdb Mon Sep 17 00:00:00 2001 From: Zakhar Smokotov Date: Wed, 28 Jan 2026 19:47:52 +0300 Subject: [PATCH 46/88] Update ru.json --- GUI/locales/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUI/locales/ru.json b/GUI/locales/ru.json index 44cf831..8527b8d 100644 --- a/GUI/locales/ru.json +++ b/GUI/locales/ru.json @@ -205,7 +205,7 @@ "modsDeletingMod": "Удаление мода...", "modsLoadingMods": "Загрузка модов с CurseForge...", "modsInstalledSuccess": "{name} успешно установлен! 🎉", - "modsDeletedSuccess": "{name} удален успешно!", + "modsDeletedSuccess": "{name} удалён успешно!", "modsDownloadFailed": "Не получилось скачать мод: {error}", "modsToggleFailed": "Не получилось включить мод: {error}", "modsDeleteFailed": "Не получилось удалить мод: {error}", From 779f6820cb89085b8582ba32273aa28b74d33660 Mon Sep 17 00:00:00 2001 From: Zakhar Smokotov Date: Wed, 28 Jan 2026 19:49:37 +0300 Subject: [PATCH 47/88] Update ru.json --- GUI/locales/ru.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/GUI/locales/ru.json b/GUI/locales/ru.json index 8527b8d..a016cd5 100644 --- a/GUI/locales/ru.json +++ b/GUI/locales/ru.json @@ -233,17 +233,17 @@ "downloading": "Скачивание...", "installing": "Установка...", "extracting": "Извлечение...", - "verifying": "Сверка...", + "verifying": "Проверка...", "switchingProfile": "Смена профиля...", - "profileSwitched": "Профиль сменен!", + "profileSwitched": "Профиль сменён!", "startingGame": "Запуск игры...", "launching": "ЗАПУСК...", "uninstallingGame": "Удаление игры...", "gameUninstalled": "Игра успешно удалена!", - "uninstallFailed": "Удаление игры не удалось: {error}", + "uninstallFailed": "Удаление игры прервано с ошибкой: {error}", "startingUpdate": "Начало обязательного обновления игры...", "installationComplete": "Установка успешно завершена!", - "installationFailed": "Установка не удалась: {error}", + "installationFailed": "Установка прервана с ошибкой: {error}", "installingGameFiles": "Установка файлов игры...", "installComplete": "Установка завершена!" } From 0e4e332dab201411b249b96ed8f6058ffb4f5b52 Mon Sep 17 00:00:00 2001 From: Zakhar Smokotov Date: Wed, 28 Jan 2026 19:53:46 +0300 Subject: [PATCH 48/88] Update ru.json --- GUI/locales/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUI/locales/ru.json b/GUI/locales/ru.json index a016cd5..38f4c3c 100644 --- a/GUI/locales/ru.json +++ b/GUI/locales/ru.json @@ -136,7 +136,7 @@ "gameBranch": "Ветка игры", "branchRelease": "Релиз", "branchPreRelease": "Пре-Релиз", - "branchHint": "Переключает между релизом и бетой игры", + "branchHint": "Переключает между релизом и пре-релизом игры", "branchWarning": "Изменение ветки скачает и установит другую версию игры", "branchSwitching": "Переключение на {branch}...", "branchSwitched": "Переключение на {branch} выполнено успешно!", From a07f0f1de15a779f28e6a0d2f915c149a524d5d5 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Thu, 29 Jan 2026 03:01:38 +0800 Subject: [PATCH 49/88] fix: timeout getLatestClient fixes #138 --- backend/services/versionManager.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/services/versionManager.js b/backend/services/versionManager.js index 46002b7..3cf0010 100644 --- a/backend/services/versionManager.js +++ b/backend/services/versionManager.js @@ -5,7 +5,7 @@ async function getLatestClientVersion(branch = 'release') { console.log(`Fetching latest client version from API (branch: ${branch})...`); const response = await axios.get('https://files.hytalef2p.com/api/version_client', { params: { branch }, - timeout: 5000, + timeout: 40000, // fixed from 5000 to 40000 to make sure the client trying to connect on the server with slow internet headers: { 'User-Agent': 'Hytale-F2P-Launcher' } @@ -16,13 +16,13 @@ async function getLatestClientVersion(branch = 'release') { console.log(`Latest client version for ${branch}: ${version}`); return version; } else { - console.log('Warning: Invalid API response, falling back to default version'); - return '4.pwr'; + console.log('Warning: Invalid API response, falling back to latest known version (7.pwr - 2026-01-29)'); // added latest version fallback and latest known version as per today + return '7.pwr'; } } catch (error) { console.error('Error fetching client version:', error.message); - console.log('Warning: API unavailable, falling back to default version'); - return '4.pwr'; + console.log('Warning: API unavailable, falling back to latest known version (7.pwr - 2026-01-29)'); + return '7.pwr'; } } From 534b3f1f34ae7e878fb0e422c7b22ff6fa7e971b Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Thu, 29 Jan 2026 03:19:17 +0800 Subject: [PATCH 50/88] fix: change default version to 7.pwr in main.js --- main.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/main.js b/main.js index 3f9f04b..eb4dc92 100644 --- a/main.js +++ b/main.js @@ -547,7 +547,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath, console.log('[Main] Processing Butler error with retry context'); errorData.retryData = { branch: error.branch || 'release', - fileName: error.fileName || '4.pwr', + fileName: error.fileName || '7.pwr', cacheDir: error.cacheDir }; errorData.canRetry = error.canRetry !== false; @@ -567,7 +567,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath, console.log('[Main] Processing generic error, creating default retry data'); errorData.retryData = { branch: 'release', - fileName: '4.pwr' + fileName: '7.pwr' }; // For generic errors, assume it's retryable unless specified errorData.canRetry = error.canRetry !== false; @@ -802,7 +802,7 @@ ipcMain.handle('retry-download', async (event, retryData) => { console.log('[IPC] Invalid retry data, using PWR defaults'); retryData = { branch: 'release', - fileName: '4.pwr' + fileName: '7.pwr' }; } @@ -836,7 +836,7 @@ ipcMain.handle('retry-download', async (event, retryData) => { } : { branch: retryData?.branch || 'release', - fileName: retryData?.fileName || '4.pwr', + fileName: retryData?.fileName || '7.pwr', cacheDir: retryData?.cacheDir }; @@ -1379,3 +1379,4 @@ ipcMain.handle('profile-update', async (event, id, updates) => { return { error: error.message }; } }); + From bc7f46cf4589d1d0ab785371a0151b8d3b894720 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Thu, 29 Jan 2026 03:22:30 +0800 Subject: [PATCH 51/88] fix: change default release version to 7.pwr --- GUI/js/ui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GUI/js/ui.js b/GUI/js/ui.js index 44182cb..486047c 100644 --- a/GUI/js/ui.js +++ b/GUI/js/ui.js @@ -972,7 +972,7 @@ function setupRetryButton() { if (!currentDownloadState.retryData || currentDownloadState.errorType === 'jre') { currentDownloadState.retryData = { branch: 'release', - fileName: '4.pwr' + fileName: '7.pwr' }; console.log('[UI] Created default PWR retry data:', currentDownloadState.retryData); } @@ -1040,7 +1040,7 @@ function setupRetryButton() { } else { currentDownloadState.retryData = { branch: 'release', - fileName: '4.pwr' + fileName: '7.pwr' }; } console.log('[UI] Created default retry data:', currentDownloadState.retryData); From 966de83eadb155b115cb5b2f41378cf45dee7eb6 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Thu, 29 Jan 2026 03:23:19 +0800 Subject: [PATCH 52/88] fix: change version release to 7.pwr --- backend/managers/gameManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index 2f164bd..9f905b7 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -11,7 +11,7 @@ const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, lo const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager'); const { getUserDataPath, migrateUserDataToCentralized } = require('../utils/userDataMigration'); -async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) { +async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) { const osName = getOS(); const arch = getArch(); From 28a4f65f21375052d3bc3d07c3f58c9d92c78096 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Jan 2026 02:24:39 +0700 Subject: [PATCH 53/88] docs: Add comprehensive troubleshooting guide (#209) Add TROUBLESHOOTING.md with solutions for common issues including: - Windows: Firewall configuration, duplicate mods, SmartScreen - Linux: GPU detection (NVIDIA/AMD), SDL3_image/libpng dependencies, Wayland/X11 issues, Steam Deck support - macOS: Rosetta 2 for Apple Silicon, code signing, quarantine - Connection: Server boot failures, regional restrictions - Authentication: Token errors, config reset procedures - Avatar/Cosmetics: F2P limitations documentation - Backup locations for all platforms - Log locations for bug reports Solutions compiled from closed GitHub issues (#205, #155, #90, #60, #144, #192) and community feedback. Co-authored-by: Claude Opus 4.5 --- TROUBLESHOOTING.md | 460 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) create mode 100644 TROUBLESHOOTING.md diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..345ee3e --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,460 @@ +# Hytale F2P Launcher - Troubleshooting Guide + +This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [Discord](https://discord.gg/gME8rUy3MB). + +--- + +## Table of Contents + +- [Windows Issues](#windows-issues) +- [Linux Issues](#linux-issues) +- [macOS Issues](#macos-issues) +- [Connection & Server Issues](#connection--server-issues) +- [Authentication & Token Issues](#authentication--token-issues) +- [Avatar & Cosmetics Issues](#avatar--cosmetics-issues) +- [General Issues](#general-issues) +- [Known Limitations](#known-limitations) + +--- + +## Windows Issues + +### "Failed to connect to server" / Server won't boot + +**Symptoms:** Singleplayer world fails to load, "Server failed to boot" error. + +**Solution - Whitelist in Windows Firewall:** +1. Open **Windows Settings** > **Privacy & Security** > **Windows Security** +2. Click **Firewall & network protection** > **Allow an app through firewall** +3. Click **Change settings** +4. Find `HytaleClient.exe` and check both **Private** and **Public** +5. If not listed, click **Allow another app** and browse to: + ``` + %localappdata%\HytaleF2P\release\package\game\latest\Client\HytaleClient.exe + ``` + +### Duplicate mod error + +**Symptoms:** `java.lang.IllegalArgumentException: Tried to load duplicate plugin` + +**Solution:** +1. Navigate to your mods folder: + ``` + %localappdata%\HytaleF2P\release\package\game\latest\Client\UserData\Mods + ``` +2. Remove any duplicate `.jar` files +3. Restart the launcher + +### SmartScreen blocks the launcher + +**Solution:** +1. Click **More info** +2. Click **Run anyway** + +--- + +## Linux Issues + +### GPU not detected / Using software rendering (llvmpipe) + +**Symptoms:** +- Game uses integrated GPU instead of dedicated GPU +- Very low FPS / unplayable performance +- Play button not clickable +- Log shows `llvmpipe` instead of your GPU + +**Solution for NVIDIA:** +```bash +__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json ./HytaleF2P.AppImage +``` + +**Solution for AMD (hybrid GPU systems):** +```bash +DRI_PRIME=1 ./HytaleF2P.AppImage +``` + +**Permanent fix - Create a launcher script:** +```bash +#!/bin/bash +export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json +export DRI_PRIME=1 +./HytaleF2P.AppImage +``` + +**Note:** On some desktop systems with AMD iGPU + dGPU, the GPU selector may be inverted (selecting iGPU actually uses dGPU). Use whichever option works. + +### SDL3_image / libpng errors + +**Symptoms:** +- `DllNotFoundException: Unable to load shared library 'SDL3_image'` +- `libpng` related errors +- Game crashes on startup + +**Solution - Install dependencies:** + +**Fedora / RHEL:** +```bash +sudo dnf install libpng libpng-devel +``` + +**Debian / Ubuntu:** +```bash +sudo apt install libpng16-16 libpng-dev libgdiplus libc6-dev +``` + +**Arch Linux:** +```bash +sudo pacman -S libpng +``` + +**Alternative - Replace corrupted library:** +```bash +cd ~/.hytalef2p/release/package/game/latest/Client/ +mv libSDL3_image.so libSDL3_image.so.bak +wget https://github.com/user-attachments/files/24710966/libSDL3_image.zip +unzip libSDL3_image.zip +chmod 644 libSDL3_image.so +rm libSDL3_image.zip +``` + +### AppImage won't launch / FUSE error + +**Solution:** +```bash +# Debian/Ubuntu +sudo apt install libfuse2 + +# Fedora +sudo dnf install fuse + +# Arch +sudo pacman -S fuse2 +``` + +### Missing libxcrypt.so.1 + +**Solution:** +```bash +# Fedora/RHEL +sudo dnf install libxcrypt-compat + +# Arch +sudo pacman -S libxcrypt-compat +``` + +### Wayland display issues + +**Symptoms:** Game doesn't launch, stuck at loading, or display glitches on Wayland. + +**Solution - Force X11:** +```bash +GDK_BACKEND=x11 ./HytaleF2P.AppImage +``` + +**Alternative - Electron Wayland hint:** +```bash +ELECTRON_OZONE_PLATFORM_HINT=auto ./HytaleF2P.AppImage +``` + +### Steam Deck / Gamescope issues + +**Solution 1 - Add custom launch options in Steam:** +``` +ELECTRON_OZONE_PLATFORM_HINT=x11 %command% +``` + +**Solution 2 - Launch from Desktop Mode** instead of Game Mode. + +**Solution 3 - Force X11:** +```bash +GDK_BACKEND=x11 ./HytaleF2P.AppImage +``` + +### Ubuntu LTS-based distros (Linux Mint, Zorin OS, Pop!_OS) + +These distributions may have compatibility issues due to older system packages. This is a limitation of the Hytale game client, not the launcher. + +**Workarounds:** +1. Install all dependencies listed above +2. Try the SDL3_image replacement +3. Consider using a more recent distribution or Flatpak/AppImage with bundled dependencies + +--- + +## macOS Issues + +### "Butler system error -86" (Apple Silicon) + +**Symptoms:** `Butler execution failed: spawn Unknown system error -86` (EXC_BAD_CPU_TYPE) + +**Cause:** Butler (the update tool) is x86_64 only. + +**Solution - Install Rosetta 2:** +```bash +softwareupdate --install-rosetta +``` + +Then restart the launcher. + +### Auto-update fails with code signature error + +**Symptoms:** +``` +Code signature at URL did not pass validation +domain: 'SQRLCodeSignatureErrorDomain' +``` + +**Solution - Manual update:** +1. Download the latest version manually from [Releases](https://github.com/amiayweb/Hytale-F2P/releases/latest) +2. Backup your data first (see [Backup Locations](#backup-locations)) +3. Install the fresh download + +### "Unidentified developer" warning + +**Solution:** +1. Open **System Settings** > **Privacy & Security** +2. Scroll to **Security** section +3. Find the message about "Hytale F2P Launcher" +4. Click **Open Anyway** +5. Authenticate and click **Open** + +### App won't open (quarantine) + +**Solution:** +```bash +xattr -rd com.apple.quarantine /Applications/Hytale-F2P-Launcher.app +``` + +--- + +## Connection & Server Issues + +### "Failed to connect to server" in Singleplayer + +**Possible causes:** +1. Windows Firewall blocking (see [Windows section](#failed-to-connect-to-server--server-wont-boot)) +2. Patched server JAR download failed +3. Regional network restrictions + +**Solution - Check patched JAR:** +1. Look for `HytaleServer.jar` in: + - Windows: `%localappdata%\HytaleF2P\release\package\game\latest\Server\` + - Linux: `~/.hytalef2p/release/package/game/latest/Server/` + - macOS: `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server/` +2. If missing or very small, the download may have failed + +**Solution - Regional restrictions:** + +Some countries (Russia, Turkey, Indonesia, etc.) may have issues accessing download servers. +- Try using a VPN for the initial download +- Once downloaded, the patched JAR is cached locally + +### "Infinite Booting Server" / Server stuck loading + +**Solution:** +1. Check if the patched JAR downloaded successfully (see above) +2. Ensure your GPU meets minimum requirements +3. Check launcher logs for specific errors +4. Try with a VPN if in a restricted region + +### "Connection timed out from inactivity" + +**This is expected behavior.** Sessions have a 10-hour TTL and will timeout after extended inactivity. Simply reconnect to continue playing. + +--- + +## Authentication & Token Issues + +### "Invalid identity token" / "Failed to start Hytale" + +**Solution:** +1. **Restart the launcher** - This fetches fresh tokens +2. **Check system time** - JWT validation requires accurate system time +3. **Clear cached tokens:** + - Delete `config.json` from your HytaleF2P folder + - Restart the launcher + - Re-enter your username + +**Locations:** +- Windows: `%localappdata%\HytaleF2P\config.json` +- Linux: `~/.hytalef2p/config.json` +- macOS: `~/Library/Application Support/HytaleF2P/config.json` + +### Token refresh errors + +If you see issuer mismatch errors in logs: +1. Delete `config.json` and `player_id.json` +2. Restart the launcher +3. This forces a fresh authentication + +--- + +## Avatar & Cosmetics Issues + +### Avatar/skin changes not saving + +**This is a known F2P limitation:** +- F2P mode has no password protection for usernames +- Anyone can use any username +- Cosmetics are stored server-side by username +- If someone else uses your username, they can change your cosmetics + +**Workaround:** Use a unique username that others are unlikely to choose. + +### Character invisible / Customization crashes + +**Solution:** +1. Use **Repair Game** in launcher Settings +2. Verify `Assets.zip` exists in your game folder +3. Clear cached assets: + - Windows: Delete `%localappdata%\HytaleF2P\release\package\game\latest\Client\UserData\CachedAssets\` +4. Restart the launcher + +### Avatar creator shows "Offline Mode" + +**Cause:** Cannot connect to auth server. + +**Solution:** +1. Check your internet connection +2. Test connectivity: Open `https://auth.sanasol.ws/health` in browser (should show "OK") +3. Check if firewall is blocking the connection +4. Try disabling VPN (or enabling one if in restricted region) + +--- + +## General Issues + +### Mods not showing up + +**Solution:** +1. Ensure mods are placed in the correct folder: + - Windows: `%localappdata%\HytaleF2P\release\package\game\latest\Client\UserData\Mods\` + - Linux: `~/.hytalef2p/release/package/game/latest/Client/UserData/Mods/` + - macOS: `~/Library/Application Support/HytaleF2P/release/package/game/latest/Client/UserData/Mods/` +2. Verify mod files are `.jar` format +3. Check launcher logs for mod loading errors + +### Game updates delete configurations/mods + +**This is a known issue being worked on.** + +**Prevention - Always backup before updating:** +- Server configs and worlds +- Mods folder +- `config.json` and `player_id.json` + +See [Backup Locations](#backup-locations) below. + +### Play button not clickable + +Usually caused by GPU detection failure. See [GPU not detected](#gpu-not-detected--using-software-rendering-llvmpipe). + +**Alternative:** +1. Go to **Settings** > **Graphics** +2. Manually select your GPU +3. Restart the launcher + +### Read timeout errors + +**Cause:** Network connectivity issues. + +**Solutions:** +1. Check your internet connection stability +2. Try using a VPN +3. Check firewall settings +4. Try at a different time (server load varies) + +--- + +## Known Limitations + +### Linux ARM64 not supported + +Hytale does not provide ARM64 game client builds. The launcher downloads from official Hytale servers which only provide: +- Windows x64 +- macOS (Universal/Intel) +- Linux x64 + +This is outside our control. + +### F2P Username System + +- No password protection for usernames +- Anyone can claim any username +- Cosmetics shared by username +- UUIDs generated based on username + +A per-player password system is planned for future versions. + +### Session Timeout + +Game sessions have a 10-hour TTL. This is by design for security. + +--- + +## Backup Locations + +### Windows +``` +%localappdata%\HytaleF2P\ +├── config.json # Launcher settings +├── player_id.json # Player identity +└── release\package\game\latest\ + ├── Client\UserData\ # Saves, settings, mods + └── Server\ + ├── universe\ # World data + └── config.json # Server config +``` + +### Linux +``` +~/.hytalef2p/ +├── config.json +├── player_id.json +└── release/package/game/latest/ + ├── Client/UserData/ + └── Server/ + ├── universe/ + └── config.json +``` + +### macOS +``` +~/Library/Application Support/HytaleF2P/ +├── config.json +├── player_id.json +└── release/package/game/latest/ + ├── Client/UserData/ + └── Server/ + ├── universe/ + └── config.json +``` + +--- + +## Getting Help + +If your issue isn't resolved by this guide: + +1. **Check existing issues:** [GitHub Issues](https://github.com/amiayweb/Hytale-F2P/issues) +2. **Join Discord:** [discord.gg/gME8rUy3MB](https://discord.gg/gME8rUy3MB) +3. **Open a new issue** with: + - Your operating system and version + - Launcher version + - Full launcher logs from: + - Windows: `%localappdata%\HytaleF2P\logs\` + - Linux: `~/.hytalef2p/logs/` + - macOS: `~/Library/Application Support/HytaleF2P/logs/` + - Steps to reproduce the issue + +--- + +## Logs Location + +For bug reports, please include logs from: + +| OS | Path | +|----|------| +| Windows | `%localappdata%\HytaleF2P\logs\` | +| Linux | `~/.hytalef2p/logs/` | +| macOS | `~/Library/Application Support/HytaleF2P/logs/` | From b708f4a7d77118a580d9d9efb19891d9e62af3d4 Mon Sep 17 00:00:00 2001 From: xSamiVS Date: Wed, 28 Jan 2026 20:25:47 +0100 Subject: [PATCH 54/88] Standardize language codes, improve formatting, and update all locale files. (#224) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update German (Germany) localization * Update Español (España) localization * Update French (France) localization * Update Polish (Poland) localization * Update Portuguese (Brazil) localization * Update Russian (Russia) localization * Update Swedish (Sweden) localization * Update Turkish (Turkey) localization * Update language codes, names and alphabetical in i18n system * Changed Spanish language name to the Formal name "Spanish (Spain)" --- GUI/js/i18n.js | 14 ++--- GUI/locales/{de.json => de-DE.json} | 37 +------------ GUI/locales/es-ES.json | 8 ++- GUI/locales/{fr.json => fr-FR.json} | 83 +++++++++++++++++------------ GUI/locales/pl-PL.json | 46 ++++++++++------ GUI/locales/pt-BR.json | 13 +++-- GUI/locales/{ru.json => ru-RU.json} | 0 GUI/locales/{sv.json => sv-SE.json} | 37 +------------ GUI/locales/tr-TR.json | 8 ++- 9 files changed, 112 insertions(+), 134 deletions(-) rename GUI/locales/{de.json => de-DE.json} (84%) rename GUI/locales/{fr.json => fr-FR.json} (75%) rename GUI/locales/{ru.json => ru-RU.json} (100%) rename GUI/locales/{sv.json => sv-SE.json} (84%) diff --git a/GUI/js/i18n.js b/GUI/js/i18n.js index d69e186..2cccd2b 100644 --- a/GUI/js/i18n.js +++ b/GUI/js/i18n.js @@ -4,14 +4,14 @@ const i18n = (() => { let translations = {}; const availableLanguages = [ { code: 'en', name: 'English' }, - { code: 'fr', name: 'Français' }, - { code: 'de', name: 'Deutsch' }, - { code: 'sv', name: 'Svenska' }, - { code: 'es-ES', name: 'Español (España)' }, - { code: 'pt-BR', name: 'Portuguese (Brazil)' }, - { code: 'tr-TR', name: 'Turkish (Turkey)' }, + { code: 'de-DE', name: 'German (Germany)' }, + { code: 'es-ES', name: 'Spanish (Spain)' }, + { code: 'fr-FR', name: 'French (France)' }, { code: 'pl-PL', name: 'Polish (Poland)' }, - { code: 'ru', name: 'Русский' } + { code: 'pt-BR', name: 'Portuguese (Brazil)' }, + { code: 'ru-RU', name: 'Russian (Russia)' }, + { code: 'sv-SE', name: 'Swedish (Sweden)' }, + { code: 'tr-TR', name: 'Turkish (Turkey)' } ]; // Load single language file diff --git a/GUI/locales/de.json b/GUI/locales/de-DE.json similarity index 84% rename from GUI/locales/de.json rename to GUI/locales/de-DE.json index d509cdb..ace40e0 100644 --- a/GUI/locales/de.json +++ b/GUI/locales/de-DE.json @@ -211,40 +211,7 @@ "modsDeleteFailed": "Mod konnte nicht gelöscht werden: {error}", "modsModNotFound": "Mod-Informationen nicht gefunden", "hwAccelSaved": "Hardware-Beschleunigungseinstellung gespeichert", - "hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden", - "javaPathCopied": "Java-Pfad in die Zwischenablage kopiert!", - "javaPathCopyFailed": "Java-Pfad konnte nicht kopiert werden", - "javaPathSaved": "Java-Pfad erfolgreich gespeichert!", - "javaPathSaveFailed": "Java-Pfad konnte nicht gespeichert werden", - "javaPathInvalid": "Ungültiger Java-Pfad", - "javaPathReset": "Java-Pfad auf Standardwerte zurückgesetzt", - "gameLocationError": "Spielordner konnte nicht geöffnet werden", - "launcherRestartRequired": "Launcher-Neustart erforderlich, um Änderungen anzuwenden", - "gameRepairConfirm": "Möchtest du das Spiel wirklich reparieren? Dies wird alle Spieldateien neu installieren.", - "gameRepairInProgress": "Spiel wird repariert...", - "gameRepairSuccess": "Spiel erfolgreich repariert!", - "gameRepairFailed": "Spielreparatur fehlgeschlagen: {error}", - "invalidUsername": "Ungültiger Benutzername", - "usernameInUse": "Benutzername bereits vergeben", - "chatJoinSuccess": "Du bist dem Chat beigetreten!", - "chatJoinFailed": "Chat-Beitritt fehlgeschlagen", - "messageTooLong": "Nachricht zu lang", - "messageSent": "Nachricht gesendet", - "messageSendFailed": "Nachricht konnte nicht gesendet werden", - "colorUpdated": "Farbe aktualisiert!", - "colorUpdateFailed": "Farbe konnte nicht aktualisiert werden", - "profileCreated": "Profil erfolgreich erstellt!", - "profileCreateFailed": "Profil konnte nicht erstellt werden", - "profileDeleted": "Profil gelöscht", - "profileDeleteFailed": "Profil konnte nicht gelöscht werden", - "profileSwitched": "Profil gewechselt zu: {name}", - "profileSwitchFailed": "Profilwechsel fehlgeschlagen", - "invalidProfileName": "Ungültiger Profilname", - "profileNameExists": "Ein Profil mit diesem Namen existiert bereits", - "noInternet": "Keine Internetverbindung", - "checkInternetConnection": "Überprüfe deine Internetverbindung", - "serverError": "Serverfehler. Bitte versuche es später erneut.", - "unknownError": "Ein unbekannter Fehler ist aufgetreten" + "hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden" }, "confirm": { "defaultTitle": "Aktion bestätigen", @@ -280,4 +247,4 @@ "installingGameFiles": "Spieldateien werden installiert...", "installComplete": "Installation abgeschlossen!" } -} +} \ No newline at end of file diff --git a/GUI/locales/es-ES.json b/GUI/locales/es-ES.json index 61c261b..2bcfeed 100644 --- a/GUI/locales/es-ES.json +++ b/GUI/locales/es-ES.json @@ -131,6 +131,8 @@ "closeLauncher": "Comportamiento del Launcher", "closeOnStart": "Cerrar Launcher al iniciar el juego", "closeOnStartDescription": "Cierra automáticamente el launcher después de que Hytale se haya iniciado", + "hwAccel": "Aceleración por Hardware", + "hwAccelDescription": "Habilitar aceleración por hardware para el launcher", "gameBranch": "Rama del Juego", "branchRelease": "Lanzamiento", "branchPreRelease": "Pre-Lanzamiento", @@ -207,7 +209,9 @@ "modsDownloadFailed": "Error al descargar mod: {error}", "modsToggleFailed": "Error al alternar mod: {error}", "modsDeleteFailed": "Error al eliminar mod: {error}", - "modsModNotFound": "Información del mod no encontrada" + "modsModNotFound": "Información del mod no encontrada", + "hwAccelSaved": "Configuración de aceleración por hardware guardada", + "hwAccelSaveFailed": "Error al guardar la configuración de aceleración por hardware" }, "confirm": { "defaultTitle": "Confirmar acción", @@ -243,4 +247,4 @@ "installingGameFiles": "Instalando archivos del juego...", "installComplete": "¡Instalación completa!" } -} +} \ No newline at end of file diff --git a/GUI/locales/fr.json b/GUI/locales/fr-FR.json similarity index 75% rename from GUI/locales/fr.json rename to GUI/locales/fr-FR.json index 4ad3316..88ded41 100644 --- a/GUI/locales/fr.json +++ b/GUI/locales/fr-FR.json @@ -198,38 +198,53 @@ "uuidInvalidFormat": "Format UUID invalide", "uuidSetFailed": "Échec de la définition de l'UUID personnalisé", "uuidSetSuccess": "UUID personnalisé défini avec succès!", - "javaPathCopied": "Chemin Java copié dans le presse-papiers!", - "javaPathCopyFailed": "Échec de la copie du chemin Java", - "javaPathSaved": "Chemin Java sauvegardé avec succès!", - "javaPathSaveFailed": "Échec de la sauvegarde du chemin Java", - "javaPathInvalid": "Chemin Java invalide", - "javaPathReset": "Chemin Java réinitialisé aux valeurs par défaut", - "gameLocationError": "Impossible d'ouvrir l'emplacement du jeu", - "launcherRestartRequired": "Redémarrage du launcher requis pour appliquer les modifications", - "gameRepairConfirm": "Êtes-vous sûr de vouloir réparer le jeu? Cela réinstallera tous les fichiers du jeu.", - "gameRepairInProgress": "Réparation du jeu en cours...", - "gameRepairSuccess": "Jeu réparé avec succès!", - "gameRepairFailed": "Échec de la réparation du jeu: {error}", - "invalidUsername": "Nom d'utilisateur invalide", - "usernameInUse": "Nom d'utilisateur déjà utilisé", - "chatJoinSuccess": "Vous avez rejoint le chat!", - "chatJoinFailed": "Échec de la connexion au chat", - "messageTooLong": "Message trop long", - "messageSent": "Message envoyé", - "messageSendFailed": "Échec de l'envoi du message", - "colorUpdated": "Couleur mise à jour!", - "colorUpdateFailed": "Échec de la mise à jour de la couleur", - "profileCreated": "Profil créé avec succès!", - "profileCreateFailed": "Échec de la création du profil", - "profileDeleted": "Profil supprimé", - "profileDeleteFailed": "Échec de la suppression du profil", - "profileSwitched": "Profil changé vers: {name}", - "profileSwitchFailed": "Échec du changement de profil", - "invalidProfileName": "Nom de profil invalide", - "profileNameExists": "Un profil avec ce nom existe déjà", - "noInternet": "Pas de connexion Internet", - "checkInternetConnection": "Vérifiez votre connexion Internet", - "serverError": "Erreur serveur. Veuillez réessayer plus tard.", - "unknownError": "Une erreur inconnue s'est produite" + "uuidDeleteFailed": "Échec de la suppression de l'UUID", + "uuidDeleteSuccess": "UUID supprimé avec succès!", + "modsDownloading": "Téléchargement de {name}...", + "modsTogglingMod": "Basculement du mod...", + "modsDeletingMod": "Suppression du mod...", + "modsLoadingMods": "Chargement des mods depuis CurseForge...", + "modsInstalledSuccess": "{name} installé avec succès! 🎉", + "modsDeletedSuccess": "{name} supprimé avec succès", + "modsDownloadFailed": "Échec du téléchargement du mod: {error}", + "modsToggleFailed": "Échec du basculement du mod: {error}", + "modsDeleteFailed": "Échec de la suppression du mod: {error}", + "modsModNotFound": "Informations du mod introuvables", + "hwAccelSaved": "Paramètre d'accélération matérielle sauvegardé", + "hwAccelSaveFailed": "Échec de la sauvegarde du paramètre d'accélération matérielle" + }, + "confirm": { + "defaultTitle": "Confirmer l'action", + "regenerateUuidTitle": "Générer un nouvel UUID", + "regenerateUuidMessage": "Êtes-vous sûr de vouloir générer un nouvel UUID? Cela changera votre identité de joueur.", + "regenerateUuidButton": "Générer", + "setCustomUuidTitle": "Définir UUID personnalisé", + "setCustomUuidMessage": "Êtes-vous sûr de vouloir définir cet UUID personnalisé? Cela changera votre identité de joueur.", + "setCustomUuidButton": "Définir UUID", + "deleteUuidTitle": "Supprimer UUID", + "deleteUuidMessage": "Êtes-vous sûr de vouloir supprimer l'UUID de \"{username}\"? Cette action est irréversible.", + "deleteUuidButton": "Supprimer", + "uninstallGameTitle": "Désinstaller le jeu", + "uninstallGameMessage": "Êtes-vous sûr de vouloir désinstaller Hytale? Tous les fichiers du jeu seront supprimés.", + "uninstallGameButton": "Désinstaller" + }, + "progress": { + "initializing": "Initialisation...", + "downloading": "Téléchargement...", + "installing": "Installation...", + "extracting": "Extraction...", + "verifying": "Vérification...", + "switchingProfile": "Changement de profil...", + "profileSwitched": "Profil changé!", + "startingGame": "Démarrage du jeu...", + "launching": "LANCEMENT...", + "uninstallingGame": "Désinstallation du jeu...", + "gameUninstalled": "Jeu désinstallé avec succès!", + "uninstallFailed": "Échec de la désinstallation: {error}", + "startingUpdate": "Démarrage de la mise à jour obligatoire du jeu...", + "installationComplete": "Installation terminée avec succès!", + "installationFailed": "Échec de l'installation: {error}", + "installingGameFiles": "Installation des fichiers du jeu...", + "installComplete": "Installation terminée!" } -} +} \ No newline at end of file diff --git a/GUI/locales/pl-PL.json b/GUI/locales/pl-PL.json index 7d03edf..fc3c757 100644 --- a/GUI/locales/pl-PL.json +++ b/GUI/locales/pl-PL.json @@ -4,19 +4,20 @@ "mods": "Mody", "news": "Wiadomości", "chat": "Chat z graczami", - "settings": "Ustawienia", - "skins": "Skiny" + "settings": "Ustawienia" }, "header": { "playersLabel": "Graczy:", "manageProfiles": "Zarządzaj Profilami", - "defaultProfile": "Domyślny", - "f2p": "FREE TO PLAY" + "defaultProfile": "Domyślny" }, "install": { - "title": "FREE TO PLAY LAUNCHER", + "title": "DARMOWY LAUNCHER", "playerName": "Nazwa Gracza", "playerNamePlaceholder": "Wprowadź Nazwę", + "gameBranch": "Wersja Gry", + "releaseVersion": "Wydanie (Stabilna)", + "preReleaseVersion": "Przed-Wydaniem (Eksperymentalna)", "customInstallation": "Dostosuj Instalacje", "installationFolder": "Folder docelowy", "pathPlaceholder": "Domyślna lokalizacja", @@ -56,7 +57,9 @@ "noDescription": "Brak opisu", "confirmDelete": "Czy na pewno chcesz usunąć \"{name}\"?", "confirmDeleteDesc": "Tej czynności nie można cofnąć.", - "confirmDeletion": "Potwierdź" + "confirmDeletion": "Potwierdź", + "apiKeyRequired": "Wymagany Klucz API", + "apiKeyRequiredDesc": "Klucz API CurseForge jest potrzebny do przeglądania modów" }, "news": { "title": "WSZYSTKIE WIADOMOŚCI", @@ -120,11 +123,25 @@ "gpuAuto": "Auto", "gpuIntegrated": "Zintegrowana", "gpuDedicated": "Dedykowana", - "logs": "SYSTEM LOGS", + "logs": "DZIENNIKI SYSTEMOWE", "logsCopy": "Kopiuj", "logsRefresh": "Odśwież", "logsFolder": "Otwórz Folder", - "logsLoading": "Ładowanie logów..." + "logsLoading": "Ładowanie logów...", + "closeLauncher": "Zachowanie Launchera", + "closeOnStart": "Zamknij Launcher przy starcie gry", + "closeOnStartDescription": "Automatycznie zamknij launcher po uruchomieniu Hytale", + "hwAccel": "Przyspieszenie Sprzętowe", + "hwAccelDescription": "Włącz przyspieszenie sprzętowe dla launchera", + "gameBranch": "Gałąź Gry", + "branchRelease": "Wydanie", + "branchPreRelease": "Przed-Wydaniem", + "branchHint": "Przełączaj między stabilnym wydaniem a eksperymentalną wersją przed-wydaniem", + "branchWarning": "Zmiana gałęzi spowoduje pobranie i instalację innej wersji gry", + "branchSwitching": "Przełączanie na {branch}...", + "branchSwitched": "Pomyślnie przełączono na {branch}!", + "installRequired": "Wymagana Instalacja", + "branchInstallConfirm": "Gra zostanie zainstalowana dla gałęzi {branch}. Kontynuować?" }, "uuid": { "modalTitle": "Zarządzanie UUID", @@ -148,10 +165,6 @@ "notificationText": "Dołącz do naszej społeczności Discord!", "joinButton": "Dołącz Discord" }, - "skins": { - "title": "Skiny", - "comingSoon": "Personalizacja skórek już wkrótce..." - }, "common": { "confirm": "Potwierdź", "cancel": "Anuluj", @@ -160,7 +173,8 @@ "delete": "Usuń", "edit": "Edytuj", "loading": "Ładowanie...", - "apply": "Zastosuj" + "apply": "Zastosuj", + "install": "Zainstaluj" }, "notifications": { "gameDataNotFound": "Błąd: Nie znaleziono danych gry", @@ -195,7 +209,9 @@ "modsDownloadFailed": "Nie udało się pobrać moda: {error}", "modsToggleFailed": "Nie udało się przełączyć moda: {error}", "modsDeleteFailed": "Nie udało się usunąć moda: {error}", - "modsModNotFound": "Nie znaleziono informacji o modzie" + "modsModNotFound": "Nie znaleziono informacji o modzie", + "hwAccelSaved": "Zapisano ustawienie przyspieszenia sprzętowego", + "hwAccelSaveFailed": "Nie udało się zapisać ustawienia przyspieszenia sprzętowego" }, "confirm": { "defaultTitle": "Potwierdź działanie", @@ -231,4 +247,4 @@ "installingGameFiles": "Instalowanie plików gry...", "installComplete": "Instalacja zakończona!" } -} +} \ No newline at end of file diff --git a/GUI/locales/pt-BR.json b/GUI/locales/pt-BR.json index 4f83c55..f53bf38 100644 --- a/GUI/locales/pt-BR.json +++ b/GUI/locales/pt-BR.json @@ -14,9 +14,11 @@ "install": { "title": "LANÇADOR JOGO GRATUITO", "playerName": "Nome do Jogador", - "playerNamePlaceholder": "Digite seu nome", "gameBranch": "Versão do Jogo", + "playerNamePlaceholder": "Digite seu nome", + "gameBranch": "Versão do Jogo", "releaseVersion": "Lançamento (Estável)", - "preReleaseVersion": "Pré-Lançamento (Experimental)", "customInstallation": "Instalação Personalizada", + "preReleaseVersion": "Pré-Lançamento (Experimental)", + "customInstallation": "Instalação Personalizada", "installationFolder": "Pasta de Instalação", "pathPlaceholder": "Local padrão", "browse": "Procurar", @@ -129,6 +131,8 @@ "closeLauncher": "Comportamento do Lançador", "closeOnStart": "Fechar Lançador ao iniciar o jogo", "closeOnStartDescription": "Fechar automaticamente o lançador após o Hytale ter sido iniciado", + "hwAccel": "Aceleração de Hardware", + "hwAccelDescription": "Ativar aceleração de hardware para o lançador", "gameBranch": "Versão do Jogo", "branchRelease": "Lançamento", "branchPreRelease": "Pré-Lançamento", @@ -161,7 +165,6 @@ "notificationText": "Junte-se à nossa comunidade do Discord!", "joinButton": "Entrar no Discord" }, - "common": { "confirm": "Confirmar", "cancel": "Cancelar", @@ -206,7 +209,9 @@ "modsDownloadFailed": "Falha ao baixar mod: {error}", "modsToggleFailed": "Falha ao alternar mod: {error}", "modsDeleteFailed": "Falha ao excluir mod: {error}", - "modsModNotFound": "Informações do mod não encontradas" + "modsModNotFound": "Informações do mod não encontradas", + "hwAccelSaved": "Configuração de aceleração de hardware salva", + "hwAccelSaveFailed": "Falha ao salvar configuração de aceleração de hardware" }, "confirm": { "defaultTitle": "Confirmar ação", diff --git a/GUI/locales/ru.json b/GUI/locales/ru-RU.json similarity index 100% rename from GUI/locales/ru.json rename to GUI/locales/ru-RU.json diff --git a/GUI/locales/sv.json b/GUI/locales/sv-SE.json similarity index 84% rename from GUI/locales/sv.json rename to GUI/locales/sv-SE.json index 04a9773..a74da81 100644 --- a/GUI/locales/sv.json +++ b/GUI/locales/sv-SE.json @@ -211,40 +211,7 @@ "modsDeleteFailed": "Misslyckades med att ta bort modd: {error}", "modsModNotFound": "Moddinformation hittades inte", "hwAccelSaved": "Hårdvaruaccelerationsinställning sparad", - "hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning", - "javaPathCopied": "Java-sökväg kopierad till urklipp!", - "javaPathCopyFailed": "Misslyckades med att kopiera Java-sökväg", - "javaPathSaved": "Java-sökväg sparad framgångsrikt!", - "javaPathSaveFailed": "Misslyckades med att spara Java-sökväg", - "javaPathInvalid": "Ogiltig Java-sökväg", - "javaPathReset": "Java-sökväg återställd till standardvärden", - "gameLocationError": "Kunde inte öppna spelplats", - "launcherRestartRequired": "Launcher-omstart krävs för att tillämpa ändringar", - "gameRepairConfirm": "Är du säker på att du vill reparera spelet? Detta kommer att ominstallera alla spelfiler.", - "gameRepairInProgress": "Reparerar spel...", - "gameRepairSuccess": "Spel reparerat framgångsrikt!", - "gameRepairFailed": "Spelreparation misslyckades: {error}", - "invalidUsername": "Ogiltigt användarnamn", - "usernameInUse": "Användarnamn upptaget", - "chatJoinSuccess": "Du har gått med i chatten!", - "chatJoinFailed": "Misslyckades med att gå med i chatten", - "messageTooLong": "Meddelande för långt", - "messageSent": "Meddelande skickat", - "messageSendFailed": "Misslyckades med att skicka meddelande", - "colorUpdated": "Färg uppdaterad!", - "colorUpdateFailed": "Misslyckades med att uppdatera färg", - "profileCreated": "Profil skapad framgångsrikt!", - "profileCreateFailed": "Misslyckades med att skapa profil", - "profileDeleted": "Profil borttagen", - "profileDeleteFailed": "Misslyckades med att ta bort profil", - "profileSwitched": "Bytte profil till: {name}", - "profileSwitchFailed": "Profilbyte misslyckades", - "invalidProfileName": "Ogiltigt profilnamn", - "profileNameExists": "En profil med detta namn finns redan", - "noInternet": "Ingen internetanslutning", - "checkInternetConnection": "Kontrollera din internetanslutning", - "serverError": "Serverfel. Försök igen senare.", - "unknownError": "Ett okänt fel inträffade" + "hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning" }, "confirm": { "defaultTitle": "Bekräfta åtgärd", @@ -280,4 +247,4 @@ "installingGameFiles": "Installerar spelfiler...", "installComplete": "Installation slutförd!" } -} +} \ No newline at end of file diff --git a/GUI/locales/tr-TR.json b/GUI/locales/tr-TR.json index 41d15b9..23ac15d 100644 --- a/GUI/locales/tr-TR.json +++ b/GUI/locales/tr-TR.json @@ -131,6 +131,8 @@ "closeLauncher": "Başlatıcı Davranışı", "closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat", "closeOnStartDescription": "Hytale başlatıldıktan sonra başlatıcıyı otomatik olarak kapatın", + "hwAccel": "Donanım Hızlandırma", + "hwAccelDescription": "Başlatıcı için donanım hızlandırmasını etkinleştir", "gameBranch": "Oyun Dalı", "branchRelease": "Yayın", "branchPreRelease": "Ön-Yayın", @@ -207,7 +209,9 @@ "modsDownloadFailed": "Mod indirilemedi: {error}", "modsToggleFailed": "Mod değiştirilemedi: {error}", "modsDeleteFailed": "Mod silinemedi: {error}", - "modsModNotFound": "Mod bilgileri bulunamadı" + "modsModNotFound": "Mod bilgileri bulunamadı", + "hwAccelSaved": "Donanım hızlandırma ayarı kaydedildi", + "hwAccelSaveFailed": "Donanım hızlandırma ayarı kaydedilemedi" }, "confirm": { "defaultTitle": "Eylemi onayla", @@ -243,4 +247,4 @@ "installingGameFiles": "Oyun dosyaları kuruluyor...", "installComplete": "Kurulum tamamlandı!" } -} +} \ No newline at end of file From a5b930a9f088da013f40a7f45295fb2a50c85df0 Mon Sep 17 00:00:00 2001 From: Terromur <79866197+Terromur@users.noreply.github.com> Date: Thu, 29 Jan 2026 04:45:44 +0500 Subject: [PATCH 55/88] Fix PKGBUILD-git --- PKGBUILD-git | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/PKGBUILD-git b/PKGBUILD-git index d3e690d..f30755c 100644 --- a/PKGBUILD-git +++ b/PKGBUILD-git @@ -10,25 +10,25 @@ url="https://github.com/amiayweb/Hytale-F2P" license=('custom') depends=('gtk3' 'nss' 'libxcrypt-compat') makedepends=('git' 'npm') +provides=('Hytale-F2P' 'hytale-f2p-git') +conflicts=('Hytale-F2P' 'hytale-f2p-git') source=("git+$url.git" "$_pkgname.desktop") sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30') pkgver() { - cd "$srcdir/$_pkgname" + cd "$_pkgname" git describe --tags --long | sed 's/^v//;s/-/.r/;s/-/./' } build() { - cd "$srcdir/$_pkgname" + cd "$_pkgname" npm ci npm run build:arch } package() { - cd "$srcdir/$_pkgname" - install -d "$pkgdir/opt/$_pkgname" + mkdir -p "$pkgdir/opt/$_pkgname" cp -r "$_pkgname/dist/linux-unpacked/"* "$pkgdir/opt/$_pkgname" - - install -Dm644 "$srcdir/$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop" - install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png" + install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop" + install -Dm644 "$_pkgname/GUI/icon.png" "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png" } From baa585d6b31c06301f0be222d245f0ba7e054166 Mon Sep 17 00:00:00 2001 From: Terromur <79866197+Terromur@users.noreply.github.com> Date: Thu, 29 Jan 2026 04:49:02 +0500 Subject: [PATCH 56/88] Fix PKGBUILD --- PKGBUILD | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PKGBUILD b/PKGBUILD index d5518ce..5c81a5e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -10,6 +10,8 @@ url="https://github.com/amiayweb/Hytale-F2P" license=('custom') depends=('gtk3' 'nss' 'libxcrypt-compat') makedepends=('npm') +provides=('Hytale-F2P-git' 'hytale-f2p-git') +conflicts=('Hytale-F2P-git' 'hytale-f2p-git') source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop") sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30') From 90db069e4c50fcce14f8aab9c53a1b32967f7845 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Thu, 29 Jan 2026 00:58:47 +0100 Subject: [PATCH 57/88] delete cache after installation --- backend/managers/gameManager.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index 9f905b7..139008d 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -300,6 +300,16 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir fs.rmSync(stagingDir, { recursive: true, force: true }); } + // Delete PWR file from cache after successful installation + try { + if (fs.existsSync(pwrFile)) { + fs.unlinkSync(pwrFile); + console.log('[Butler] PWR file deleted from cache after successful installation:', pwrFile); + } + } catch (delErr) { + console.warn('[Butler] Failed to delete PWR file from cache:', delErr.message); + } + if (progressCallback) { progressCallback('Installation complete', null, null, null, null); } @@ -352,7 +362,15 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, } await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir); - + // Delete PWR file from cache after successful update + try { + if (fs.existsSync(pwrFile)) { + fs.unlinkSync(pwrFile); + console.log('[UpdateGameFiles] PWR file deleted from cache after successful update:', pwrFile); + } + } catch (delErr) { + console.warn('[UpdateGameFiles] Failed to delete PWR file from cache:', delErr.message); + } if (progressCallback) { progressCallback('Replacing game files...', 80, null, null, null); } From 4775e9adbdd6f92c5ce8a0c6d0eda359128281e7 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Thu, 29 Jan 2026 03:33:56 +0100 Subject: [PATCH 58/88] Enforce 16-char player name limit and update mod sync Added a maxlength attribute to the player name input and enforced a 16-character limit in both install and settings scripts, providing user feedback if exceeded. Refactored modManager.js to replace symlink-based mod management with a copy-based system, copying enabled mods to HytaleSaves\Mods and removing legacy symlink logic to improve compatibility and avoid permission issues. --- GUI/index.html | 2 +- GUI/js/install.js | 21 +++++- GUI/js/settings.js | 7 ++ backend/managers/modManager.js | 130 +++++++++++---------------------- 4 files changed, 70 insertions(+), 90 deletions(-) diff --git a/GUI/index.html b/GUI/index.html index 8e8b4e1..7f3d624 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -120,7 +120,7 @@ + value="Player" maxlength="16" />
diff --git a/GUI/js/install.js b/GUI/js/install.js index a20d1ef..d7ea0d1 100644 --- a/GUI/js/install.js +++ b/GUI/js/install.js @@ -45,9 +45,17 @@ export function setupInstallation() { export async function installGame() { if (isDownloading || (installBtn && installBtn.disabled)) return; - const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player'; + let playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player'; const installPath = installPathInput ? installPathInput.value.trim() : ''; + // Limit player name to 16 characters + if (playerName.length > 16) { + playerName = playerName.substring(0, 16); + if (installPlayerName) { + installPlayerName.value = playerName; + } + } + const selectedBranchRadio = document.querySelector('input[name="installBranch"]:checked'); const selectedBranch = selectedBranchRadio ? selectedBranchRadio.value : 'release'; @@ -194,7 +202,16 @@ export async function browseInstallPath() { async function savePlayerName() { try { if (window.electronAPI && window.electronAPI.saveSettings) { - const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player'; + let playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player'; + + // Limit player name to 16 characters + if (playerName.length > 16) { + playerName = playerName.substring(0, 16); + if (installPlayerName) { + installPlayerName.value = playerName; + } + } + await window.electronAPI.saveSettings({ playerName }); } } catch (error) { diff --git a/GUI/js/settings.js b/GUI/js/settings.js index 0a2efc9..42b4c36 100644 --- a/GUI/js/settings.js +++ b/GUI/js/settings.js @@ -439,6 +439,13 @@ async function savePlayerName() { return; } + if (playerName.length > 16) { + const msg = window.i18n ? window.i18n.t('notifications.playerNameTooLong') : 'Player name must be 16 characters or less'; + showNotification(msg, 'error'); + settingsPlayerName.value = playerName.substring(0, 16); + return; + } + await window.electronAPI.saveUsername(playerName); const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully'; showNotification(successMsg, 'success'); diff --git a/backend/managers/modManager.js b/backend/managers/modManager.js index 631db7f..cb7775e 100644 --- a/backend/managers/modManager.js +++ b/backend/managers/modManager.js @@ -3,7 +3,7 @@ const path = require('path'); const crypto = require('crypto'); const axios = require('axios'); const { getOS } = require('../utils/platformUtils'); -const { getModsPath, getProfilesDir } = require('../core/paths'); +const { getModsPath, getProfilesDir, getHytaleSavesDir } = require('../core/paths'); const { saveModsToConfig, loadModsFromConfig } = require('../core/config'); const profileManager = require('./profileManager'); @@ -296,8 +296,9 @@ async function syncModsForCurrentProfile() { console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name} (${activeProfile.id})`); // 1. Resolve Paths - // globalModsPath is the one the game uses (symlink target) - const globalModsPath = await getModsPath(); + // centralModsPath is HytaleSaves\Mods (centralized location for active mods) + const hytaleSavesDir = getHytaleSavesDir(); + const centralModsPath = path.join(hytaleSavesDir, 'Mods'); // profileModsPath is the real storage for this profile const profileModsPath = getProfileModsPath(activeProfile.id); const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods'); @@ -306,96 +307,51 @@ async function syncModsForCurrentProfile() { fs.mkdirSync(profileDisabledModsPath, { recursive: true }); } - // 2. Symlink / Migration Logic - let needsLink = false; - let globalStats = null; - + // 2. Copy-based Mod Sync (No symlinks - avoids permission issues) + // Ensure HytaleSaves\Mods directory exists + if (!fs.existsSync(centralModsPath)) { + fs.mkdirSync(centralModsPath, { recursive: true }); + console.log(`[ModManager] Created centralized mods directory: ${centralModsPath}`); + } + + // Check for old symlink and convert to real directory if needed (one-time migration) try { - globalStats = fs.lstatSync(globalModsPath); + const centralStats = fs.lstatSync(centralModsPath); + if (centralStats.isSymbolicLink()) { + console.log('[ModManager] Removing old symlink, converting to copy-based system...'); + fs.unlinkSync(centralModsPath); + fs.mkdirSync(centralModsPath, { recursive: true }); + } } catch (e) { - // Path doesn't exist + // Path doesn't exist, will be created above } - if (globalStats) { - if (globalStats.isSymbolicLink()) { - const linkTarget = fs.readlinkSync(globalModsPath); - // Normalize paths for comparison - if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) { - console.log(`[ModManager] Updating symlink from ${linkTarget} to ${profileModsPath}`); - fs.unlinkSync(globalModsPath); - needsLink = true; - } - } else if (globalStats.isDirectory()) { - // MIGRATION: It's a real directory. Move contents to profile. - console.log('[ModManager] Migrating global mods folder to profile folder...'); - const files = fs.readdirSync(globalModsPath); - for (const file of files) { - const src = path.join(globalModsPath, file); - const dest = path.join(profileModsPath, file); - // Only move if dest doesn't exist to avoid overwriting - if (!fs.existsSync(dest)) { - fs.renameSync(src, dest); - } - } - - // Also migrate DisabledMods if it exists globally - const globalDisabledPath = path.join(path.dirname(globalModsPath), 'DisabledMods'); - if (fs.existsSync(globalDisabledPath) && fs.lstatSync(globalDisabledPath).isDirectory()) { - const dFiles = fs.readdirSync(globalDisabledPath); - for (const file of dFiles) { - const src = path.join(globalDisabledPath, file); - const dest = path.join(profileDisabledModsPath, file); - if (!fs.existsSync(dest)) { - fs.renameSync(src, dest); - } - } - // We can remove global DisabledMods now, as it's not used by game - try { fs.rmSync(globalDisabledPath, { recursive: true, force: true }); } catch(e) {} - } - - // Remove the directory so we can link it - try { - let retries = 3; - while (retries > 0) { - try { - fs.rmSync(globalModsPath, { recursive: true, force: true }); - break; - } catch (err) { - if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) { - retries--; - await new Promise(resolve => setTimeout(resolve, 500)); - } else { - throw err; - } - } - } - needsLink = true; - } catch (e) { - console.error('Failed to remove global mods dir:', e); - // Throw error to stop. - throw new Error('Failed to migrate mods directory. Please clear ' + globalModsPath); - } - } - } else { - needsLink = true; - } - - if (needsLink) { - console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`); + // Copy enabled mods from profile to HytaleSaves\Mods (for game to use) + console.log(`[ModManager] Copying enabled mods from ${profileModsPath} to ${centralModsPath}`); + + // First, clear central mods folder + const existingCentralMods = fs.existsSync(centralModsPath) ? fs.readdirSync(centralModsPath) : []; + for (const file of existingCentralMods) { + const filePath = path.join(centralModsPath, file); try { - const symlinkType = getOS() === 'windows' ? 'junction' : 'dir'; - fs.symlinkSync(profileModsPath, globalModsPath, symlinkType); - } catch (err) { - // If we can't create the symlink, try creating the directory first - console.error('[ModManager] Failed to create symlink. Falling back to direct folder mode.'); - console.error(err.message); - - // Fallback: create a real directory so the game still works - if (!fs.existsSync(globalModsPath)) { - fs.mkdirSync(globalModsPath, { recursive: true }); + fs.unlinkSync(filePath); + } catch (e) { + console.warn(`Failed to remove ${file} from central mods:`, e.message); + } + } + + // Copy enabled mods to HytaleSaves\Mods + const enabledModFiles = fs.existsSync(profileModsPath) ? fs.readdirSync(profileModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : []; + for (const file of enabledModFiles) { + const src = path.join(profileModsPath, file); + const dest = path.join(centralModsPath, file); + try { + fs.copyFileSync(src, dest); + console.log(`[ModManager] Copied ${file} to HytaleSaves\\Mods`); + } catch (e) { + console.error(`Failed to copy ${file}:`, e.message); } } - } // 3. Auto-Repair (Download missing mods) const profileModsSnapshot = activeProfile.mods || []; @@ -460,7 +416,7 @@ async function syncModsForCurrentProfile() { } // 5. Enforce Enabled/Disabled State (Move files between Profile/Mods and Profile/DisabledMods) - // Note: Since Global/Mods IS Profile/Mods (via symlink), moving out of Profile/Mods disables it for the game. + // Note: Enabled mods are copied to HytaleSaves\Mods, disabled mods stay in Profile/DisabledMods const disabledFiles = fs.existsSync(profileDisabledModsPath) ? fs.readdirSync(profileDisabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : []; const allFiles = new Set([...enabledFiles, ...disabledFiles]); From 93a2a980289e4cee44dfbbd7f602f0bdd491e971 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Thu, 29 Jan 2026 03:38:46 +0100 Subject: [PATCH 59/88] Update installation subtitle --- GUI/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUI/index.html b/GUI/index.html index 7f3d624..0f22cf8 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -112,7 +112,7 @@

HYTALE

-

FREE TO PLAY LAUNCHER

+

UNOFFICIAL HYTALE LAUNCHER

From e0fd7e6900e57ba91d5978a5c4f654cb595e6426 Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Thu, 29 Jan 2026 23:14:22 +0800 Subject: [PATCH 60/88] chore: update quickstart link in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4943cf2..430ecf4 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ⭐ **If you find this project useful, please give it a STAR!** ⭐ -### ⚠️ **READ [QUICK START](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-quick-start) before Downloading & Installing the Launcher!** ⚠️ +### ⚠️ **READ [QUICK START](README.md#-quick-start) before Downloading & Installing the Launcher!** ⚠️ #### 🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑 From 4db8016a284ea601acedc79184204d459f9d0cba Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Thu, 29 Jan 2026 23:15:54 +0800 Subject: [PATCH 61/88] chore: delete warning of Ubuntu-Debian at Linux Prequisites section --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 430ecf4..b0057f5 100644 --- a/README.md +++ b/README.md @@ -176,9 +176,6 @@ ### 🐧 Linux Prequisites -> [!WARNING] -> Ubuntu-based Distro like ZorinOS or Pop!_OS or Linux Mint would encounter issues due to UbuntuLTS environment, [check this Discord post](https://discord.com/channels/1462260103951421493/1463662398501027973). - * Make sure you have already installed newest **GPU driver** especially proprietary NVIDIA, consult your distro docs or wiki. * Also make sure that your GPU can be connected to EGL, try checking it first (again, consult your distro docs or wiki) before installing Hytale game via our launcher. * Install `libpng` package to avoid `SDL3_Image` error: From 5039bcdadfd0b05791f807e74d8225b261e3982f Mon Sep 17 00:00:00 2001 From: AMIAY Date: Thu, 29 Jan 2026 17:07:29 +0100 Subject: [PATCH 62/88] added featured server list from api --- backend/managers/gameLauncher.js | 9 +++ backend/utils/serverListSync.js | 119 +++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 backend/utils/serverListSync.js diff --git a/backend/managers/gameLauncher.js b/backend/managers/gameLauncher.js index 0ac187d..4555844 100644 --- a/backend/managers/gameLauncher.js +++ b/backend/managers/gameLauncher.js @@ -13,6 +13,7 @@ const { getLatestClientVersion } = require('../services/versionManager'); const { updateGameFiles } = require('./gameManager'); const { syncModsForCurrentProfile } = require('./modManager'); const { getUserDataPath } = require('../utils/userDataMigration'); +const { syncServerList } = require('../utils/serverListSync'); // Client patcher for custom auth server (sanasol.ws) let clientPatcher = null; @@ -103,6 +104,14 @@ function generateLocalTokens(uuid, name) { } async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) { + // Synchronize server list on every game launch + try { + console.log('[Launcher] Synchronizing server list...'); + await syncServerList(); + } catch (syncError) { + console.warn('[Launcher] Server list sync failed, continuing launch:', syncError.message); + } + const branch = branchOverride || loadVersionBranch(); const customAppDir = getResolvedAppDir(installPathOverride); const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest'); diff --git a/backend/utils/serverListSync.js b/backend/utils/serverListSync.js new file mode 100644 index 0000000..59e7e47 --- /dev/null +++ b/backend/utils/serverListSync.js @@ -0,0 +1,119 @@ +const fs = require('fs'); +const path = require('path'); +const axios = require('axios'); +const { v4: uuidv4 } = require('uuid'); +const { getHytaleSavesDir } = require('../core/paths'); + +const SERVER_LIST_URL = 'https://assets.authbp.xyz/server.json'; + + +function getLocalDateTime() { + return formatLocalDateTime(new Date()); +} + +function formatLocalDateTime(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const milliseconds = String(date.getMilliseconds()).padStart(3, '0'); + const offsetMinutes = -date.getTimezoneOffset(); + const offsetHours = Math.floor(Math.abs(offsetMinutes) / 60); + const offsetMins = Math.abs(offsetMinutes) % 60; + const offsetSign = offsetMinutes >= 0 ? '+' : '-'; + const offset = `${offsetSign}${String(offsetHours).padStart(2, '0')}:${String(offsetMins).padStart(2, '0')}`; + + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}0000${offset}`; +} + +async function syncServerList() { + try { + const hytaleSavesDir = getHytaleSavesDir(); + const serverListPath = path.join(hytaleSavesDir, 'ServerList.json'); + console.log('[ServerListSync] Fetching server list from', SERVER_LIST_URL); + let remoteData; + try { + const response = await axios.get(SERVER_LIST_URL, { + timeout: 40000, + headers: { + 'User-Agent': 'Hytale-F2P-Launcher' + } + }); + remoteData = response.data; + } catch (fetchError) { + console.warn('[ServerListSync] Failed to fetch remote server list:', fetchError.message); + remoteData = { SavedServers: [] }; + } + let localData = { SavedServers: [] }; + if (fs.existsSync(serverListPath)) { + try { + const localContent = fs.readFileSync(serverListPath, 'utf-8'); + localData = JSON.parse(localContent); + console.log('[ServerListSync] Loaded existing local server list with', localData.SavedServers?.length || 0, 'servers'); + } catch (parseError) { + console.warn('[ServerListSync] Failed to parse local server list, creating new one:', parseError.message); + localData = { SavedServers: [] }; + } + } else { + console.log('[ServerListSync] Local server list does not exist, creating new one'); + } + + if (!localData.SavedServers) { + localData.SavedServers = []; + } + if (!remoteData.SavedServers) { + remoteData.SavedServers = []; + } + + const existingServersByAddress = new Map(); + const userServers = []; + + for (const server of localData.SavedServers) { + existingServersByAddress.set(server.Address.toLowerCase(), server); + } + + const remoteAddresses = new Set(remoteData.SavedServers.map(s => s.Address.toLowerCase())); + for (const server of localData.SavedServers) { + if (!remoteAddresses.has(server.Address.toLowerCase())) { + userServers.push(server); + } + } + + const currentDate = getLocalDateTime(); + + + const apiServers = []; + for (const remoteServer of remoteData.SavedServers) { + const serverToAdd = { + Id: uuidv4(), + Name: "@ " + remoteServer.Name, + Address: remoteServer.Address, + DateSaved: currentDate + }; + apiServers.push(serverToAdd); + console.log('[ServerListSync] Added/Updated server with new ID:', remoteServer.Name); + } + + localData.SavedServers = [...apiServers, ...userServers]; + + const addedCount = apiServers.length; + + if (!fs.existsSync(hytaleSavesDir)) { + fs.mkdirSync(hytaleSavesDir, { recursive: true }); + } + + fs.writeFileSync(serverListPath, JSON.stringify(localData, null, 2), 'utf-8'); + console.log('[ServerListSync] Server list synchronized:', addedCount, 'API servers added, total:', localData.SavedServers.length); + + return { success: true, added: addedCount, total: localData.SavedServers.length }; + } catch (error) { + console.error('[ServerListSync] Failed to synchronize server list:', error.message); + return { success: false, error: error.message }; + } +} + +module.exports = { + syncServerList +}; From 22ea2f56d36a6d3630f9bde14dde85326d3f1058 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Thu, 29 Jan 2026 19:00:13 +0100 Subject: [PATCH 63/88] Add Featured Servers page to GUI --- GUI/index.html | 39 ++ GUI/js/featured.js | 165 +++++ GUI/style.css | 1028 +++++++++++++++++++------------ backend/utils/serverListSync.js | 3 +- 4 files changed, 825 insertions(+), 410 deletions(-) create mode 100644 GUI/js/featured.js diff --git a/GUI/index.html b/GUI/index.html index 0f22cf8..8a041e2 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -35,6 +35,10 @@ Play
+ + +
@@ -880,6 +918,7 @@
+ diff --git a/GUI/js/featured.js b/GUI/js/featured.js new file mode 100644 index 0000000..9e44cc1 --- /dev/null +++ b/GUI/js/featured.js @@ -0,0 +1,165 @@ +// Featured Servers Management +const FEATURED_SERVERS_API = 'https://assets.authbp.xyz/featured.json'; + +/** + * Load and display featured servers + */ +async function loadFeaturedServers() { + const featuredContainer = document.getElementById('featuredServersList'); + const myServersContainer = document.getElementById('myServersList'); + + try { + console.log('[FeaturedServers] Fetching from', FEATURED_SERVERS_API); + + // Fetch featured servers from API (no cache) + const response = await fetch(FEATURED_SERVERS_API, { + cache: 'no-store', + headers: { + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + } + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + const featuredServers = data.featuredServers || []; + + console.log('[FeaturedServers] Loaded', featuredServers.length, 'featured servers'); + + // Render featured servers + if (featuredServers.length === 0) { + featuredContainer.innerHTML = ` +
+ +

No featured servers

+
+ `; + } else { + const featuredHTML = featuredServers.map((server, index) => { + console.log(`[FeaturedServers] Building featured card ${index + 1}:`, server.Name); + + const escapedName = (server.Name || 'Unknown Server').replace(/"/g, '"').replace(/'/g, ''').replace(//g, '>'); + const escapedAddress = (server.Address || '').replace(/"/g, '"').replace(/'/g, '''); + const bannerUrl = server.img_Banner || 'https://via.placeholder.com/400x240/1e293b/ffffff?text=Server+Banner'; + + return ` + + `; + }).join(''); + + featuredContainer.innerHTML = featuredHTML; + } + + // Show "Coming Soon" for my servers + myServersContainer.innerHTML = ` +
+

Coming Soon

+
+ `; + + } catch (error) { + console.error('[FeaturedServers] Error loading servers:', error); + featuredContainer.innerHTML = ` +
+ +

Failed to load servers

+

${error.message}

+
+ `; + myServersContainer.innerHTML = ` +
+

Coming Soon

+
+ `; + } +} + +/** + * Copy server address to clipboard + */ +async function copyServerAddress(address, button) { + try { + await navigator.clipboard.writeText(address); + + // Visual feedback + const originalHTML = button.innerHTML; + button.classList.add('copied'); + button.innerHTML = 'Copied!'; + + setTimeout(() => { + button.classList.remove('copied'); + button.innerHTML = originalHTML; + }, 2000); + + console.log('[FeaturedServers] Copied address:', address); + } catch (error) { + console.error('[FeaturedServers] Failed to copy address:', error); + + // Fallback for older browsers + const textArea = document.createElement('textarea'); + textArea.value = address; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + document.body.appendChild(textArea); + textArea.select(); + + try { + document.execCommand('copy'); + const originalHTML = button.innerHTML; + button.classList.add('copied'); + button.innerHTML = 'Copied!'; + + setTimeout(() => { + button.classList.remove('copied'); + button.innerHTML = originalHTML; + }, 2000); + } catch (err) { + console.error('[FeaturedServers] Fallback copy also failed:', err); + } + + document.body.removeChild(textArea); + } +} + +// Load featured servers when the featured page becomes visible +document.addEventListener('DOMContentLoaded', () => { + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + const featuredPage = document.getElementById('featured-page'); + if (featuredPage && featuredPage.classList.contains('active')) { + loadFeaturedServers(); + } + } + }); + }); + + const featuredPage = document.getElementById('featured-page'); + if (featuredPage) { + observer.observe(featuredPage, { attributes: true }); + + // Load immediately if already visible + if (featuredPage.classList.contains('active')) { + loadFeaturedServers(); + } + } +}); diff --git a/GUI/style.css b/GUI/style.css index 8a6e826..e591e78 100644 --- a/GUI/style.css +++ b/GUI/style.css @@ -1107,6 +1107,216 @@ body { padding-bottom: 1rem; } +/* Featured Servers Styles */ +.featured-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + height: calc(100vh - 180px); + overflow: hidden; +} + +.featured-left, +.featured-right { + display: flex; + flex-direction: column; + overflow: hidden; + min-height: 0; +} + +.featured-header { + margin-bottom: 1.5rem; + flex-shrink: 0; +} + +.featured-title { + font-size: 1.5rem; + font-weight: 700; + color: white; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.featured-list { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding-right: 0.5rem; + display: flex; + flex-direction: column; + gap: 1.25rem; + min-height: 0; +} + +.featured-list::-webkit-scrollbar { + width: 8px; +} + +.featured-list::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.3); + border-radius: 4px; +} + +.featured-list::-webkit-scrollbar-thumb { + background: rgba(147, 51, 234, 0.5); + border-radius: 4px; +} + +.featured-list::-webkit-scrollbar-thumb:hover { + background: rgba(147, 51, 234, 0.7); +} + +.featured-server-card { + position: relative; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + overflow: hidden; + transition: all 0.3s ease; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + display: grid; + grid-template-columns: 200px 1fr; + min-height: 120px; + flex-shrink: 0; +} + +.featured-server-card:hover { + transform: translateX(4px); + border-color: rgba(147, 51, 234, 0.5); + box-shadow: 0 8px 40px rgba(147, 51, 234, 0.2); +} + +.featured-server-banner { + width: 200px; + height: 100%; + min-height: 120px; + object-fit: cover; + background: linear-gradient(135deg, #1e293b, #334155); + flex-shrink: 0; +} + +.featured-server-content { + padding: 1.25rem; + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.75rem; +} + +.featured-server-name { + font-size: 1.15rem; + font-weight: 600; + color: white; + line-height: 1.4; + margin: 0; +} + +.featured-server-address { + display: flex; + align-items: center; + justify-content: space-between; + background: rgba(255, 255, 255, 0.05); + padding: 0.625rem 1rem; + border-radius: 6px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.server-address-text { + font-family: 'JetBrains Mono', monospace; + color: #94a3b8; + font-size: 0.9rem; +} + +.copy-address-btn { + background: linear-gradient(135deg, #9333ea, #7c3aed); + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 6px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; +} + +.copy-address-btn:hover { + background: linear-gradient(135deg, #7c3aed, #6d28d9); + transform: scale(1.05); +} + +.copy-address-btn:active { + transform: scale(0.95); +} + +.copy-address-btn.copied { + background: linear-gradient(135deg, #10b981, #059669); +} + +.loading-spinner { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem; + color: #94a3b8; + gap: 1rem; +} + +.loading-spinner i { + color: #9333ea; +} + +/* My server card - without banner */ +.my-server-card { + position: relative; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + overflow: hidden; + transition: all 0.3s ease; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + padding: 1.25rem; + flex-shrink: 0; +} + +.my-server-card:hover { + transform: translateX(4px); + border-color: rgba(147, 51, 234, 0.5); + box-shadow: 0 8px 40px rgba(147, 51, 234, 0.2); +} + +.my-server-content { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.my-server-name { + font-size: 1.15rem; + font-weight: 600; + color: white; + line-height: 1.4; + margin: 0; +} + +.my-server-address { + display: flex; + align-items: center; + justify-content: space-between; + background: rgba(255, 255, 255, 0.05); + padding: 0.625rem 1rem; + border-radius: 6px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + + .news-view-all:hover { color: white; } @@ -1770,252 +1980,252 @@ body { animation: shimmer 2s infinite; } -@keyframes shimmer { - 0% { - left: -100%; - } - - 100% { - left: 100%; - } -} - -/* Progress Error and Retry Styles */ -.progress-error-container { - margin-top: 0.75rem; - padding-top: 0.75rem; - border-top: 1px solid rgba(239, 68, 68, 0.3); - animation: errorSlideIn 0.3s ease-out; -} - -@keyframes errorSlideIn { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.progress-error-message { - color: #f87171; - font-family: 'JetBrains Mono', monospace; - font-size: 0.75rem; - margin-bottom: 0.5rem; - text-shadow: 0 0 8px rgba(248, 113, 113, 0.4); - line-height: 1.4; -} - -.progress-retry-section { - display: flex; - justify-content: space-between; - align-items: center; - gap: 0.75rem; -} - -.progress-retry-buttons { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.progress-retry-info { - color: #fbbf24; - font-family: 'JetBrains Mono', monospace; - font-size: 0.7rem; - flex: 1; -} - -.progress-retry-btn { - background: linear-gradient(135deg, #dc2626, #ef4444); - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 6px; - font-family: 'JetBrains Mono', monospace; - font-size: 0.75rem; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - box-shadow: 0 2px 8px rgba(220, 38, 38, 0.3); - white-space: nowrap; - min-width: 120px; -} - -.progress-retry-btn:hover { - background: linear-gradient(135deg, #b91c1c, #dc2626); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4); -} - -.progress-retry-btn:active { - transform: translateY(0); - box-shadow: 0 2px 6px rgba(220, 38, 38, 0.3); -} - -.progress-retry-btn:disabled { - background: linear-gradient(135deg, #4b5563, #6b7280); - cursor: not-allowed; - transform: none; - box-shadow: none; - opacity: 0.6; -} - -/* Progress overlay error state */ -.progress-overlay.error-state { - border-color: rgba(239, 68, 68, 0.5); - box-shadow: - 0 4px 16px rgba(0, 0, 0, 0.5), - 0 0 30px rgba(239, 68, 68, 0.2), - inset 0 1px 0 rgba(255, 255, 255, 0.05); -} - -.progress-overlay.error-state #progressBarFill { - background: linear-gradient(90deg, #dc2626, #ef4444); - animation: errorPulse 1.5s ease-in-out infinite; -} - -@keyframes errorPulse { - 0%, 100% { - opacity: 0.8; - } - 50% { - opacity: 1; - } -} - -/* Error type specific styling */ -.progress-error-container.error-network { - border-top-color: rgba(59, 130, 246, 0.5); -} - -.progress-error-container.error-network .progress-error-message { - color: #60a5fa; - text-shadow: 0 0 8px rgba(96, 165, 250, 0.4); -} - -.progress-error-container.error-stall { - border-top-color: rgba(245, 158, 11, 0.5); -} - -.progress-error-container.error-stall .progress-error-message { - color: #fbbf24; - text-shadow: 0 0 8px rgba(251, 191, 36, 0.4); -} - -.progress-error-container.error-file { - border-top-color: rgba(239, 68, 68, 0.5); -} - -.progress-error-container.error-file .progress-error-message { - color: #f87171; - text-shadow: 0 0 8px rgba(248, 113, 113, 0.4); -} - -.progress-error-container.error-permission { - border-top-color: rgba(168, 85, 247, 0.5); -} - -.progress-error-container.error-permission .progress-error-message { - color: #a855f7; - text-shadow: 0 0 8px rgba(168, 85, 247, 0.4); -} - -.progress-error-container.error-server { - border-top-color: rgba(236, 72, 153, 0.5); -} - -.progress-error-container.error-server .progress-error-message { - color: #ec4899; - text-shadow: 0 0 8px rgba(236, 72, 153, 0.4); -} - -.progress-error-container.error-corruption { - border-top-color: rgba(220, 38, 38, 0.8); -} - -.progress-error-container.error-corruption .progress-error-message { - color: #dc2626; - text-shadow: 0 0 8px rgba(220, 38, 38, 0.6); - font-weight: 600; -} - -.progress-error-container.error-butler { - border-top-color: rgba(245, 158, 11, 0.5); -} - -.progress-error-container.error-butler .progress-error-message { - color: #f59e0b; - text-shadow: 0 0 8px rgba(245, 158, 11, 0.4); -} - -.progress-error-container.error-space { - border-top-color: rgba(168, 85, 247, 0.5); -} - -.progress-error-container.error-space .progress-error-message { - color: #a855f7; - text-shadow: 0 0 8px rgba(168, 85, 247, 0.4); -} - -.progress-error-container.error-conflict { - border-top-color: rgba(6, 182, 212, 0.5); -} - -.progress-error-container.error-conflict .progress-error-message { - color: #06b6d4; - text-shadow: 0 0 8px rgba(6, 182, 212, 0.4); -} - -/* Connection quality indicators */ -.progress-details { - transition: all 0.3s ease; -} - -.progress-details #progressSize { - transition: color 0.5s ease; -} - -/* Enhanced retry button states */ -.progress-retry-btn.retrying { - background: linear-gradient(135deg, #059669, #10b981); - animation: retryingPulse 1s ease-in-out infinite; -} - -@keyframes retryingPulse { - 0%, 100% { - transform: scale(1); - box-shadow: 0 2px 8px rgba(5, 150, 105, 0.3); - } - 50% { - transform: scale(1.05); - box-shadow: 0 4px 12px rgba(5, 150, 105, 0.4); - } -} - -/* Network status indicator (optional future enhancement) */ -.network-status { - position: absolute; - top: 5px; - right: 5px; - width: 8px; - height: 8px; - border-radius: 50%; - background: #10b981; - box-shadow: 0 0 6px rgba(16, 185, 129, 0.6); -} - -.network-status.poor { - background: #f87171; - box-shadow: 0 0 6px rgba(248, 113, 113, 0.6); -} - -.network-status.fair { - background: #fbbf24; - box-shadow: 0 0 6px rgba(251, 191, 36, 0.6); -} +@keyframes shimmer { + 0% { + left: -100%; + } + + 100% { + left: 100%; + } +} + +/* Progress Error and Retry Styles */ +.progress-error-container { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid rgba(239, 68, 68, 0.3); + animation: errorSlideIn 0.3s ease-out; +} + +@keyframes errorSlideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.progress-error-message { + color: #f87171; + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; + margin-bottom: 0.5rem; + text-shadow: 0 0 8px rgba(248, 113, 113, 0.4); + line-height: 1.4; +} + +.progress-retry-section { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.75rem; +} + +.progress-retry-buttons { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.progress-retry-info { + color: #fbbf24; + font-family: 'JetBrains Mono', monospace; + font-size: 0.7rem; + flex: 1; +} + +.progress-retry-btn { + background: linear-gradient(135deg, #dc2626, #ef4444); + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 6px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(220, 38, 38, 0.3); + white-space: nowrap; + min-width: 120px; +} + +.progress-retry-btn:hover { + background: linear-gradient(135deg, #b91c1c, #dc2626); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4); +} + +.progress-retry-btn:active { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(220, 38, 38, 0.3); +} + +.progress-retry-btn:disabled { + background: linear-gradient(135deg, #4b5563, #6b7280); + cursor: not-allowed; + transform: none; + box-shadow: none; + opacity: 0.6; +} + +/* Progress overlay error state */ +.progress-overlay.error-state { + border-color: rgba(239, 68, 68, 0.5); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.5), + 0 0 30px rgba(239, 68, 68, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.progress-overlay.error-state #progressBarFill { + background: linear-gradient(90deg, #dc2626, #ef4444); + animation: errorPulse 1.5s ease-in-out infinite; +} + +@keyframes errorPulse { + 0%, 100% { + opacity: 0.8; + } + 50% { + opacity: 1; + } +} + +/* Error type specific styling */ +.progress-error-container.error-network { + border-top-color: rgba(59, 130, 246, 0.5); +} + +.progress-error-container.error-network .progress-error-message { + color: #60a5fa; + text-shadow: 0 0 8px rgba(96, 165, 250, 0.4); +} + +.progress-error-container.error-stall { + border-top-color: rgba(245, 158, 11, 0.5); +} + +.progress-error-container.error-stall .progress-error-message { + color: #fbbf24; + text-shadow: 0 0 8px rgba(251, 191, 36, 0.4); +} + +.progress-error-container.error-file { + border-top-color: rgba(239, 68, 68, 0.5); +} + +.progress-error-container.error-file .progress-error-message { + color: #f87171; + text-shadow: 0 0 8px rgba(248, 113, 113, 0.4); +} + +.progress-error-container.error-permission { + border-top-color: rgba(168, 85, 247, 0.5); +} + +.progress-error-container.error-permission .progress-error-message { + color: #a855f7; + text-shadow: 0 0 8px rgba(168, 85, 247, 0.4); +} + +.progress-error-container.error-server { + border-top-color: rgba(236, 72, 153, 0.5); +} + +.progress-error-container.error-server .progress-error-message { + color: #ec4899; + text-shadow: 0 0 8px rgba(236, 72, 153, 0.4); +} + +.progress-error-container.error-corruption { + border-top-color: rgba(220, 38, 38, 0.8); +} + +.progress-error-container.error-corruption .progress-error-message { + color: #dc2626; + text-shadow: 0 0 8px rgba(220, 38, 38, 0.6); + font-weight: 600; +} + +.progress-error-container.error-butler { + border-top-color: rgba(245, 158, 11, 0.5); +} + +.progress-error-container.error-butler .progress-error-message { + color: #f59e0b; + text-shadow: 0 0 8px rgba(245, 158, 11, 0.4); +} + +.progress-error-container.error-space { + border-top-color: rgba(168, 85, 247, 0.5); +} + +.progress-error-container.error-space .progress-error-message { + color: #a855f7; + text-shadow: 0 0 8px rgba(168, 85, 247, 0.4); +} + +.progress-error-container.error-conflict { + border-top-color: rgba(6, 182, 212, 0.5); +} + +.progress-error-container.error-conflict .progress-error-message { + color: #06b6d4; + text-shadow: 0 0 8px rgba(6, 182, 212, 0.4); +} + +/* Connection quality indicators */ +.progress-details { + transition: all 0.3s ease; +} + +.progress-details #progressSize { + transition: color 0.5s ease; +} + +/* Enhanced retry button states */ +.progress-retry-btn.retrying { + background: linear-gradient(135deg, #059669, #10b981); + animation: retryingPulse 1s ease-in-out infinite; +} + +@keyframes retryingPulse { + 0%, 100% { + transform: scale(1); + box-shadow: 0 2px 8px rgba(5, 150, 105, 0.3); + } + 50% { + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(5, 150, 105, 0.4); + } +} + +/* Network status indicator (optional future enhancement) */ +.network-status { + position: absolute; + top: 5px; + right: 5px; + width: 8px; + height: 8px; + border-radius: 50%; + background: #10b981; + box-shadow: 0 0 6px rgba(16, 185, 129, 0.6); +} + +.network-status.poor { + background: #f87171; + box-shadow: 0 0 6px rgba(248, 113, 113, 0.6); +} + +.network-status.fair { + background: #fbbf24; + box-shadow: 0 0 6px rgba(251, 191, 36, 0.6); +} .progress-bar-fill { height: 100%; @@ -5990,167 +6200,167 @@ select.settings-input option { to { opacity: 1; } -} -/* Launcher Update Modal Styles */ -.update-modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.85); - backdrop-filter: blur(10px); - display: flex; - align-items: center; - justify-content: center; - z-index: 100000; - animation: fadeIn 0.3s ease; -} - -.update-modal { - background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); - border: 2px solid rgba(147, 51, 234, 0.3); - border-radius: 16px; - padding: 2rem; - max-width: 500px; - width: 90%; - box-shadow: 0 20px 60px rgba(147, 51, 234, 0.3); - animation: slideUp 0.3s ease; -} - -@keyframes slideUp { - from { - transform: translateY(30px); - opacity: 0; - } - to { - transform: translateY(0); - opacity: 1; - } -} - -.update-header { - display: flex; - align-items: center; - gap: 1rem; - margin-bottom: 1.5rem; - color: #9333ea; -} - -.update-header i { - font-size: 2rem; -} - -.update-header h2 { - margin: 0; - font-size: 1.5rem; - color: #fff; -} - -.update-content { - color: #e0e0e0; - margin-bottom: 1.5rem; -} - -.update-version { - font-size: 1.2rem; - font-weight: 600; - color: #9333ea; - margin-bottom: 0.5rem; -} - -.current-version { - color: #888; - font-size: 0.9rem; - margin-bottom: 1rem; -} - -.release-notes { - background: rgba(0, 0, 0, 0.3); - border-left: 3px solid #9333ea; - padding: 1rem; - border-radius: 8px; - max-height: 200px; - overflow-y: auto; - font-size: 0.9rem; - line-height: 1.6; -} - -.update-progress { - margin-bottom: 1.5rem; -} - -.progress-bar-container { - background: rgba(0, 0, 0, 0.3); - border-radius: 10px; - height: 20px; - overflow: hidden; - margin-bottom: 0.5rem; -} - -.progress-bar { - height: 100%; - background: linear-gradient(90deg, #9333ea 0%, #7c3aed 100%); - width: 0%; - transition: width 0.3s ease; - border-radius: 10px; -} - -.progress-text { - color: #aaa; - font-size: 0.9rem; - text-align: center; - margin: 0; -} - -.update-note { - background: rgba(147, 51, 234, 0.1); - border: 1px solid rgba(147, 51, 234, 0.3); - padding: 0.75rem; - border-radius: 8px; - font-size: 0.9rem; - margin-top: 1rem; -} - -.update-actions { - display: flex; - gap: 1rem; - justify-content: flex-end; -} - -.update-actions button { - padding: 0.75rem 1.5rem; - border: none; - border-radius: 8px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.update-actions .btn-primary { - background: linear-gradient(135deg, #9333ea 0%, #7c3aed 100%); - color: white; -} - -.update-actions .btn-primary:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(147, 51, 234, 0.4); -} - -.update-actions .btn-secondary { - background: rgba(255, 255, 255, 0.1); - color: #e0e0e0; - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.update-actions .btn-secondary:hover:not(:disabled) { - background: rgba(255, 255, 255, 0.15); -} - -.update-actions button:disabled { - opacity: 0.5; - cursor: not-allowed; +} +/* Launcher Update Modal Styles */ +.update-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(10px); + display: flex; + align-items: center; + justify-content: center; + z-index: 100000; + animation: fadeIn 0.3s ease; +} + +.update-modal { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 2px solid rgba(147, 51, 234, 0.3); + border-radius: 16px; + padding: 2rem; + max-width: 500px; + width: 90%; + box-shadow: 0 20px 60px rgba(147, 51, 234, 0.3); + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + transform: translateY(30px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.update-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; + color: #9333ea; +} + +.update-header i { + font-size: 2rem; +} + +.update-header h2 { + margin: 0; + font-size: 1.5rem; + color: #fff; +} + +.update-content { + color: #e0e0e0; + margin-bottom: 1.5rem; +} + +.update-version { + font-size: 1.2rem; + font-weight: 600; + color: #9333ea; + margin-bottom: 0.5rem; +} + +.current-version { + color: #888; + font-size: 0.9rem; + margin-bottom: 1rem; +} + +.release-notes { + background: rgba(0, 0, 0, 0.3); + border-left: 3px solid #9333ea; + padding: 1rem; + border-radius: 8px; + max-height: 200px; + overflow-y: auto; + font-size: 0.9rem; + line-height: 1.6; +} + +.update-progress { + margin-bottom: 1.5rem; +} + +.progress-bar-container { + background: rgba(0, 0, 0, 0.3); + border-radius: 10px; + height: 20px; + overflow: hidden; + margin-bottom: 0.5rem; +} + +.progress-bar { + height: 100%; + background: linear-gradient(90deg, #9333ea 0%, #7c3aed 100%); + width: 0%; + transition: width 0.3s ease; + border-radius: 10px; +} + +.progress-text { + color: #aaa; + font-size: 0.9rem; + text-align: center; + margin: 0; +} + +.update-note { + background: rgba(147, 51, 234, 0.1); + border: 1px solid rgba(147, 51, 234, 0.3); + padding: 0.75rem; + border-radius: 8px; + font-size: 0.9rem; + margin-top: 1rem; +} + +.update-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; +} + +.update-actions button { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.update-actions .btn-primary { + background: linear-gradient(135deg, #9333ea 0%, #7c3aed 100%); + color: white; +} + +.update-actions .btn-primary:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(147, 51, 234, 0.4); +} + +.update-actions .btn-secondary { + background: rgba(255, 255, 255, 0.1); + color: #e0e0e0; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.update-actions .btn-secondary:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.15); +} + +.update-actions button:disabled { + opacity: 0.5; + cursor: not-allowed; } diff --git a/backend/utils/serverListSync.js b/backend/utils/serverListSync.js index 59e7e47..52ef6c6 100644 --- a/backend/utils/serverListSync.js +++ b/backend/utils/serverListSync.js @@ -90,7 +90,8 @@ async function syncServerList() { Id: uuidv4(), Name: "@ " + remoteServer.Name, Address: remoteServer.Address, - DateSaved: currentDate + DateSaved: currentDate, + img_Banner: remoteServer.img_Banner || null // Copy banner if exists }; apiServers.push(serverToAdd); console.log('[ServerListSync] Added/Updated server with new ID:', remoteServer.Name); From fbdd9ee0cfb52603b13c23607d9f07ee85751e7b Mon Sep 17 00:00:00 2001 From: AMIAY Date: Fri, 30 Jan 2026 02:28:45 +0100 Subject: [PATCH 64/88] Update Discord invite URL in client patcher --- backend/utils/clientPatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/utils/clientPatcher.js b/backend/utils/clientPatcher.js index 4f3dd10..7e4db4f 100644 --- a/backend/utils/clientPatcher.js +++ b/backend/utils/clientPatcher.js @@ -251,7 +251,7 @@ class ClientPatcher { const result = Buffer.from(data); const oldUrl = '.gg/hytale'; - const newUrl = '.gg/MHkEjepMQ7'; + const newUrl = '.gg/hf2pdc'; const lpResult = this.replaceBytes( result, From 33a0e219fc70e22c558912764dd7336b05c96875 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Fri, 30 Jan 2026 04:11:10 +0100 Subject: [PATCH 65/88] Add differential update system --- backend/core/testConfig.js | 7 + backend/managers/differentialUpdateManager.js | 272 ++++++++++++++++++ backend/managers/gameLauncher.js | 11 +- backend/managers/gameManager.js | 19 +- backend/services/versionManager.js | 142 ++++++++- 5 files changed, 437 insertions(+), 14 deletions(-) create mode 100644 backend/core/testConfig.js create mode 100644 backend/managers/differentialUpdateManager.js diff --git a/backend/core/testConfig.js b/backend/core/testConfig.js new file mode 100644 index 0000000..e6e9687 --- /dev/null +++ b/backend/core/testConfig.js @@ -0,0 +1,7 @@ +const FORCE_CLEAN_INSTALL_VERSION = false; +const CLEAN_INSTALL_TEST_VERSION = '4.pwr'; + +module.exports = { + FORCE_CLEAN_INSTALL_VERSION, + CLEAN_INSTALL_TEST_VERSION +}; diff --git a/backend/managers/differentialUpdateManager.js b/backend/managers/differentialUpdateManager.js new file mode 100644 index 0000000..5df790f --- /dev/null +++ b/backend/managers/differentialUpdateManager.js @@ -0,0 +1,272 @@ +const fs = require('fs'); +const path = require('path'); +const { execFile } = require('child_process'); +const { downloadFile, retryDownload } = require('../utils/fileManager'); +const { getOS, getArch } = require('../utils/platformUtils'); +const { validateChecksum, extractVersionDetails, canUseDifferentialUpdate, needsIntermediatePatches, getInstalledClientVersion } = require('../services/versionManager'); +const { installButler } = require('./butlerManager'); +const { GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths'); +const { saveVersionClient } = require('../core/config'); + +async function acquireGameArchive(downloadUrl, targetPath, checksum, progressCallback, allowRetry = true) { + const osName = getOS(); + const arch = getArch(); + + if (osName === 'darwin' && arch === 'amd64') { + throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.'); + } + + if (fs.existsSync(targetPath)) { + const stats = fs.statSync(targetPath); + if (stats.size > 1024 * 1024) { + const isValid = await validateChecksum(targetPath, checksum); + if (isValid) { + console.log(`Valid archive found in cache: ${targetPath}`); + return targetPath; + } + console.log('Cached archive checksum mismatch, re-downloading'); + fs.unlinkSync(targetPath); + } + } + + console.log(`Downloading game archive from: ${downloadUrl}`); + + try { + if (allowRetry) { + await retryDownload(downloadUrl, targetPath, progressCallback); + } else { + await downloadFile(downloadUrl, targetPath, progressCallback); + } + } catch (error) { + const enhancedError = new Error(`Archive download failed: ${error.message}`); + enhancedError.originalError = error; + enhancedError.downloadUrl = downloadUrl; + enhancedError.targetPath = targetPath; + throw enhancedError; + } + + const stats = fs.statSync(targetPath); + console.log(`Archive downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`); + + const isValid = await validateChecksum(targetPath, checksum); + if (!isValid) { + console.log('Downloaded archive checksum validation failed, removing corrupted file'); + fs.unlinkSync(targetPath); + throw new Error('Downloaded archive is corrupted or invalid. Please retry'); + } + + console.log(`Archive validation passed: ${targetPath}`); + return targetPath; +} + +async function deployGameArchive(archivePath, destinationDir, toolsDir, progressCallback, isDifferential = false) { + if (!archivePath || !fs.existsSync(archivePath)) { + throw new Error(`Archive not found: ${archivePath || 'undefined'}`); + } + + const stats = fs.statSync(archivePath); + console.log(`Deploying archive: ${archivePath}`); + console.log(`Archive size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`); + console.log(`Deployment mode: ${isDifferential ? 'differential' : 'full'}`); + + const butlerPath = await installButler(toolsDir); + const stagingDir = path.join(destinationDir, 'staging-temp'); + + if (!fs.existsSync(destinationDir)) { + fs.mkdirSync(destinationDir, { recursive: true }); + } + + if (fs.existsSync(stagingDir)) { + fs.rmSync(stagingDir, { recursive: true, force: true }); + } + fs.mkdirSync(stagingDir, { recursive: true }); + + if (progressCallback) { + progressCallback(isDifferential ? 'Applying differential update...' : 'Installing game files...', null, null, null, null); + } + + const args = [ + 'apply', + '--staging-dir', + stagingDir, + archivePath, + destinationDir + ]; + + console.log(`Executing deployment: ${butlerPath} ${args.join(' ')}`); + + return new Promise((resolve, reject) => { + const child = execFile(butlerPath, args, { + maxBuffer: 1024 * 1024 * 10, + timeout: 600000 + }, (error, stdout, stderr) => { + if (error) { + const cleanStderr = stderr.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim(); + const cleanStdout = stdout.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim(); + + if (cleanStderr) console.error('Deployment stderr:', cleanStderr); + if (cleanStdout) console.error('Deployment stdout:', cleanStdout); + + const errorText = (stderr + ' ' + error.message).toLowerCase(); + let message = 'Game deployment failed'; + + if (errorText.includes('unexpected eof')) { + message = 'Corrupted archive detected. Please retry download.'; + if (fs.existsSync(archivePath)) { + fs.unlinkSync(archivePath); + } + } else if (errorText.includes('permission denied')) { + message = 'Permission denied. Check file permissions and try again.'; + } else if (errorText.includes('no space left') || errorText.includes('device full')) { + message = 'Insufficient disk space. Free up space and try again.'; + } + + const deployError = new Error(message); + deployError.originalError = error; + deployError.stderr = cleanStderr; + deployError.stdout = cleanStdout; + return reject(deployError); + } + + console.log('Game deployment completed successfully'); + const cleanOutput = stdout.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim(); + if (cleanOutput) { + console.log(cleanOutput); + } + + if (fs.existsSync(stagingDir)) { + try { + fs.rmSync(stagingDir, { recursive: true, force: true }); + } catch (cleanupErr) { + console.warn('Failed to cleanup staging directory:', cleanupErr.message); + } + } + + resolve(); + }); + + child.on('error', (err) => { + console.error('Deployment process error:', err); + reject(new Error(`Failed to execute deployment tool: ${err.message}`)); + }); + }); +} + +async function performIntelligentUpdate(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) { + console.log(`Initiating intelligent update to version ${targetVersion}`); + + const currentVersion = getInstalledClientVersion(); + console.log(`Current version: ${currentVersion || 'none (clean install)'}`); + console.log(`Target version: ${targetVersion}`); + console.log(`Branch: ${branch}`); + + if (branch !== 'release') { + console.log(`Pre-release branch detected - forcing full archive download`); + const versionDetails = await extractVersionDetails(targetVersion, branch); + const archiveName = path.basename(versionDetails.fullUrl); + const archivePath = path.join(cacheDir, `${branch}_${archiveName}`); + + if (progressCallback) { + progressCallback('Downloading full game archive (pre-release)...', 0, null, null, null); + } + + await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback); + await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false); + saveVersionClient(targetVersion); + console.log(`Pre-release installation completed. Version ${targetVersion} is now installed.`); + return; + } + + if (!currentVersion) { + console.log('No existing installation detected - downloading full archive'); + const versionDetails = await extractVersionDetails(targetVersion, branch); + const archiveName = path.basename(versionDetails.fullUrl); + const archivePath = path.join(cacheDir, `${branch}_${archiveName}`); + + if (progressCallback) { + progressCallback(`Downloading full game archive (first install - v${targetVersion})...`, 0, null, null, null); + } + + await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback); + await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false); + saveVersionClient(targetVersion); + console.log(`Initial installation completed. Version ${targetVersion} is now installed.`); + return; + } + + const patchesToApply = needsIntermediatePatches(currentVersion, targetVersion); + + if (patchesToApply.length === 0) { + console.log('Already at target version or invalid version sequence'); + return; + } + + console.log(`Applying ${patchesToApply.length} differential patch(es): ${patchesToApply.join(' -> ')}`); + + for (let i = 0; i < patchesToApply.length; i++) { + const patchVersion = patchesToApply[i]; + const versionDetails = await extractVersionDetails(patchVersion, branch); + + const canDifferential = canUseDifferentialUpdate(getInstalledClientVersion(), versionDetails); + + if (!canDifferential || !versionDetails.differentialUrl) { + console.log(`WARNING: Differential patch not available for ${patchVersion}, using full archive`); + const archiveName = path.basename(versionDetails.fullUrl); + const archivePath = path.join(cacheDir, `${branch}_${archiveName}`); + + if (progressCallback) { + progressCallback(`Downloading full archive for ${patchVersion} (${i + 1}/${patchesToApply.length})...`, 0, null, null, null); + } + + await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback); + await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false); + } else { + console.log(`Applying differential patch: ${versionDetails.sourceVersion} -> ${patchVersion}`); + const archiveName = path.basename(versionDetails.differentialUrl); + const archivePath = path.join(cacheDir, `${branch}_patch_${archiveName}`); + + if (progressCallback) { + progressCallback(`Applying patch ${i + 1}/${patchesToApply.length}: ${patchVersion}...`, 0, null, null, null); + } + + await acquireGameArchive(versionDetails.differentialUrl, archivePath, versionDetails.checksum, progressCallback); + await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, true); + + if (fs.existsSync(archivePath)) { + try { + fs.unlinkSync(archivePath); + console.log(`Cleaned up patch file: ${archiveName}`); + } catch (cleanupErr) { + console.warn(`Failed to cleanup patch file: ${cleanupErr.message}`); + } + } + } + + saveVersionClient(patchVersion); + console.log(`Patch ${patchVersion} applied successfully (${i + 1}/${patchesToApply.length})`); + } + + console.log(`Update completed successfully. Version ${targetVersion} is now installed.`); +} + +async function ensureGameInstalled(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) { + const { findClientPath } = require('../core/paths'); + const clientPath = findClientPath(gameDir); + + if (clientPath) { + const currentVersion = getInstalledClientVersion(); + if (currentVersion === targetVersion) { + console.log(`Game already installed at correct version: ${targetVersion}`); + return; + } + } + + await performIntelligentUpdate(targetVersion, branch, progressCallback, gameDir, cacheDir, toolsDir); +} + +module.exports = { + acquireGameArchive, + deployGameArchive, + performIntelligentUpdate, + ensureGameInstalled +}; diff --git a/backend/managers/gameLauncher.js b/backend/managers/gameLauncher.js index 4555844..a1c43a4 100644 --- a/backend/managers/gameLauncher.js +++ b/backend/managers/gameLauncher.js @@ -10,7 +10,8 @@ const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platf const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain, loadVersionBranch, loadVersionClient, saveVersionClient } = require('../core/config'); const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager'); const { getLatestClientVersion } = require('../services/versionManager'); -const { updateGameFiles } = require('./gameManager'); +const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig'); +const { ensureGameInstalled } = require('./differentialUpdateManager'); const { syncModsForCurrentProfile } = require('./modManager'); const { getUserDataPath } = require('../utils/userDataMigration'); const { syncServerList } = require('../utils/serverListSync'); @@ -446,7 +447,13 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac const customCacheDir = path.join(customAppDir, 'cache'); try { - await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir, branch); + let versionToInstall = latestVersion; + if (FORCE_CLEAN_INSTALL_VERSION && !installedVersion) { + versionToInstall = CLEAN_INSTALL_TEST_VERSION; + console.log(`TESTING MODE: Clean install detected, forcing version ${versionToInstall} instead of ${latestVersion}`); + } + + await ensureGameInstalled(versionToInstall, branch, progressCallback, customGameDir, customCacheDir, customToolsDir); console.log('Game updated successfully, patching will be forced on launch...'); if (progressCallback) { diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index 139008d..966a50f 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -5,6 +5,7 @@ const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursi const { getOS, getArch } = require('../utils/platformUtils'); const { downloadFile, retryDownload, retryStalledDownload, MAX_AUTOMATIC_STALL_RETRIES } = require('../utils/fileManager'); const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager'); +const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig'); const { installButler } = require('./butlerManager'); const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager'); const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config'); @@ -528,31 +529,33 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver console.log(`Installing game files for branch: ${branch}...`); const latestVersion = await getLatestClientVersion(branch); + const targetVersion = FORCE_CLEAN_INSTALL_VERSION ? CLEAN_INSTALL_TEST_VERSION : latestVersion; + + if (FORCE_CLEAN_INSTALL_VERSION) { + console.log(`TESTING MODE: Forcing installation of ${targetVersion} instead of ${latestVersion}`); + } + let pwrFile; try { - pwrFile = await downloadPWR(branch, latestVersion, progressCallback, customCacheDir); + pwrFile = await downloadPWR(branch, targetVersion, progressCallback, customCacheDir); - // If downloadPWR returns false, it means the file doesn't exist or is invalid - // We should retry the download with a manual retry flag if (!pwrFile) { console.log('[Install] PWR file not found or invalid, attempting retry...'); - pwrFile = await retryPWRDownload(branch, latestVersion, progressCallback, customCacheDir); + pwrFile = await retryPWRDownload(branch, targetVersion, progressCallback, customCacheDir); } - // Double-check we have a valid file path if (!pwrFile || typeof pwrFile !== 'string') { throw new Error(`PWR file download failed: received invalid path ${pwrFile}. Please retry download.`); } } catch (downloadError) { console.error('[Install] PWR download failed:', downloadError.message); - throw downloadError; // Re-throw to be handled by the main installGame error handler + throw downloadError; } await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir, branch, customCacheDir); - // Save the installed version and branch to config - saveVersionClient(latestVersion); + saveVersionClient(targetVersion); const { saveVersionBranch } = require('../core/config'); saveVersionBranch(branch); diff --git a/backend/services/versionManager.js b/backend/services/versionManager.js index 3cf0010..ff4e037 100644 --- a/backend/services/versionManager.js +++ b/backend/services/versionManager.js @@ -1,11 +1,17 @@ const axios = require('axios'); +const crypto = require('crypto'); +const fs = require('fs'); +const { getOS, getArch } = require('../utils/platformUtils'); + +const BASE_PATCH_URL = 'https://game-patches.hytale.com/patches'; +const MANIFEST_API = 'https://files.hytalef2p.com/api/patch_manifest'; async function getLatestClientVersion(branch = 'release') { try { console.log(`Fetching latest client version from API (branch: ${branch})...`); const response = await axios.get('https://files.hytalef2p.com/api/version_client', { params: { branch }, - timeout: 40000, // fixed from 5000 to 40000 to make sure the client trying to connect on the server with slow internet + timeout: 40000, headers: { 'User-Agent': 'Hytale-F2P-Launcher' } @@ -16,16 +22,144 @@ async function getLatestClientVersion(branch = 'release') { console.log(`Latest client version for ${branch}: ${version}`); return version; } else { - console.log('Warning: Invalid API response, falling back to latest known version (7.pwr - 2026-01-29)'); // added latest version fallback and latest known version as per today + console.log('Warning: Invalid API response, falling back to latest known version (7.pwr)'); return '7.pwr'; } } catch (error) { console.error('Error fetching client version:', error.message); - console.log('Warning: API unavailable, falling back to latest known version (7.pwr - 2026-01-29)'); + console.log('Warning: API unavailable, falling back to latest known version (7.pwr)'); return '7.pwr'; } } +function buildArchiveUrl(buildNumber, branch = 'release') { + const os = getOS(); + const arch = getArch(); + return `${BASE_PATCH_URL}/${os}/${arch}/${branch}/0/${buildNumber}.pwr`; +} + +async function checkArchiveExists(buildNumber, branch = 'release') { + const url = buildArchiveUrl(buildNumber, branch); + try { + const response = await axios.head(url, { timeout: 10000 }); + return response.status === 200; + } catch (error) { + return false; + } +} + +async function discoverAvailableVersions(latestKnown, branch = 'release', maxProbe = 50) { + const available = []; + const latest = parseInt(latestKnown.replace('.pwr', '')); + + for (let i = latest; i >= Math.max(1, latest - maxProbe); i--) { + const exists = await checkArchiveExists(i, branch); + if (exists) { + available.push(`${i}.pwr`); + } + } + + return available; +} + +async function fetchPatchManifest(branch = 'release') { + try { + const os = getOS(); + const arch = getArch(); + const response = await axios.get(MANIFEST_API, { + params: { branch, os, arch }, + timeout: 10000 + }); + return response.data.patches || {}; + } catch (error) { + console.error('Failed to fetch patch manifest:', error.message); + return {}; + } +} + +async function extractVersionDetails(targetVersion, branch = 'release') { + const buildNumber = parseInt(targetVersion.replace('.pwr', '')); + const previousBuild = buildNumber - 1; + + const manifest = await fetchPatchManifest(branch); + const patchInfo = manifest[buildNumber]; + + return { + version: targetVersion, + buildNumber: buildNumber, + buildName: `HYTALE-Build-${buildNumber}`, + fullUrl: patchInfo?.original_url || buildArchiveUrl(buildNumber, branch), + differentialUrl: patchInfo?.patch_url || null, + checksum: patchInfo?.patch_hash || null, + sourceVersion: patchInfo?.from ? `${patchInfo.from}.pwr` : (previousBuild > 0 ? `${previousBuild}.pwr` : null), + isDifferential: !!patchInfo?.proper_patch, + releaseNotes: patchInfo?.patch_note || null + }; +} + +function canUseDifferentialUpdate(currentVersion, targetDetails) { + if (!targetDetails) return false; + if (!targetDetails.differentialUrl) return false; + if (!targetDetails.isDifferential) return false; + + if (!currentVersion) return false; + + const currentBuild = parseInt(currentVersion.replace('.pwr', '')); + const expectedSource = parseInt(targetDetails.sourceVersion?.replace('.pwr', '') || '0'); + + return currentBuild === expectedSource; +} + +function needsIntermediatePatches(currentVersion, targetVersion) { + if (!currentVersion) return []; + + const current = parseInt(currentVersion.replace('.pwr', '')); + const target = parseInt(targetVersion.replace('.pwr', '')); + + const intermediates = []; + for (let i = current + 1; i <= target; i++) { + intermediates.push(`${i}.pwr`); + } + + return intermediates; +} + +async function computeFileChecksum(filePath) { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('sha256'); + const stream = fs.createReadStream(filePath); + + stream.on('data', data => hash.update(data)); + stream.on('end', () => resolve(hash.digest('hex'))); + stream.on('error', reject); + }); +} + +async function validateChecksum(filePath, expectedChecksum) { + if (!expectedChecksum) return true; + + const actualChecksum = await computeFileChecksum(filePath); + return actualChecksum === expectedChecksum; +} + +function getInstalledClientVersion() { + try { + const { loadVersionClient } = require('../core/config'); + return loadVersionClient(); + } catch (err) { + return null; + } +} + module.exports = { - getLatestClientVersion + getLatestClientVersion, + buildArchiveUrl, + checkArchiveExists, + discoverAvailableVersions, + extractVersionDetails, + canUseDifferentialUpdate, + needsIntermediatePatches, + computeFileChecksum, + validateChecksum, + getInstalledClientVersion }; From 30a43276556ebfa3e480ba29bb4b6539d6acc604 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Fri, 30 Jan 2026 14:44:46 +0100 Subject: [PATCH 66/88] Remove launcher chat and add Discord popup --- GUI/index.html | 170 +++----------- GUI/js/chat.js | 500 ----------------------------------------- GUI/js/featured.js | 19 +- GUI/js/script.js | 76 ++++--- GUI/js/ui.js | 11 +- GUI/style.css | 232 ++++++++++--------- backend/core/config.js | 24 -- backend/launcher.js | 15 +- main.js | 27 +-- preload.js | 6 +- 10 files changed, 245 insertions(+), 835 deletions(-) delete mode 100644 GUI/js/chat.js diff --git a/GUI/index.html b/GUI/index.html index 8a041e2..8b8634c 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -47,10 +47,6 @@ News
- +
@@ -294,50 +294,6 @@
-
-
-
-

- - PLAYERS CHAT -

-
- -
- - 0 online -
-
-
- -
-
-
-
- - -
-
-
@@ -697,41 +653,6 @@
- -