From f438d6c8e0f0e1f2617c59b6b00fdc425428cd37 Mon Sep 17 00:00:00 2001 From: Terromur <79866197+Terromur@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:41:48 +0500 Subject: [PATCH 1/3] Update PKGBUILD Set png file from GUI/icon.png to 256x256 resolution for compatibility support. --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 4f05e42..e8eb44f 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -29,5 +29,5 @@ 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/icon.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/$_pkgname.png" + install -Dm644 "$_pkgname/GUI/icon.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/$_pkgname.png" } From 81c52e9507cbe92c3ce7c3c9964a0ce7c9dfb62e Mon Sep 17 00:00:00 2001 From: Terromur <79866197+Terromur@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:28:09 +0500 Subject: [PATCH 2/3] Fix icon --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index e8eb44f..12f6707 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -29,5 +29,5 @@ 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/512x512/apps/$_pkgname.png" + install -Dm644 "$_pkgname/GUI/icon.png" "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png" } From c4a32ce1e009a5f601e127a30ed21e2c7e73f3cc Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Mon, 26 Jan 2026 12:29:14 +0800 Subject: [PATCH 3/3] Release v2.1.1: Fix EPERM cross-platform error (#183) * 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. * fix: missing pacman builds * prepare release for 2.1.1 minor fix for EPERM error permission * Update README.md Windows Prequisites for ARM64 builds * fix: remove broken symlink after detected * fix: add pathexists for paths.js to check symlink * fix: isbrokenlink should be true to remove the symlink --- .github/workflows/release.yml | 1 + README.md | 21 +++++++++++++------- backend/core/paths.js | 24 ++++++++++++++++++++-- backend/managers/gameManager.js | 16 ++++++++++++++- backend/managers/modManager.js | 35 +++++++++++++++++++++++++-------- package-lock.json | 4 ++-- package.json | 2 +- 7 files changed, 82 insertions(+), 21 deletions(-) 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 diff --git a/README.md b/README.md index 3a7bcb2..ac43863 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.

@@ -160,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 diff --git a/backend/core/paths.js b/backend/core/paths.js index 17a7b92..78a5289 100644 --- a/backend/core/paths.js +++ b/backend/core/paths.js @@ -179,8 +179,28 @@ 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; + let pathExists = false; + try { + 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 + } + if (!pathExists || isBrokenLink) { + 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.'); 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", 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",