From cefb4c55752ed8b6154af4989636658f698e93f2 Mon Sep 17 00:00:00 2001 From: chasem-dev Date: Thu, 22 Jan 2026 00:03:02 -0500 Subject: [PATCH 01/95] Add electron-updater auto-update support - Install electron-updater package - Configure GitHub releases publish settings - Create AppUpdater class with full update lifecycle - Integrate auto-update into main.js - Add comprehensive documentation (AUTO-UPDATES.md, TESTING-UPDATES.md) - Set up dev-app-update.yml for testing --- AUTO-UPDATES.md | 284 ++++++++++++++++++++++++++++++++++++++++++ TESTING-UPDATES.md | 196 +++++++++++++++++++++++++++++ backend/appUpdater.js | 120 ++++++++++++++++++ dev-app-update.yml | 3 + main.js | 47 +++++-- package-lock.json | 95 ++++++++++++-- package.json | 75 +++++++++-- 7 files changed, 788 insertions(+), 32 deletions(-) create mode 100644 AUTO-UPDATES.md create mode 100644 TESTING-UPDATES.md create mode 100644 backend/appUpdater.js create mode 100644 dev-app-update.yml diff --git a/AUTO-UPDATES.md b/AUTO-UPDATES.md new file mode 100644 index 0000000..1d5ca63 --- /dev/null +++ b/AUTO-UPDATES.md @@ -0,0 +1,284 @@ +# Auto-Updates System + +This document explains how the automatic update system works in the Hytale F2P Launcher. + +## Overview + +The launcher uses [electron-updater](https://www.electron.build/auto-update) to automatically check for, download, and install updates. When a new version is available, users are notified and the update is downloaded in the background. + +## How It Works + +### 1. Update Checking + +- **Automatic Check**: The app automatically checks for updates 3 seconds after startup +- **Manual Check**: Users can manually check for updates through the UI +- **Update Source**: Updates are fetched from GitHub Releases + +### 2. Update Process + +1. **Check for Updates**: The app queries GitHub Releases for a newer version +2. **Notify User**: If an update is available, the user is notified via the UI +3. **Download**: The update is automatically downloaded in the background +4. **Progress Tracking**: Download progress is shown to the user +5. **Install**: When the download completes, the user can choose to install immediately or wait until the app restarts + +### 3. Installation + +- Updates are installed when the app quits (if `autoInstallOnAppQuit` is enabled) +- Users can also manually trigger installation through the UI +- The app will restart automatically after installation + +## Version Detection & Comparison + +### Current Version Source + +The app's current version is read from `package.json`: + +```json +{ + "version": "2.0.2b" +} +``` + +This version is embedded into the built application and is accessible via `app.getVersion()` in Electron. When the app is built, electron-builder also creates an internal `app-update.yml` file in the app's resources that contains this version information. + +### How Version Detection Works + +1. **Current Version**: The app knows its own version from `package.json`, which is: + - Read at build time + - Embedded in the application binary + - Stored in the app's metadata + +2. **Fetching Latest Version**: When checking for updates, electron-updater: + - Queries the GitHub Releases API: `https://api.github.com/repos/amiayweb/Hytale-F2P/releases/latest` + - Or reads the update metadata file: `https://github.com/amiayweb/Hytale-F2P/releases/download/latest/latest.yml` (or `latest-mac.yml` for macOS) + - The metadata file contains: + ```yaml + version: 2.0.3 + releaseDate: '2024-01-15T10:30:00.000Z' + path: Hytale-F2P-Launcher-2.0.3-x64.exe + sha512: ... + ``` + +3. **Version Comparison**: electron-updater uses semantic versioning comparison: + - Compares the **current version** (from `package.json`) with the **latest version** (from GitHub Releases) + - Uses semantic versioning rules: `major.minor.patch` (e.g., `2.0.2` vs `2.0.3`) + - An update is available if the remote version is **greater than** the current version + - Examples: + - Current: `2.0.2` → Remote: `2.0.3` ✅ Update available + - Current: `2.0.2` → Remote: `2.0.2` ❌ No update (same version) + - Current: `2.0.3` → Remote: `2.0.2` ❌ No update (current is newer) + - Current: `2.0.2b` → Remote: `2.0.3` ✅ Update available (prerelease tags are handled) + +4. **Version Format Handling**: + - **Semantic versions** (e.g., `1.0.0`, `2.1.3`) are compared numerically + - **Prerelease versions** (e.g., `2.0.2b`, `2.0.2-beta`) are compared with special handling + - **Non-semantic versions** may cause issues - it's recommended to use semantic versioning + +### Update Metadata Files + +When you build and publish a release, electron-builder generates platform-specific metadata files: + +**Windows/Linux** (`latest.yml`): +```yaml +version: 2.0.3 +files: + - url: Hytale-F2P-Launcher-2.0.3-x64.exe + sha512: abc123... + size: 12345678 +path: Hytale-F2P-Launcher-2.0.3-x64.exe +sha512: abc123... +releaseDate: '2024-01-15T10:30:00.000Z' +``` + +**macOS** (`latest-mac.yml`): +```yaml +version: 2.0.3 +files: + - url: Hytale-F2P-Launcher-2.0.3-arm64-mac.zip + sha512: def456... + size: 23456789 +path: Hytale-F2P-Launcher-2.0.3-arm64-mac.zip +sha512: def456... +releaseDate: '2024-01-15T10:30:00.000Z' +``` + +These files are: +- Automatically generated during build +- Uploaded to GitHub Releases +- Fetched by electron-updater to check for updates +- Used to determine if an update is available and what to download + +### The Check Process in Detail + +When `appUpdater.checkForUpdatesAndNotify()` is called: + +1. **Read Current Version**: Gets version from `app.getVersion()` (which reads from `package.json`) +2. **Fetch Update Info**: + - Makes HTTP request to GitHub Releases API or reads `latest.yml` + - Gets the version number from the metadata +3. **Compare Versions**: + - Uses semantic versioning comparison (e.g., `semver.gt(remoteVersion, currentVersion)`) + - If remote > current: update available + - If remote <= current: no update +4. **Emit Events**: + - `update-available` if newer version found + - `update-not-available` if already up to date +5. **Download if Available**: If `autoDownload` is enabled, starts downloading automatically + +### Example Flow + +``` +App Version: 2.0.2 (from package.json) + ↓ +Check GitHub Releases API + ↓ +Latest Release: 2.0.3 + ↓ +Compare: 2.0.3 > 2.0.2? YES + ↓ +Emit: 'update-available' event + ↓ +Download update automatically + ↓ +Emit: 'update-downloaded' event + ↓ +User can install on next restart +``` + +## Components + +### AppUpdater Class (`backend/appUpdater.js`) + +The main class that handles all update operations: + +- **`checkForUpdatesAndNotify()`**: Checks for updates and shows a system notification if available +- **`checkForUpdates()`**: Manually checks for updates (returns a promise) +- **`quitAndInstall()`**: Quits the app and installs the downloaded update + +### Events + +The AppUpdater emits the following events that the UI can listen to: + +- `update-checking`: Update check has started +- `update-available`: A new update is available +- `update-not-available`: App is up to date +- `update-download-progress`: Download progress updates +- `update-downloaded`: Update has finished downloading +- `update-error`: An error occurred during the update process + +## Configuration + +### Package.json + +The publish configuration in `package.json` tells electron-builder where to publish updates: + +```json +"publish": { + "provider": "github", + "owner": "amiayweb", + "repo": "Hytale-F2P" +} +``` + +This means updates will be fetched from GitHub Releases for the `amiayweb/Hytale-F2P` repository. + +## Publishing Updates + +### For Developers + +1. **Update Version**: Bump the version in `package.json` (e.g., `2.0.2b` → `2.0.3`) + +2. **Build the App**: Run the build command for your platform: + ```bash + npm run build:win # Windows + npm run build:mac # macOS + npm run build:linux # Linux + ``` + +3. **Publish to GitHub**: When building with electron-builder, it will: + - Generate update metadata files (`latest.yml`, `latest-mac.yml`, etc.) + - Upload the built files to GitHub Releases (if configured with `GH_TOKEN`) + - Make them available for auto-update + +4. **Release on GitHub**: Create a GitHub Release with the new version tag + +### Important Notes + +- **macOS Code Signing**: macOS apps **must** be code-signed for auto-updates to work +- **Version Format**: Use semantic versioning (e.g., `1.0.0`, `2.0.1`) for best compatibility +- **Update Files**: electron-builder automatically generates the required metadata files (`latest.yml`, etc.) + +## Testing Updates + +### Development Mode + +To test updates during development, create a `dev-app-update.yml` file in the project root: + +```yaml +owner: amiayweb +repo: Hytale-F2P +provider: github +``` + +Then enable dev mode in the code: +```javascript +autoUpdater.forceDevUpdateConfig = true; +``` + +### Local Testing + +For local testing, you can use a local server (like Minio) or a generic HTTP server to host update files. + +## User Experience + +### What Users See + +1. **On Startup**: The app silently checks for updates in the background +2. **Update Available**: A notification appears if an update is found +3. **Downloading**: Progress bar shows download status +4. **Ready to Install**: User is notified when the update is ready +5. **Installation**: Update installs on app restart or when user clicks "Install Now" + +### User Actions + +- Users can manually check for updates through the settings/update menu +- Users can choose to install immediately or wait until next app launch +- Users can continue using the app while updates download in the background + +## Troubleshooting + +### Updates Not Working + +1. **Check GitHub Releases**: Ensure releases are published on GitHub +2. **Check Version**: Make sure the version in `package.json` is higher than the current release +3. **Check Logs**: Check the app logs for update-related errors +4. **Code Signing (macOS)**: Verify the app is properly code-signed + +### Common Issues + +- **"Update not available"**: Version in `package.json` may not be higher than the current release +- **"Download failed"**: Network issues or GitHub API rate limits +- **"Installation failed"**: Permissions issue or app is running from an unsupported location + +## Technical Details + +### Supported Platforms + +- **Windows**: NSIS installer (auto-update supported) +- **macOS**: DMG + ZIP (auto-update supported, requires code signing) +- **Linux**: AppImage, DEB, RPM, Pacman (auto-update supported) + +### Update Files Generated + +When building, electron-builder generates: +- `latest.yml` (Windows/Linux) +- `latest-mac.yml` (macOS) +- `latest-linux.yml` (Linux) + +These files contain metadata about the latest release and are automatically uploaded to GitHub Releases. + +## References + +- [electron-updater Documentation](https://www.electron.build/auto-update) +- [electron-builder Auto Update Guide](https://www.electron.build/auto-update) diff --git a/TESTING-UPDATES.md b/TESTING-UPDATES.md new file mode 100644 index 0000000..0103272 --- /dev/null +++ b/TESTING-UPDATES.md @@ -0,0 +1,196 @@ +# Testing Auto-Updates + +This guide explains how to test the auto-update system during development. + +## Quick Start + +### Option 1: Test with GitHub Releases (Easiest) + +1. **Set up dev-app-update.yml** (already done): + ```yaml + provider: github + owner: amiayweb + repo: Hytale-F2P + ``` + +2. **Lower your current version** in `package.json`: + - Change version to something lower than what's on GitHub (e.g., `2.0.1` if GitHub has `2.0.3`) + +3. **Run the app in dev mode**: + ```bash + npm run dev + # or + npm start + ``` + +4. **The app will check for updates** 3 seconds after startup + - If a newer version exists on GitHub, it will detect it + - Check the console logs for update messages + +### Option 2: Test with Local HTTP Server + +For more control, you can set up a local server: + +1. **Create a test update server**: + ```bash + # Create a test directory + mkdir -p test-updates + cd test-updates + ``` + +2. **Build a test version** with a higher version number: + ```bash + # In package.json, set version to 2.0.4 + npm run build + ``` + +3. **Copy the generated files** to your test server: + - Copy `dist/latest.yml` (or `latest-mac.yml` for macOS) + - Copy the built installer/package + +4. **Start a simple HTTP server**: + ```bash + # Using Python + python3 -m http.server 8080 + + # Or using Node.js http-server + npx http-server -p 8080 + ``` + +5. **Update dev-app-update.yml** to point to local server: + ```yaml + provider: generic + url: http://localhost:8080 + ``` + +6. **Run the app** and it will check your local server + +## Testing Steps + +### 1. Prepare Test Environment + +**Current version**: `2.0.3` (in package.json) +**Test version**: `2.0.4` (on GitHub or local server) + +### 2. Run the App + +```bash +npm run dev +``` + +### 3. Watch for Update Events + +The app will automatically check for updates 3 seconds after startup. Watch the console for: + +``` +Checking for updates... +Update available: 2.0.4 +``` + +### 4. Check Console Logs + +Look for these messages: +- `Checking for updates...` - Update check started +- `Update available: 2.0.4` - New version found +- `Download speed: ...` - Download progress +- `Update downloaded: 2.0.4` - Download complete + +### 5. Test UI Integration + +The app sends these events to the renderer: +- `update-checking` +- `update-available` (with version info) +- `update-download-progress` (with progress data) +- `update-downloaded` (ready to install) + +You can listen to these in your frontend code to show update notifications. + +## Manual Testing + +### Trigger Manual Update Check + +You can also trigger a manual check via IPC: +```javascript +// In renderer process +const result = await window.electronAPI.invoke('check-for-updates'); +console.log(result); +``` + +### Install Update + +After an update is downloaded: +```javascript +// In renderer process +await window.electronAPI.invoke('quit-and-install-update'); +``` + +## Testing Scenarios + +### Scenario 1: Update Available +1. Set `package.json` version to `2.0.1` +2. Ensure GitHub has version `2.0.3` or higher +3. Run app → Should detect update + +### Scenario 2: Already Up to Date +1. Set `package.json` version to `2.0.3` +2. Ensure GitHub has version `2.0.3` or lower +3. Run app → Should show "no update available" + +### Scenario 3: Prerelease Version +1. Set `package.json` version to `2.0.2b` +2. Ensure GitHub has version `2.0.3` +3. Run app → Should detect update (prerelease < release) + +## Troubleshooting + +### Update Not Detected + +1. **Check dev-app-update.yml exists** in project root +2. **Verify dev mode is enabled** - Check console for "Dev update mode enabled" +3. **Check version numbers** - Remote version must be higher than current +4. **Check network** - App needs internet to reach GitHub/local server +5. **Check logs** - Look for error messages in console + +### Common Errors + +- **"Cannot find module 'electron-updater'"**: Run `npm install` +- **"Update check failed"**: Check network connection or GitHub API access +- **"No update available"**: Version comparison issue - check versions + +### Debug Mode + +Enable more verbose logging by checking the console output. The logger will show: +- Update check requests +- Version comparisons +- Download progress +- Any errors + +## Testing with Real GitHub Releases + +For the most realistic test: + +1. **Create a test release on GitHub**: + - Build the app with version `2.0.4` + - Create a GitHub release with tag `v2.0.4` + - Upload the built files + +2. **Lower your local version**: + - Set `package.json` to `2.0.3` + +3. **Run the app**: + - It will check GitHub and find `2.0.4` + - Download and install the update + +## Notes + +- **Dev mode only works when app is NOT packaged** (`!app.isPackaged`) +- **Production builds** ignore `dev-app-update.yml` and use the built-in `app-update.yml` +- **macOS**: Code signing is required for updates to work in production +- **Windows**: NSIS installer is required for auto-updates + +## Next Steps + +Once testing is complete: +1. Remove or comment out `forceDevUpdateConfig` for production +2. Ensure proper code signing for macOS +3. Set up CI/CD to automatically publish releases diff --git a/backend/appUpdater.js b/backend/appUpdater.js new file mode 100644 index 0000000..001b711 --- /dev/null +++ b/backend/appUpdater.js @@ -0,0 +1,120 @@ +const { autoUpdater } = require('electron-updater'); +const { app } = require('electron'); +const logger = require('./logger'); + +class AppUpdater { + constructor(mainWindow) { + this.mainWindow = mainWindow; + this.setupAutoUpdater(); + } + + setupAutoUpdater() { + // Enable dev mode for testing (reads dev-app-update.yml) + // Only enable in development, not in production builds + if (process.env.NODE_ENV === 'development' || !app.isPackaged) { + autoUpdater.forceDevUpdateConfig = true; + console.log('Dev update mode enabled - using dev-app-update.yml'); + } + + // Configure logger for electron-updater + // Create a compatible logger interface + autoUpdater.logger = { + info: (...args) => logger.info(...args), + warn: (...args) => logger.warn(...args), + error: (...args) => logger.error(...args), + debug: (...args) => logger.log(...args) + }; + + // Auto download updates + autoUpdater.autoDownload = true; + // Auto install on quit (after download) + autoUpdater.autoInstallOnAppQuit = true; + + // Event handlers + autoUpdater.on('checking-for-update', () => { + console.log('Checking for updates...'); + if (this.mainWindow && !this.mainWindow.isDestroyed()) { + this.mainWindow.webContents.send('update-checking'); + } + }); + + autoUpdater.on('update-available', (info) => { + console.log('Update available:', info.version); + if (this.mainWindow && !this.mainWindow.isDestroyed()) { + this.mainWindow.webContents.send('update-available', { + version: info.version, + releaseName: info.releaseName, + releaseNotes: info.releaseNotes + }); + } + }); + + autoUpdater.on('update-not-available', (info) => { + console.log('Update not available. Current version is latest.'); + if (this.mainWindow && !this.mainWindow.isDestroyed()) { + this.mainWindow.webContents.send('update-not-available', { + version: info.version + }); + } + }); + + autoUpdater.on('error', (err) => { + console.error('Error in auto-updater:', err); + if (this.mainWindow && !this.mainWindow.isDestroyed()) { + this.mainWindow.webContents.send('update-error', { + message: err.message + }); + } + }); + + autoUpdater.on('download-progress', (progressObj) => { + const message = `Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`; + console.log(message); + if (this.mainWindow && !this.mainWindow.isDestroyed()) { + this.mainWindow.webContents.send('update-download-progress', { + percent: progressObj.percent, + bytesPerSecond: progressObj.bytesPerSecond, + transferred: progressObj.transferred, + total: progressObj.total + }); + } + }); + + autoUpdater.on('update-downloaded', (info) => { + console.log('Update downloaded:', info.version); + if (this.mainWindow && !this.mainWindow.isDestroyed()) { + this.mainWindow.webContents.send('update-downloaded', { + version: info.version, + releaseName: info.releaseName, + releaseNotes: info.releaseNotes + }); + } + }); + } + + checkForUpdatesAndNotify() { + // Check for updates and notify if available + autoUpdater.checkForUpdatesAndNotify().catch(err => { + console.error('Failed to check for updates:', err); + }); + } + + checkForUpdates() { + // Manual check for updates (returns promise) + return autoUpdater.checkForUpdates(); + } + + quitAndInstall() { + // Quit and install the update + autoUpdater.quitAndInstall(false, true); + } + + getUpdateInfo() { + return { + currentVersion: app.getVersion(), + updateAvailable: false + }; + } +} + +module.exports = AppUpdater; diff --git a/dev-app-update.yml b/dev-app-update.yml new file mode 100644 index 0000000..e171683 --- /dev/null +++ b/dev-app-update.yml @@ -0,0 +1,3 @@ +provider: github +owner: amiayweb +repo: Hytale-F2P diff --git a/main.js b/main.js index b7e639a..d6eb302 100644 --- a/main.js +++ b/main.js @@ -2,14 +2,14 @@ const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const path = require('path'); const fs = require('fs'); const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); -const UpdateManager = require('./backend/updateManager'); +const AppUpdater = require('./backend/appUpdater'); const logger = require('./backend/logger'); const profileManager = require('./backend/managers/profileManager'); logger.interceptConsole(); let mainWindow; -let updateManager; +let appUpdater; let discordRPC = null; // Discord Rich Presence setup @@ -113,11 +113,13 @@ function createWindow() { // Initialize Discord Rich Presence initDiscordRPC(); - updateManager = new UpdateManager(); - setTimeout(async () => { - const updateInfo = await updateManager.checkForUpdates(); - if (updateInfo.updateAvailable) { - mainWindow.webContents.send('show-update-popup', updateInfo); + // Initialize App Updater + appUpdater = new AppUpdater(mainWindow); + + // Check for updates after a short delay (3 seconds) + setTimeout(() => { + if (appUpdater) { + appUpdater.checkForUpdatesAndNotify(); } }, 3000); @@ -724,7 +726,15 @@ ipcMain.handle('copy-mod-file', async (event, sourcePath, modsPath) => { ipcMain.handle('check-for-updates', async () => { try { - return await updateManager.checkForUpdates(); + if (appUpdater) { + const result = await appUpdater.checkForUpdates(); + return { + updateAvailable: result?.updateInfo ? true : false, + version: result?.updateInfo?.version, + currentVersion: app.getVersion() + }; + } + return { updateAvailable: false, error: 'AppUpdater not initialized' }; } catch (error) { console.error('Error checking for updates:', error); return { updateAvailable: false, error: error.message }; @@ -733,7 +743,8 @@ ipcMain.handle('check-for-updates', async () => { ipcMain.handle('open-download-page', async () => { try { - await shell.openExternal(updateManager.getDownloadUrl()); + // Open GitHub releases page + await shell.openExternal('https://github.com/amiayweb/Hytale-F2P/releases'); setTimeout(() => { if (mainWindow && !mainWindow.isDestroyed()) { @@ -748,8 +759,24 @@ ipcMain.handle('open-download-page', async () => { } }); +ipcMain.handle('quit-and-install-update', async () => { + try { + if (appUpdater) { + appUpdater.quitAndInstall(); + return { success: true }; + } + return { success: false, error: 'AppUpdater not initialized' }; + } catch (error) { + console.error('Error installing update:', error); + return { success: false, error: error.message }; + } +}); + ipcMain.handle('get-update-info', async () => { - return updateManager.getUpdateInfo(); + if (appUpdater) { + return appUpdater.getUpdateInfo(); + } + return { currentVersion: app.getVersion(), updateAvailable: false }; }); ipcMain.handle('get-gpu-info', () => { diff --git a/package-lock.json b/package-lock.json index 90d6855..6efcf38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { - "name": "hytale-f2p-launcherv2", + "name": "hytale-f2p-launcher", "version": "2.0.2b", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "hytale-f2p-launcherv2", + "name": "hytale-f2p-launcher", "version": "2.0.2b", "license": "MIT", "dependencies": { "adm-zip": "^0.5.10", "axios": "^1.6.0", "discord-rpc": "^4.0.1", + "electron-updater": "^6.7.3", "tar": "^6.2.1", "uuid": "^9.0.1" }, @@ -1073,7 +1074,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/assert-plus": { @@ -1283,7 +1283,6 @@ "version": "9.5.1", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -1711,7 +1710,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2178,6 +2176,69 @@ "node": ">= 10.0.0" } }, + "node_modules/electron-updater": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.7.3.tgz", + "integrity": "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.5.1", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "~7.7.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-winstaller": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", @@ -2759,7 +2820,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-flag": { @@ -3082,7 +3142,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3150,7 +3209,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, "license": "MIT" }, "node_modules/lodash": { @@ -3160,6 +3218,19 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -3476,7 +3547,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/negotiator": { @@ -4131,7 +4201,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -4602,6 +4671,12 @@ "semver": "bin/semver" } }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/package.json b/package.json index 51dd605..9ce03ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hytale-f2p-launcher", - "version": "2.0.2b", + "version": "2.0.3", "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", @@ -24,7 +24,7 @@ "mod-manager", "chat" ], - "maintainers": [ + "maintainers": [ { "name": "Terromur", "url": "https://github.com/Terromur" @@ -33,7 +33,7 @@ "name": "Fari Gading", "email": "fazrigading@gmail.com", "url": "https://github.com/fazrigading" - } + } ], "author": { "name": "AMIAY", @@ -48,6 +48,7 @@ "adm-zip": "^0.5.10", "axios": "^1.6.0", "discord-rpc": "^4.0.1", + "electron-updater": "^6.7.3", "tar": "^6.2.1", "uuid": "^9.0.1" }, @@ -70,25 +71,70 @@ ], "win": { "target": [ - { "target": "nsis", "arch": ["x64", "arm64"] }, - { "target": "portable", "arch": ["x64"] } + { + "target": "nsis", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "portable", + "arch": [ + "x64" + ] + } ], "icon": "icon.ico" }, "linux": { "target": [ - { "target": "AppImage", "arch": ["x64", "arm64"] }, - { "target": "deb", "arch": ["x64", "arm64"] }, - { "target": "rpm", "arch": ["x64", "arm64"] }, - { "target": "pacman", "arch": ["x64", "arm64"] } + { + "target": "AppImage", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "deb", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "rpm", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "pacman", + "arch": [ + "x64", + "arm64" + ] + } ], "icon": "build/icon.png", "category": "Game" }, "mac": { "target": [ - { "target": "dmg", "arch": ["universal"] }, - { "target": "zip", "arch": ["universal"] } + { + "target": "dmg", + "arch": [ + "universal" + ] + }, + { + "target": "zip", + "arch": [ + "universal" + ] + } ], "icon": "build/icon.icns", "category": "public.app-category.games" @@ -98,6 +144,11 @@ "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true, "createStartMenuShortcut": true + }, + "publish": { + "provider": "github", + "owner": "amiayweb", + "repo": "Hytale-F2P" } } -} \ No newline at end of file +} From 753bd4fd61c38c505cc119b118891db036f93cb1 Mon Sep 17 00:00:00 2001 From: chasem-dev Date: Thu, 22 Jan 2026 00:26:01 -0500 Subject: [PATCH 02/95] Add cache clearing documentation for electron-updater - Introduced CLEAR-UPDATE-CACHE.md to guide users on clearing the electron-updater cache across macOS, Windows, and Linux. - Added programmatic method for cache clearing in JavaScript. - Enhanced update handling in main.js and preload.js to support new update events. - Updated GUI styles for download buttons and progress indicators in update.js and style.css. --- CLEAR-UPDATE-CACHE.md | 78 ++++++++++++++++++++++++++ GUI/js/update.js | 125 +++++++++++++++++++++++++++++++++++++++--- GUI/style.css | 21 +++++++ backend/appUpdater.js | 8 +++ main.js | 1 + preload.js | 10 ++++ 6 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 CLEAR-UPDATE-CACHE.md diff --git a/CLEAR-UPDATE-CACHE.md b/CLEAR-UPDATE-CACHE.md new file mode 100644 index 0000000..c72d89c --- /dev/null +++ b/CLEAR-UPDATE-CACHE.md @@ -0,0 +1,78 @@ +# Clearing Electron-Updater Cache + +To force electron-updater to re-download an update file, you need to clear the cached download. + +## Quick Method (Terminal) + +### macOS +```bash +# Remove the entire cache directory +rm -rf ~/Library/Caches/hytale-f2p-launcher + +# Or just remove pending downloads +rm -rf ~/Library/Caches/hytale-f2p-launcher/pending +``` + +### Windows +```bash +# Remove the entire cache directory +rmdir /s "%LOCALAPPDATA%\hytale-f2p-launcher-updater" + +# Or just remove pending downloads +rmdir /s "%LOCALAPPDATA%\hytale-f2p-launcher-updater\pending" +``` + +### Linux +```bash +# Remove the entire cache directory +rm -rf ~/.cache/hytale-f2p-launcher-updater + +# Or just remove pending downloads +rm -rf ~/.cache/hytale-f2p-launcher-updater/pending +``` + +## Cache Locations + +electron-updater stores downloaded updates in: + +- **macOS**: `~/Library/Caches/hytale-f2p-launcher/` +- **Windows**: `%LOCALAPPDATA%\hytale-f2p-launcher-updater\` +- **Linux**: `~/.cache/hytale-f2p-launcher-updater/` + +The cache typically contains: +- `pending/` - Downloaded update files waiting to be installed +- Metadata files about available updates + +## After Clearing + +After clearing the cache: +1. Restart the launcher +2. It will check for updates again +3. The update will be re-downloaded from scratch + +## Programmatic Method + +You can also clear the cache programmatically by adding this to your code: + +```javascript +const { autoUpdater } = require('electron-updater'); +const path = require('path'); +const fs = require('fs'); +const os = require('os'); + +function clearUpdateCache() { + const cacheDir = path.join( + os.homedir(), + process.platform === 'win32' + ? 'AppData/Local/hytale-f2p-launcher-updater' + : process.platform === 'darwin' + ? 'Library/Caches/hytale-f2p-launcher' + : '.cache/hytale-f2p-launcher-updater' + ); + + if (fs.existsSync(cacheDir)) { + fs.rmSync(cacheDir, { recursive: true, force: true }); + console.log('Update cache cleared'); + } +} +``` diff --git a/GUI/js/update.js b/GUI/js/update.js index 00393b4..bd1403c 100644 --- a/GUI/js/update.js +++ b/GUI/js/update.js @@ -10,6 +10,19 @@ class ClientUpdateManager { this.showUpdatePopup(updateInfo); }); + // Listen for electron-updater events + window.electronAPI.onUpdateAvailable((updateInfo) => { + this.showUpdatePopup(updateInfo); + }); + + window.electronAPI.onUpdateDownloadProgress((progress) => { + this.updateDownloadProgress(progress); + }); + + window.electronAPI.onUpdateDownloaded((updateInfo) => { + this.showUpdateDownloaded(updateInfo); + }); + this.checkForUpdatesOnDemand(); } @@ -33,23 +46,42 @@ class ClientUpdateManager {
Current Version: - ${updateInfo.currentVersion} + ${updateInfo.currentVersion || updateInfo.version || 'Unknown'}
New Version: - ${updateInfo.newVersion} + ${updateInfo.newVersion || updateInfo.version || 'Unknown'}
A new version of Hytale F2P Launcher is available.
- Please download the latest version to continue using the launcher. + Downloading update automatically...
- + + + - -
@@ -114,7 +110,7 @@

- HYTALE + HYTALE

FREE TO PLAY LAUNCHER

@@ -462,14 +458,6 @@
-
-
- -

Skins

-

Skin customization coming soon...

-
-
-
@@ -532,6 +520,20 @@
+ + + diff --git a/GUI/js/mods.js b/GUI/js/mods.js index 32f4ddd..631db3f 100644 --- a/GUI/js/mods.js +++ b/GUI/js/mods.js @@ -1,5 +1,5 @@ -const API_KEY = '$2a$10$bqk254NMZOWVTzLVJCcxEOmhcyUujKxA5xk.kQCN9q0KNYFJd5b32'; +let API_KEY = null; const CURSEFORGE_API = 'https://api.curseforge.com/v1'; const HYTALE_GAME_ID = 70216; @@ -11,6 +11,15 @@ let modsPageSize = 20; let modsTotalPages = 1; export async function initModsManager() { + try { + if (window.electronAPI && window.electronAPI.getEnvVar) { + API_KEY = await window.electronAPI.getEnvVar('CURSEFORGE_API_KEY'); + console.log('Loaded API Key:', API_KEY ? 'Yes' : 'No'); + } + } catch (err) { + console.error('Failed to load API Key:', err); + } + setupModsEventListeners(); await loadInstalledMods(); await loadBrowseMods(); @@ -417,10 +426,10 @@ async function deleteMod(modId) { const mod = installedMods.find(m => m.id === modId); if (!mod) return; - const confirmMsg = window.i18n ? + const confirmMsg = window.i18n ? window.i18n.t('mods.confirmDelete').replace('{name}', mod.name) + ' ' + window.i18n.t('mods.confirmDeleteDesc') : `Are you sure you want to delete "${mod.name}"? This action cannot be undone.`; - + showConfirmModal( confirmMsg, async () => { diff --git a/backend/core/paths.js b/backend/core/paths.js index e5ee8d0..b82de75 100644 --- a/backend/core/paths.js +++ b/backend/core/paths.js @@ -162,13 +162,18 @@ async function getModsPath(customInstallPath = null) { const modsPath = path.join(userDataPath, 'Mods'); const disabledModsPath = path.join(userDataPath, 'DisabledMods'); + const profilesPath = path.join(userDataPath, 'Profiles'); if (!fs.existsSync(modsPath)) { + // Ensure the Mods directory exists fs.mkdirSync(modsPath, { recursive: true }); } if (!fs.existsSync(disabledModsPath)) { fs.mkdirSync(disabledModsPath, { recursive: true }); } + if (!fs.existsSync(profilesPath)) { + fs.mkdirSync(profilesPath, { recursive: true }); + } return modsPath; } catch (error) { @@ -177,6 +182,34 @@ 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 gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest'); + const userDataPath = findUserDataPath(gameLatest); + const profilesDir = path.join(userDataPath, 'Profiles'); + + if (!fs.existsSync(profilesDir)) { + fs.mkdirSync(profilesDir, { recursive: true }); + } + + return profilesDir; + } catch (err) { + console.error('Error getting profiles dir:', err); + return null; + } +} + module.exports = { getAppDir, getResolvedAppDir, @@ -191,5 +224,6 @@ module.exports = { findClientPath, findUserDataPath, findUserDataRecursive, - getModsPath + getModsPath, + getProfilesDir }; diff --git a/backend/managers/modManager.js b/backend/managers/modManager.js index 50c5744..5756e4e 100644 --- a/backend/managers/modManager.js +++ b/backend/managers/modManager.js @@ -2,10 +2,30 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const axios = require('axios'); -const { getModsPath } = require('../core/paths'); +const { getModsPath, getProfilesDir } = require('../core/paths'); const { saveModsToConfig, loadModsFromConfig } = require('../core/config'); const profileManager = require('./profileManager'); +const API_KEY = process.env.CURSEFORGE_API_KEY; + +/** + * Get the physical mods path for a specific profile. + * Each profile now has its own 'mods' folder. + */ +function getProfileModsPath(profileId) { + const profilesDir = getProfilesDir(); + if (!profilesDir) return null; + + const profileDir = path.join(profilesDir, profileId); + const modsDir = path.join(profileDir, 'mods'); + + if (!fs.existsSync(modsDir)) { + fs.mkdirSync(modsDir, { recursive: true }); + } + + return modsDir; +} + function generateModId(filename) { return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8); } @@ -35,30 +55,33 @@ function getProfileMods() { async function loadInstalledMods(modsPath) { try { + // Sync first to ensure we detect any manually added mods and paths are correct + await syncModsForCurrentProfile(); + const activeProfile = profileManager.getActiveProfile(); if (!activeProfile) return []; const profileMods = activeProfile.mods || []; - const profileModFiles = new Set(profileMods.map(m => m.fileName)); - - // We only return mods that are explicitly in the profile - // Check which ones are physically present (either in mods/ or DisabledMods/) - - const physicalModsPath = modsPath; // .../mods - const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods'); + + // Use profile-specific paths + const profileModsPath = getProfileModsPath(activeProfile.id); + const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods'); + + if (!fs.existsSync(profileModsPath)) fs.mkdirSync(profileModsPath, { recursive: true }); + if (!fs.existsSync(profileDisabledModsPath)) fs.mkdirSync(profileDisabledModsPath, { recursive: true }); const validMods = []; for (const modConfig of profileMods) { // Check if file exists in either location - const inEnabled = fs.existsSync(path.join(physicalModsPath, modConfig.fileName)); - const inDisabled = fs.existsSync(path.join(disabledModsPath, modConfig.fileName)); + const inEnabled = fs.existsSync(path.join(profileModsPath, modConfig.fileName)); + const inDisabled = fs.existsSync(path.join(profileDisabledModsPath, modConfig.fileName)); if (inEnabled || inDisabled) { validMods.push({ ...modConfig, // Set filePath based on physical location - filePath: inEnabled ? path.join(physicalModsPath, modConfig.fileName) : path.join(disabledModsPath, modConfig.fileName), + filePath: inEnabled ? path.join(profileModsPath, modConfig.fileName) : path.join(profileDisabledModsPath, modConfig.fileName), enabled: modConfig.enabled !== false // Default true }); } else { @@ -82,7 +105,11 @@ async function loadInstalledMods(modsPath) { async function downloadMod(modInfo) { try { - const modsPath = await getModsPath(); + const activeProfile = profileManager.getActiveProfile(); + if (!activeProfile) throw new Error('No active profile to save mod to'); + + const modsPath = getProfileModsPath(activeProfile.id); + if (!modsPath) throw new Error('Could not determine profile mods path'); if (!modInfo.downloadUrl && !modInfo.fileId) { throw new Error('No download URL or file ID provided'); @@ -91,9 +118,9 @@ async function downloadMod(modInfo) { let downloadUrl = modInfo.downloadUrl; if (!downloadUrl && modInfo.fileId && modInfo.modId) { - const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId}/files/${modInfo.fileId}`, { + const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId || modInfo.curseForgeId}/files/${modInfo.fileId || modInfo.curseForgeFileId}`, { headers: { - 'x-api-key': modInfo.apiKey, + 'x-api-key': modInfo.apiKey || API_KEY, 'Accept': 'application/json' } }); @@ -119,35 +146,30 @@ async function downloadMod(modInfo) { return new Promise((resolve, reject) => { writer.on('finish', () => { - // NEW: Update Active Profile instead of global config - const activeProfile = profileManager.getActiveProfile(); - if (activeProfile) { - const newMod = { - id: modInfo.id || generateModId(fileName), - name: modInfo.name || extractModName(fileName), - version: modInfo.version || '1.0.0', - description: modInfo.summary || modInfo.description || 'Downloaded from CurseForge', - author: modInfo.author || 'Unknown', - enabled: true, - fileName: fileName, - fileSize: fs.statSync(filePath).size, - dateInstalled: new Date().toISOString(), - curseForgeId: modInfo.modId, - curseForgeFileId: modInfo.fileId - }; + // Update Active Profile + const newMod = { + id: modInfo.id || generateModId(fileName), + name: modInfo.name || extractModName(fileName), + version: modInfo.version || '1.0.0', + description: modInfo.summary || modInfo.description || 'Downloaded from CurseForge', + author: modInfo.author || 'Unknown', + enabled: true, + fileName: fileName, + fileSize: fs.statSync(filePath).size, + dateInstalled: new Date().toISOString(), + curseForgeId: modInfo.modId, + curseForgeFileId: modInfo.fileId + }; - const updatedMods = [...(activeProfile.mods || []), newMod]; - profileManager.updateProfile(activeProfile.id, { mods: updatedMods }); + const updatedMods = [...(activeProfile.mods || []), newMod]; + profileManager.updateProfile(activeProfile.id, { mods: updatedMods }); - resolve({ - success: true, - filePath: filePath, - fileName: fileName, - modInfo: newMod - }); - } else { - reject(new Error('No active profile to save mod to')); - } + resolve({ + success: true, + filePath: filePath, + fileName: fileName, + modInfo: newMod + }); }); writer.on('error', reject); }); @@ -173,8 +195,11 @@ async function uninstallMod(modId, modsPath) { throw new Error('Mod not found in profile'); } - const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods'); - const enabledPath = path.join(modsPath, mod.fileName); + // Use profile paths + const profileModsPath = getProfileModsPath(activeProfile.id); + const disabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods'); + + const enabledPath = path.join(profileModsPath, mod.fileName); const disabledPath = path.join(disabledModsPath, mod.fileName); let fileRemoved = false; @@ -226,31 +251,25 @@ async function toggleMod(modId, modsPath) { updatedMods[modIndex] = { ...mod, enabled: newEnabled }; profileManager.updateProfile(activeProfile.id, { mods: updatedMods }); - // Manually move the file to reflect the new state - const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods'); + // Move file between Profile/Mods and Profile/DisabledMods + const profileModsPath = getProfileModsPath(activeProfile.id); + const disabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods'); + if (!fs.existsSync(disabledModsPath)) fs.mkdirSync(disabledModsPath, { recursive: true }); - const currentPath = mod.enabled ? path.join(modsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName); - - // Determine target paths - - const targetDir = newEnabled ? modsPath : disabledModsPath; + const currentPath = mod.enabled ? path.join(profileModsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName); + const targetDir = newEnabled ? profileModsPath : disabledModsPath; const targetPath = path.join(targetDir, mod.fileName); if (fs.existsSync(currentPath)) { fs.renameSync(currentPath, targetPath); } else { // Fallback: check if it's already in target? - - if (fs.existsSync(targetPath)) { - // It's already there, maybe just state was wrong. - console.log(`[ModManager] Mod ${mod.fileName} is already in the correct state`); - } else { // Try finding it - const altPath = mod.enabled ? path.join(disabledModsPath, mod.fileName) : path.join(modsPath, mod.fileName); + const altPath = mod.enabled ? path.join(disabledModsPath, mod.fileName) : path.join(profileModsPath, mod.fileName); if (fs.existsSync(altPath)) fs.renameSync(altPath, targetPath); } } @@ -273,35 +292,166 @@ async function syncModsForCurrentProfile() { return; } - console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name}`); + console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name} (${activeProfile.id})`); - const modsPath = await getModsPath(); - const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods'); + // 1. Resolve Paths + // globalModsPath is the one the game uses (symlink target) + const globalModsPath = await getModsPath(); + // profileModsPath is the real storage for this profile + const profileModsPath = getProfileModsPath(activeProfile.id); + const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods'); - if (!fs.existsSync(disabledModsPath)) { - fs.mkdirSync(disabledModsPath, { recursive: true }); + if (!fs.existsSync(profileDisabledModsPath)) { + fs.mkdirSync(profileDisabledModsPath, { recursive: true }); } - // Get all physical files from both folders - const enabledFiles = fs.existsSync(modsPath) ? fs.readdirSync(modsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : []; - const disabledFiles = fs.existsSync(disabledModsPath) ? fs.readdirSync(disabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : []; + // 2. Symlink / Migration Logic + let needsLink = false; + if (fs.existsSync(globalModsPath)) { + const stats = fs.lstatSync(globalModsPath); + + if (stats.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 (stats.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 { + fs.rmSync(globalModsPath, { recursive: true, force: true }); + 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}`); + try { + // 'junction' is key for Windows without admin + fs.symlinkSync(profileModsPath, globalModsPath, 'junction'); + } 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 }); + } + } + } + + // 3. Auto-Repair (Download missing mods) + const profileModsSnapshot = activeProfile.mods || []; + for (const mod of profileModsSnapshot) { + if (mod.enabled && !mod.manual) { + const inEnabled = fs.existsSync(path.join(profileModsPath, mod.fileName)); + const inDisabled = fs.existsSync(path.join(profileDisabledModsPath, mod.fileName)); + + if (!inEnabled && !inDisabled) { + if (mod.curseForgeId && (mod.curseForgeFileId || mod.fileId)) { + console.log(`[ModManager] Auto-repair: Re-downloading missing mod "${mod.name}"...`); + try { + await downloadMod({ + ...mod, + modId: mod.curseForgeId, + fileId: mod.curseForgeFileId || mod.fileId, + apiKey: API_KEY + }); + } catch (err) { + console.error(`[ModManager] Auto-repair failed for "${mod.name}": ${err.message}`); + } + } + } + } + } + + // 4. Auto-Import (Detect manual drops in the profile folder) + const enabledFiles = fs.existsSync(profileModsPath) ? fs.readdirSync(profileModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : []; + + let profileMods = activeProfile.mods || []; + let profileUpdated = false; + + + // Anything in this folder belongs to this profile. + + for (const file of enabledFiles) { + const isKnown = profileMods.some(m => m.fileName === file); + + if (!isKnown) { + console.log(`[ModManager] Auto-importing manual mod: ${file}`); + const newMod = { + id: generateModId(file), + name: extractModName(file), + version: 'Unknown', + description: 'Manually installed', + author: 'Local', + enabled: true, + fileName: file, + fileSize: 0, + dateInstalled: new Date().toISOString(), + manual: true + }; + profileMods.push(newMod); + profileUpdated = true; + } + } + + if (profileUpdated) { + profileManager.updateProfile(activeProfile.id, { mods: profileMods }); + const updatedProfile = profileManager.getActiveProfile(); + profileMods = updatedProfile ? (updatedProfile.mods || []) : profileMods; + } + + // 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. + + const disabledFiles = fs.existsSync(profileDisabledModsPath) ? fs.readdirSync(profileDisabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : []; const allFiles = new Set([...enabledFiles, ...disabledFiles]); - // Profile.mods contains the list of ALL mods for that profile, with their enabled state. - - const profileMods = activeProfile.mods || []; - for (const fileName of allFiles) { const modConfig = profileMods.find(m => m.fileName === fileName); - const shouldBeEnabled = modConfig && modConfig.enabled !== false; // Default to true if in list, unless explicitly false + const shouldBeEnabled = modConfig && modConfig.enabled !== false; - // Logic: - // If it should be enabled -> Move to mods/ - // If it should be disabled -> Move to DisabledMods/ - - const currentPath = enabledFiles.includes(fileName) ? path.join(modsPath, fileName) : path.join(disabledModsPath, fileName); - const targetDir = shouldBeEnabled ? modsPath : disabledModsPath; + const currentPath = enabledFiles.includes(fileName) ? path.join(profileModsPath, fileName) : path.join(profileDisabledModsPath, fileName); + const targetDir = shouldBeEnabled ? profileModsPath : profileDisabledModsPath; const targetPath = path.join(targetDir, fileName); if (path.dirname(currentPath) !== targetDir) { diff --git a/main.js b/main.js index ddd8091..e0ab2d0 100644 --- a/main.js +++ b/main.js @@ -1,5 +1,6 @@ -const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '.env') }); +const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const fs = require('fs'); const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); const UpdateManager = require('./backend/updateManager'); @@ -28,7 +29,7 @@ let updateManager; let discordRPC = null; // Discord Rich Presence setup -const DISCORD_CLIENT_ID = '1462244937868513373'; +const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID; function initDiscordRPC() { try { @@ -690,6 +691,10 @@ ipcMain.handle('get-local-app-data', async () => { return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); }); +ipcMain.handle('get-env-var', async (event, key) => { + return process.env[key]; +}); + ipcMain.handle('get-user-id', async () => { try { const { getOrCreatePlayerId } = require('./backend/launcher'); diff --git a/package.json b/package.json index 51dd605..2abb38f 100644 --- a/package.json +++ b/package.json @@ -1,103 +1,150 @@ -{ - "name": "hytale-f2p-launcher", - "version": "2.0.2b", - "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", - "scripts": { - "start": "electron .", - "dev": "electron . --dev", - "build": "electron-builder", - "build:win": "electron-builder --win", - "build:linux": "electron-builder --linux", - "build:mac": "electron-builder --mac", - "build:all": "electron-builder --win --linux --mac" - }, - "keywords": [ - "hytale", - "launcher", - "game", - "client", - "cross-platform", - "electron", - "auto-update", - "mod-manager", - "chat" - ], - "maintainers": [ - { - "name": "Terromur", - "url": "https://github.com/Terromur" - }, - { - "name": "Fari Gading", - "email": "fazrigading@gmail.com", - "url": "https://github.com/fazrigading" - } - ], - "author": { - "name": "AMIAY", - "email": "support@amiay.dev" - }, - "license": "MIT", - "devDependencies": { - "electron": "^40.0.0", - "electron-builder": "^26.4.0" - }, - "dependencies": { - "adm-zip": "^0.5.10", - "axios": "^1.6.0", - "discord-rpc": "^4.0.1", - "tar": "^6.2.1", - "uuid": "^9.0.1" - }, - "overrides": { - "tar": "$tar" - }, - "build": { - "appId": "com.hytalef2p.launcher", - "productName": "Hytale F2P Launcher", - "artifactName": "${name}_${version}_${arch}.${ext}", - "directories": { - "output": "dist" - }, - "files": [ - "main.js", - "preload.js", - "backend/**/*", - "GUI/**/*", - "package.json" - ], - "win": { - "target": [ - { "target": "nsis", "arch": ["x64", "arm64"] }, - { "target": "portable", "arch": ["x64"] } - ], - "icon": "icon.ico" - }, - "linux": { - "target": [ - { "target": "AppImage", "arch": ["x64", "arm64"] }, - { "target": "deb", "arch": ["x64", "arm64"] }, - { "target": "rpm", "arch": ["x64", "arm64"] }, - { "target": "pacman", "arch": ["x64", "arm64"] } - ], - "icon": "build/icon.png", - "category": "Game" - }, - "mac": { - "target": [ - { "target": "dmg", "arch": ["universal"] }, - { "target": "zip", "arch": ["universal"] } - ], - "icon": "build/icon.icns", - "category": "public.app-category.games" - }, - "nsis": { - "oneClick": false, - "allowToChangeInstallationDirectory": true, - "createDesktopShortcut": true, - "createStartMenuShortcut": true - } - } +{ + "name": "hytale-f2p-launcher", + "version": "2.0.2b", + "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", + "scripts": { + "start": "electron .", + "dev": "electron . --dev", + "build": "electron-builder", + "build:win": "electron-builder --win", + "build:linux": "electron-builder --linux", + "build:mac": "electron-builder --mac", + "build:all": "electron-builder --win --linux --mac" + }, + "keywords": [ + "hytale", + "launcher", + "game", + "client", + "cross-platform", + "electron", + "auto-update", + "mod-manager", + "chat" + ], + "maintainers": [ + { + "name": "Terromur", + "url": "https://github.com/Terromur" + }, + { + "name": "Fari Gading", + "email": "fazrigading@gmail.com", + "url": "https://github.com/fazrigading" + } + ], + "author": { + "name": "AMIAY", + "email": "support@amiay.dev" + }, + "license": "MIT", + "devDependencies": { + "electron": "^40.0.0", + "electron-builder": "^26.4.0" + }, + "dependencies": { + "adm-zip": "^0.5.10", + "axios": "^1.6.0", + "discord-rpc": "^4.0.1", + "dotenv": "^17.2.3", + "tar": "^6.2.1", + "uuid": "^9.0.1" + }, + "overrides": { + "tar": "$tar" + }, + "build": { + "appId": "com.hytalef2p.launcher", + "productName": "Hytale F2P Launcher", + "artifactName": "${name}_${version}_${arch}.${ext}", + "directories": { + "output": "dist" + }, + "files": [ + "main.js", + "preload.js", + "backend/**/*", + "GUI/**/*", + "package.json", + ".env" + ], + "win": { + "target": [ + { + "target": "nsis", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "portable", + "arch": [ + "x64" + ] + } + ], + "icon": "icon.ico" + }, + "linux": { + "target": [ + { + "target": "AppImage", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "deb", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "rpm", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "pacman", + "arch": [ + "x64", + "arm64" + ] + } + ], + "icon": "build/icon.png", + "category": "Game" + }, + "mac": { + "target": [ + { + "target": "dmg", + "arch": [ + "universal" + ] + }, + { + "target": "zip", + "arch": [ + "universal" + ] + } + ], + "icon": "build/icon.icns", + "category": "public.app-category.games" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true + } + } } \ No newline at end of file diff --git a/preload.js b/preload.js index 84f4ed0..759eb5e 100644 --- a/preload.js +++ b/preload.js @@ -32,6 +32,7 @@ contextBridge.exposeInMainWorld('electronAPI', { openGameLocation: () => ipcRenderer.invoke('open-game-location'), saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings), loadSettings: () => ipcRenderer.invoke('load-settings'), + getEnvVar: (key) => ipcRenderer.invoke('get-env-var', key), getLocalAppData: () => ipcRenderer.invoke('get-local-app-data'), getModsPath: () => ipcRenderer.invoke('get-mods-path'), loadInstalledMods: (modsPath) => ipcRenderer.invoke('load-installed-mods', modsPath), @@ -59,7 +60,7 @@ contextBridge.exposeInMainWorld('electronAPI', { onUpdatePopup: (callback) => { ipcRenderer.on('show-update-popup', (event, data) => callback(data)); }, - + getGpuInfo: () => ipcRenderer.invoke('get-gpu-info'), saveGpuPreference: (gpuPreference) => ipcRenderer.invoke('save-gpu-preference', gpuPreference), loadGpuPreference: () => ipcRenderer.invoke('load-gpu-preference'), From 68d697576a35731a8105b619c6ccdf59dab656ac Mon Sep 17 00:00:00 2001 From: Arnav Singh <72737311+ArnavSingh77@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:41:16 +0530 Subject: [PATCH 09/95] feat: add 'Close launcher on game start' option and improve app termination behavior (#93) * update main branch to release/v2.0.2b (#86) * add more linux pkgs, create auto-release and pre-release feature for Github Actions * removed package-lock from gitignore * update .gitignore for local build * add package-lock.json to maintain stability development * update version to 2.0.2b also add deps for rpm and arch * update 2.0.2b: add arm64 support, product and executable name, maintainers; remove snap; * update 2.0.2b: add latest.yml for win & linux, arm64 support; remove snap * fix release build naming * Prepare release v2.0.2b * feat: add 'Close launcher on game start' option and improve app termination behavior - Added 'Close launcher on game start' setting in GUI and backend. - Implemented automatic app quit after game launch if setting is enabled. - Added Cmd+Q (Mac) and Ctrl+Q/Alt+F4 (Win/Linux) shortcuts to quit the app. - Updated 'window-close' handler to fully quit the app instead of just closing the window. - Added i18n support for the new setting in English, Spanish, and Portuguese. --------- Co-authored-by: Fazri Gading Co-authored-by: Arnav Singh --- .gitignore | 3 +- GUI/index.html | 21 ++++++++++ GUI/js/settings.js | 92 ++++++++++++++++++++++++++++------------- GUI/locales/en.json | 5 ++- GUI/locales/es.json | 5 ++- GUI/locales/pt-BR.json | 5 ++- backend/core/config.js | 14 ++++++- backend/launcher.js | 6 +++ main.js | 94 ++++++++++++++++++++++++++++-------------- package-lock.json | 4 +- preload.js | 2 + 11 files changed, 184 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index e578779..b533c73 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ pkg/ # Package files *.tar.zst +*.zst.DS_Store *.zst bun.lockb -.env \ No newline at end of file +.env diff --git a/GUI/index.html b/GUI/index.html index 843aaad..c1d0399 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -431,6 +431,27 @@
+
+

+ + Launcher Behavior +

+ +
+ +
+
+ +

diff --git a/GUI/js/settings.js b/GUI/js/settings.js index 0ec95e6..dd383be 100644 --- a/GUI/js/settings.js +++ b/GUI/js/settings.js @@ -3,9 +3,11 @@ let customJavaCheck; let customJavaOptions; let customJavaPath; let browseJavaBtn; -let settingsPlayerName; -let discordRPCCheck; -let gpuPreferenceRadios; +let settingsPlayerName; +let discordRPCCheck; +let closeLauncherCheck; +let gpuPreferenceRadios; + // UUID Management elements let currentUuidDisplay; @@ -159,9 +161,11 @@ function setupSettingsElements() { customJavaOptions = document.getElementById('customJavaOptions'); customJavaPath = document.getElementById('customJavaPath'); browseJavaBtn = document.getElementById('browseJavaBtn'); - settingsPlayerName = document.getElementById('settingsPlayerName'); - discordRPCCheck = document.getElementById('discordRPCCheck'); - gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]'); + settingsPlayerName = document.getElementById('settingsPlayerName'); + discordRPCCheck = document.getElementById('discordRPCCheck'); + closeLauncherCheck = document.getElementById('closeLauncherCheck'); + gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]'); + // UUID Management elements currentUuidDisplay = document.getElementById('currentUuid'); @@ -190,9 +194,14 @@ function setupSettingsElements() { settingsPlayerName.addEventListener('change', savePlayerName); } - if (discordRPCCheck) { - discordRPCCheck.addEventListener('change', saveDiscordRPC); - } + if (discordRPCCheck) { + discordRPCCheck.addEventListener('change', saveDiscordRPC); + } + + if (closeLauncherCheck) { + closeLauncherCheck.addEventListener('change', saveCloseLauncher); + } + // UUID event listeners if (copyUuidBtn) { @@ -335,18 +344,43 @@ async function saveDiscordRPC() { } } -async function loadDiscordRPC() { - try { - if (window.electronAPI && window.electronAPI.loadDiscordRPC) { - const enabled = await window.electronAPI.loadDiscordRPC(); - if (discordRPCCheck) { - discordRPCCheck.checked = enabled; - } - } - } catch (error) { - console.error('Error loading Discord RPC setting:', error); - } -} +async function loadDiscordRPC() { + try { + if (window.electronAPI && window.electronAPI.loadDiscordRPC) { + const enabled = await window.electronAPI.loadDiscordRPC(); + if (discordRPCCheck) { + discordRPCCheck.checked = enabled; + } + } + } catch (error) { + console.error('Error loading Discord RPC setting:', error); + } +} + +async function saveCloseLauncher() { + try { + if (window.electronAPI && window.electronAPI.saveCloseLauncher && closeLauncherCheck) { + const enabled = closeLauncherCheck.checked; + await window.electronAPI.saveCloseLauncher(enabled); + } + } catch (error) { + console.error('Error saving close launcher setting:', error); + } +} + +async function loadCloseLauncher() { + try { + if (window.electronAPI && window.electronAPI.loadCloseLauncher) { + const enabled = await window.electronAPI.loadCloseLauncher(); + if (closeLauncherCheck) { + closeLauncherCheck.checked = enabled; + } + } + } catch (error) { + console.error('Error loading close launcher setting:', error); + } +} + async function savePlayerName() { try { @@ -457,13 +491,15 @@ async function loadGpuPreference() { } } -async function loadAllSettings() { - await loadCustomJavaPath(); - await loadPlayerName(); - await loadCurrentUuid(); - await loadDiscordRPC(); - await loadGpuPreference(); -} +async function loadAllSettings() { + await loadCustomJavaPath(); + await loadPlayerName(); + await loadCurrentUuid(); + await loadDiscordRPC(); + await loadCloseLauncher(); + await loadGpuPreference(); +} + async function openGameLocation() { try { diff --git a/GUI/locales/en.json b/GUI/locales/en.json index b7981be..d142831 100644 --- a/GUI/locales/en.json +++ b/GUI/locales/en.json @@ -122,7 +122,10 @@ "logsCopy": "Copy", "logsRefresh": "Refresh", "logsFolder": "Open Folder", - "logsLoading": "Loading logs..." + "logsLoading": "Loading logs...", + "closeLauncher": "Launcher Behavior", + "closeOnStart": "Close Launcher on game start", + "closeOnStartDescription": "Automatically close the launcher after Hytale has launched" }, "uuid": { "modalTitle": "UUID Management", diff --git a/GUI/locales/es.json b/GUI/locales/es.json index 283108b..4bb89c8 100644 --- a/GUI/locales/es.json +++ b/GUI/locales/es.json @@ -122,7 +122,10 @@ "logsCopy": "Copiar", "logsRefresh": "Actualizar", "logsFolder": "Abrir Carpeta", - "logsLoading": "Cargando registros..." + "logsLoading": "Cargando registros...", + "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" }, "uuid": { "modalTitle": "Gestión de UUID", diff --git a/GUI/locales/pt-BR.json b/GUI/locales/pt-BR.json index e48c1b0..492440b 100644 --- a/GUI/locales/pt-BR.json +++ b/GUI/locales/pt-BR.json @@ -122,7 +122,10 @@ "logsCopy": "Copiar", "logsRefresh": "Atualizar", "logsFolder": "Abrir Pasta", - "logsLoading": "Carregando registros..." + "logsLoading": "Carregando registros...", + "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" }, "uuid": { "modalTitle": "Gerenciamento de UUID", diff --git a/backend/core/config.js b/backend/core/config.js index 23332a8..03cff49 100644 --- a/backend/core/config.js +++ b/backend/core/config.js @@ -156,6 +156,15 @@ function loadLanguage() { return config.language || 'en'; } +function saveCloseLauncherOnStart(enabled) { + saveConfig({ closeLauncherOnStart: !!enabled }); +} + +function loadCloseLauncherOnStart() { + const config = loadConfig(); + return config.closeLauncherOnStart !== undefined ? config.closeLauncherOnStart : false; +} + function saveModsToConfig(mods) { try { const config = loadConfig(); @@ -331,5 +340,8 @@ module.exports = { resetCurrentUserUuid, // GPU Preference exports saveGpuPreference, - loadGpuPreference + loadGpuPreference, + // Close Launcher export + saveCloseLauncherOnStart, + loadCloseLauncherOnStart }; diff --git a/backend/launcher.js b/backend/launcher.js index cadee5e..32a6c59 100644 --- a/backend/launcher.js +++ b/backend/launcher.js @@ -17,6 +17,8 @@ const { loadDiscordRPC, saveLanguage, loadLanguage, + saveCloseLauncherOnStart, + loadCloseLauncherOnStart, saveModsToConfig, loadModsFromConfig, getUuidForUser, @@ -124,6 +126,10 @@ module.exports = { saveLanguage, loadLanguage, + // Close Launcher functions + saveCloseLauncherOnStart, + loadCloseLauncherOnStart, + // GPU Preference functions saveGpuPreference, loadGpuPreference, diff --git a/main.js b/main.js index e0ab2d0..9fe72d6 100644 --- a/main.js +++ b/main.js @@ -2,7 +2,8 @@ const path = require('path'); require('dotenv').config({ path: path.join(__dirname, '.env') }); const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const fs = require('fs'); -const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); +const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); + const UpdateManager = require('./backend/updateManager'); const logger = require('./backend/logger'); const profileManager = require('./backend/managers/profileManager'); @@ -186,10 +187,21 @@ function createWindow() { if (input.key === 'F12') { event.preventDefault(); } - if (input.key === 'F5') { - event.preventDefault(); - } - }); + if (input.key === 'F5') { + event.preventDefault(); + } + + // Close application shortcuts + const isMac = process.platform === 'darwin'; + const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') || + (!isMac && input.control && input.key.toLowerCase() === 'q') || + (!isMac && input.alt && input.key === 'F4'); + + if (quitShortcut) { + app.quit(); + } + }); + mainWindow.webContents.on('context-menu', (e) => { @@ -333,15 +345,14 @@ app.on('before-quit', () => { cleanupDiscordRPC(); }); -app.on('window-all-closed', () => { - console.log('=== LAUNCHER CLOSING ==='); - - cleanupDiscordRPC(); - - if (process.platform !== 'darwin') { - app.quit(); - } -}); +app.on('window-all-closed', () => { + console.log('=== LAUNCHER CLOSING ==='); + + cleanupDiscordRPC(); + + app.quit(); +}); + ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => { try { @@ -358,9 +369,20 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g } }; - const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference); - - return result; + const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference); + + if (result.success && result.launched) { + const closeOnStart = loadCloseLauncherOnStart(); + if (closeOnStart) { + console.log('Close Launcher on start enabled, quitting application...'); + setTimeout(() => { + app.quit(); + }, 1000); + } + } + + return result; + } catch (error) { console.error('Launch error:', error); const errorMessage = error.message || error.toString(); @@ -475,11 +497,21 @@ ipcMain.handle('save-language', (event, language) => { return { success: true }; }); -ipcMain.handle('load-language', () => { - return loadLanguage(); -}); - -ipcMain.handle('select-install-path', async () => { +ipcMain.handle('load-language', () => { + return loadLanguage(); +}); + +ipcMain.handle('save-close-launcher', (event, enabled) => { + saveCloseLauncherOnStart(enabled); + return { success: true }; +}); + +ipcMain.handle('load-close-launcher', () => { + return loadCloseLauncherOnStart(); +}); + +ipcMain.handle('select-install-path', async () => { + const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'], title: 'Select Installation Folder' @@ -804,11 +836,10 @@ ipcMain.handle('open-download-page', async () => { try { await shell.openExternal(updateManager.getDownloadUrl()); - setTimeout(() => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.close(); - } - }, 1000); + setTimeout(() => { + app.quit(); + }, 1000); + return { success: true }; } catch (error) { @@ -850,11 +881,10 @@ ipcMain.handle('get-detected-gpu', () => { return global.detectedGpu; }); -ipcMain.handle('window-close', () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.close(); - } -}); +ipcMain.handle('window-close', () => { + app.quit(); +}); + ipcMain.handle('window-minimize', () => { if (mainWindow && !mainWindow.isDestroyed()) { diff --git a/package-lock.json b/package-lock.json index 90d6855..bb1bcd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "hytale-f2p-launcherv2", + "name": "hytale-f2p-launcher", "version": "2.0.2b", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "hytale-f2p-launcherv2", + "name": "hytale-f2p-launcher", "version": "2.0.2b", "license": "MIT", "dependencies": { diff --git a/preload.js b/preload.js index 759eb5e..b00d840 100644 --- a/preload.js +++ b/preload.js @@ -21,6 +21,8 @@ contextBridge.exposeInMainWorld('electronAPI', { loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'), saveLanguage: (language) => ipcRenderer.invoke('save-language', language), loadLanguage: () => ipcRenderer.invoke('load-language'), + saveCloseLauncher: (enabled) => ipcRenderer.invoke('save-close-launcher', enabled), + loadCloseLauncher: () => ipcRenderer.invoke('load-close-launcher'), selectInstallPath: () => ipcRenderer.invoke('select-install-path'), browseJavaPath: () => ipcRenderer.invoke('browse-java-path'), isGameInstalled: () => ipcRenderer.invoke('is-game-installed'), From 9eb5d1759ca2761ede90bcd19c82547cd31befda Mon Sep 17 00:00:00 2001 From: chasem-dev Date: Thu, 22 Jan 2026 09:11:10 -0500 Subject: [PATCH 10/95] Update publish config to point to chasem-dev fork --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f8303c..53c788e 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ }, "publish": { "provider": "github", - "owner": "amiayweb", + "owner": "chasem-dev", "repo": "Hytale-F2P" } } From 61433bfeeaf2263bd5ba59e2574a938fd230fe91 Mon Sep 17 00:00:00 2001 From: chasem-dev Date: Thu, 22 Jan 2026 10:18:28 -0500 Subject: [PATCH 11/95] Fix Linux metadata files in workflow and improve error handling --- .github/workflows/release.yml | 4 +- GUI/js/update.js | 90 +++++++++++++++++- backend/appUpdater.js | 166 +++++++++++++++++++++++++++++++++- preload.js | 3 + 4 files changed, 255 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f8216a..f7aeef8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: dist/*.deb dist/*.rpm dist/*.pacman - dist/latest.yml + dist/latest*.yml build-windows: runs-on: windows-latest @@ -115,4 +115,4 @@ jobs: generate_release_notes: true draft: true # DYNAMIC FLAGS: Mark as pre-release ONLY IF it's NOT a tag (meaning it's a branch push) - prerelease: ${{ github.ref_type != 'tag' }} + prerelease: ${{ github.ref_type != 'tag' }} \ No newline at end of file diff --git a/GUI/js/update.js b/GUI/js/update.js index bd1403c..9f78f80 100644 --- a/GUI/js/update.js +++ b/GUI/js/update.js @@ -23,6 +23,10 @@ class ClientUpdateManager { this.showUpdateDownloaded(updateInfo); }); + window.electronAPI.onUpdateError((errorInfo) => { + this.handleUpdateError(errorInfo); + }); + this.checkForUpdatesOnDemand(); } @@ -57,6 +61,10 @@ class ClientUpdateManager {
A new version of Hytale F2P Launcher is available.
Downloading update automatically... +
From 3579d82776ae1a600a851a545e0b88ab43969f58 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sat, 24 Jan 2026 01:41:09 +0100 Subject: [PATCH 36/95] fix (to try) --- GUI/index.html | 1 + GUI/js/settings.js | 43 +++++++++++++------ backend/logger.js | 2 +- backend/managers/gameManager.js | 73 ++++++++++++++++++++++++++------- backend/utils/userDataBackup.js | 16 ++++---- 5 files changed, 99 insertions(+), 36 deletions(-) diff --git a/GUI/index.html b/GUI/index.html index 507ca85..1280900 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -849,6 +849,7 @@

+ diff --git a/GUI/js/settings.js b/GUI/js/settings.js index 0d268b8..6dd54b5 100644 --- a/GUI/js/settings.js +++ b/GUI/js/settings.js @@ -152,9 +152,9 @@ function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmTe } -export function initSettings() { +export async function initSettings() { setupSettingsElements(); - loadAllSettings(); + await loadAllSettings(); } function setupSettingsElements() { @@ -953,14 +953,7 @@ async function switchBranch(newBranch) { playButton.classList.add('disabled'); } - // Save new branch - await window.electronAPI.saveVersionBranch(newBranch); - - const switchedMsg = window.i18n ? - window.i18n.t('settings.branchSwitched').replace('{branch}', newBranch) : - `Switched to ${newBranch} successfully!`; - - showNotification(switchedMsg, 'success'); + // DON'T save branch yet - wait for installation confirmation // Suggest reinstalling setTimeout(() => { @@ -986,12 +979,24 @@ async function switchBranch(newBranch) { const result = await window.electronAPI.installGame(playerName || 'Player', '', '', newBranch); if (result.success) { + // Save branch ONLY after successful installation + await window.electronAPI.saveVersionBranch(newBranch); + + const switchedMsg = window.i18n ? + window.i18n.t('settings.branchSwitched').replace('{branch}', newBranch) : + `Switched to ${newBranch} successfully!`; + const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!'; + showNotification(switchedMsg, 'success'); showNotification(successMsg, 'success'); + // Refresh radio buttons to reflect the new branch + await loadVersionBranch(); + console.log('[Settings] Radio buttons updated after branch switch'); + setTimeout(() => { if (window.LauncherUI) { window.LauncherUI.hideProgress(); @@ -1019,6 +1024,14 @@ async function switchBranch(newBranch) { window.LauncherUI.hideProgress(); } + // Revert radio selection to old branch + loadVersionBranch().then(oldBranch => { + const radioToCheck = document.querySelector(`input[name="gameBranch"][value="${oldBranch}"]`); + if (radioToCheck) { + radioToCheck.checked = true; + } + }); + // Unlock play button const playButton = document.getElementById('playButton'); if (playButton) { @@ -1058,15 +1071,21 @@ async function loadVersionBranch() { try { if (window.electronAPI && window.electronAPI.loadVersionBranch) { const branch = await window.electronAPI.loadVersionBranch(); + console.log('[Settings] Loaded version_branch from config:', branch); + + // Use default if branch is null/undefined + const selectedBranch = branch || 'release'; + console.log('[Settings] Selected branch:', selectedBranch); // Update radio buttons if (gameBranchRadios) { gameBranchRadios.forEach(radio => { - radio.checked = radio.value === branch; + radio.checked = radio.value === selectedBranch; + console.log(`[Settings] Radio ${radio.value}: ${radio.checked ? 'checked' : 'unchecked'}`); }); } - return branch; + return selectedBranch; } return 'release'; // Default } catch (error) { diff --git a/backend/logger.js b/backend/logger.js index 2064969..95852b8 100644 --- a/backend/logger.js +++ b/backend/logger.js @@ -85,7 +85,7 @@ class Logger { fs.appendFileSync(this.logFile, message, 'utf8'); } catch (error) { - this.originalConsole.error('Impossible d\'écrire dans le fichier de log:', error.message); + this.originalConsole.error('Unable to write to log file:', error.message); } } diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index 1785461..109651b 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -23,13 +23,32 @@ async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallb const dest = path.join(cacheDir, `${branch}_${fileName}`); + // Check if file exists and validate it if (fs.existsSync(dest)) { console.log('PWR file found in cache:', dest); - return dest; + + // Validate file size (PWR files should be > 1MB) + const stats = fs.statSync(dest); + if (stats.size < 1024 * 1024) { + console.log('Cached PWR file seems corrupted (too small), re-downloading...'); + fs.unlinkSync(dest); + } else { + return dest; + } } console.log('Fetching PWR patch file:', url); await downloadFile(url, dest, progressCallback); + + // Validate downloaded file + const stats = fs.statSync(dest); + console.log(`PWR file downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`); + + if (stats.size < 1024 * 1024) { + fs.unlinkSync(dest); + throw new Error('Downloaded PWR file is corrupted (file too small)'); + } + console.log('PWR saved to:', dest); return dest; @@ -85,7 +104,13 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir if (error) { console.error('Butler stderr:', stderr); console.error('Butler stdout:', stdout); - reject(new Error(`Patch installation failed: ${error.message}${stderr ? '\n' + stderr : ''}`)); + + // Check for EOF error (corrupted PWR file) + if (stderr && stderr.includes('unexpected EOF')) { + reject(new Error(`Corrupted PWR file detected. Please delete the cache and reinstall: ${pwrFile}`)); + } else { + reject(new Error(`Patch installation failed: ${error.message}${stderr ? '\n' + stderr : ''}`)); + } } else { resolve(); } @@ -114,7 +139,9 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, // 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}`); + console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`); try { if (progressCallback) { @@ -145,11 +172,15 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, progressCallback('Backing up user data...', 70, null, null, null); } - // Backup UserData using new system + // Backup UserData from OLD branch (before switching) try { - backupPath = await userDataBackup.backupUserData(installPath, branch, hasVersionConfig); + 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}`); + } } catch (backupError) { - console.warn('UserData backup failed:', backupError.message); + console.warn('[UpdateGameFiles] ✗ UserData backup failed:', backupError.message); } if (progressCallback) { @@ -169,6 +200,13 @@ 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); } @@ -176,11 +214,16 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, // 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('UserData restore failed:', restoreError.message); + console.warn('[UpdateGameFiles] ✗ UserData restore failed:', restoreError.message); } + } else { + console.log('[UpdateGameFiles] No backup to restore, empty UserData folder created'); } console.log(`Game files updated successfully to version: ${newVersion}`); @@ -238,7 +281,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver // 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 détectée - version_client: ${config.version_client}, version_branch: ${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 @@ -248,13 +291,13 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver } try { - console.log(`[InstallGame] Tentative de backup UserData (hasVersionConfig: ${hasVersionConfig})...`); + console.log(`[InstallGame] Attempting UserData backup (hasVersionConfig: ${hasVersionConfig})...`); backupPath = await userDataBackup.backupUserData(customAppDir, branch, hasVersionConfig); if (backupPath) { - console.log(`[InstallGame] ✓ UserData sauvegardé dans: ${backupPath}`); + console.log(`[InstallGame] ✓ UserData backed up to: ${backupPath}`); } } catch (backupError) { - console.warn('[InstallGame] ✗ Backup UserData échoué:', backupError.message); + console.warn('[InstallGame] ✗ UserData backup failed:', backupError.message); } [customAppDir, customCacheDir, customToolsDir].forEach(dir => { @@ -332,7 +375,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver // Ensure UserData directory exists if (!fs.existsSync(userDataDir)) { - console.log(`[InstallGame] Création du dossier UserData dans: ${userDataDir}`); + console.log(`[InstallGame] Creating UserData directory at: ${userDataDir}`); fs.mkdirSync(userDataDir, { recursive: true }); } @@ -343,15 +386,15 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver } try { - console.log(`[InstallGame] Restauration du UserData depuis: ${backupPath}`); + console.log(`[InstallGame] Restoring UserData from: ${backupPath}`); await userDataBackup.restoreUserData(backupPath, customAppDir, branch); await userDataBackup.cleanupBackup(backupPath); - console.log('[InstallGame] ✓ UserData restauré avec succès'); + console.log('[InstallGame] ✓ UserData restored successfully'); } catch (restoreError) { - console.warn('[InstallGame] ✗ Erreur lors de la restauration UserData:', restoreError.message); + console.warn('[InstallGame] ✗ UserData restore failed:', restoreError.message); } } else { - console.log('[InstallGame] Aucun backup à restaurer, dossier UserData vide créé'); + console.log('[InstallGame] No backup to restore, empty UserData folder created'); } if (progressCallback) { diff --git a/backend/utils/userDataBackup.js b/backend/utils/userDataBackup.js index df1b69b..0da8614 100644 --- a/backend/utils/userDataBackup.js +++ b/backend/utils/userDataBackup.js @@ -19,36 +19,36 @@ class UserDataBackup { // c'est une ancienne installation, on cherche dans installPath/HytaleF2P/release if (!hasVersionConfig) { const oldPath = path.join(installPath, 'HytaleF2P', 'release', 'package', 'game', 'latest', 'Client', 'UserData'); - console.log(`[UserDataBackup] Pas de version_client/version_branch détecté, recherche ancienne installation dans: ${oldPath}`); + console.log(`[UserDataBackup] No version_client/version_branch detected, searching old installation in: ${oldPath}`); if (fs.existsSync(oldPath)) { userDataPath = oldPath; - console.log(`[UserDataBackup] ✓ Ancienne installation trouvée ! UserData existe dans l'ancien emplacement`); + console.log(`[UserDataBackup] ✓ Old installation found! UserData exists in old location`); } else { - console.log(`[UserDataBackup] ✗ Aucune ancienne installation trouvée dans ${oldPath}`); + console.log(`[UserDataBackup] ✗ No old installation found in ${oldPath}`); userDataPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData'); } } else { // Si on a version_client/version_branch, on cherche dans installPath/HytaleF2P/ userDataPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData'); - console.log(`[UserDataBackup] Version configurée, recherche dans: ${userDataPath}`); + console.log(`[UserDataBackup] Version configured, searching in: ${userDataPath}`); } if (!fs.existsSync(userDataPath)) { - console.log(`[UserDataBackup] ✗ Aucun UserData trouvé à ${userDataPath}, backup ignoré`); + console.log(`[UserDataBackup] ✗ No UserData found at ${userDataPath}, backup skipped`); return null; } - console.log(`[UserDataBackup] ✓ UserData trouvé à ${userDataPath}`); + console.log(`[UserDataBackup] ✓ UserData found at ${userDataPath}`); const backupPath = path.join(installPath, `UserData_backup_${branch}_${Date.now()}`); try { - console.log(`[UserDataBackup] Copie de ${userDataPath} vers ${backupPath}...`); + console.log(`[UserDataBackup] Copying from ${userDataPath} to ${backupPath}...`); await fs.copy(userDataPath, backupPath, { overwrite: true, errorOnExist: false }); - console.log('[UserDataBackup] ✓ Backup complété avec succès'); + console.log('[UserDataBackup] ✓ Backup completed successfully'); return backupPath; } catch (error) { console.error('[UserDataBackup] ✗ Erreur lors du backup:', error); From d8393543dfca7b28dd7774c1db7f32c10e2b1afb Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sat, 24 Jan 2026 02:49:21 +0100 Subject: [PATCH 37/95] fixing --- backend/managers/gameManager.js | 12 +++++++++++- backend/services/firstLaunch.js | 8 +------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index 109651b..c2be5d7 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -107,7 +107,17 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir // Check for EOF error (corrupted PWR file) if (stderr && stderr.includes('unexpected EOF')) { - reject(new Error(`Corrupted PWR file detected. Please delete the cache and reinstall: ${pwrFile}`)); + // Delete corrupted PWR file + console.log('Corrupted PWR file detected, deleting:', pwrFile); + try { + if (fs.existsSync(pwrFile)) { + fs.unlinkSync(pwrFile); + console.log('Corrupted PWR file deleted. Please try again to re-download.'); + } + } catch (delErr) { + console.error('Failed to delete corrupted PWR file:', delErr); + } + reject(new Error(`Corrupted PWR file detected and deleted. Please try launching the game again.`)); } else { reject(new Error(`Patch installation failed: ${error.message}${stderr ? '\n' + stderr : ''}`)); } diff --git a/backend/services/firstLaunch.js b/backend/services/firstLaunch.js index 5cd78dd..7bd1c7a 100644 --- a/backend/services/firstLaunch.js +++ b/backend/services/firstLaunch.js @@ -56,15 +56,9 @@ async function handleFirstLaunchCheck(progressCallback) { try { const config = loadConfig(); - // Initialize version_branch and version_client if not set - const currentBranch = loadVersionBranch(); + // Initialize version_client if not set (but don't force version_branch) const currentVersion = loadVersionClient(); - if (!currentBranch) { - console.log('Initializing version_branch to "release"'); - saveVersionBranch('release'); - } - if (currentVersion === undefined || currentVersion === null) { console.log('Initializing version_client to null (will trigger installation)'); saveVersionClient(null); From 34f93e962b61069e9e050b5a45637c0c49898aaf Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Sat, 24 Jan 2026 15:57:20 +0800 Subject: [PATCH 38/95] docs: adjusted github template & add new contributors name (#133) * docs: add new contributors to the list * docs: fix template and new adjustments * docs: fix github template & add new contributors * removed config.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 38 ++++++++----------- .github/ISSUE_TEMPLATE/feature_request.yml | 12 +++++- .../new_translation_request.yml | 24 ++++++++++++ .github/ISSUE_TEMPLATE/support_request.yml | 33 +++++++++++----- ...equest.yml => translation_fix_request.yml} | 19 +++++----- GUI/index.html | 8 +++- README.md | 17 +++++---- 7 files changed, 99 insertions(+), 52 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/new_translation_request.yml rename .github/ISSUE_TEMPLATE/{translation_request.yml => translation_fix_request.yml} (68%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6fa5672..dafdc62 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -3,6 +3,13 @@ description: Create a report to help us improve title: "[BUG] " labels: ["bug"] body: + - type: markdown + attributes: + value: | + Bug is a problem which impairs or prevents the functions of the launcher from working as intended. + Thanks for taking the time to fill out a bug report! + Please provide as much information as you can to help us understand and reproduce the issue. + - type: textarea id: description attributes: @@ -43,8 +50,8 @@ body: id: version attributes: label: Version - description: What version of the project are you running? - placeholder: "e.g. v1.2.3" + description: What version of the launcher are you running? + placeholder: "e.g. \"v2.0.11 stable/pre-release\"" validations: required: true @@ -54,29 +61,16 @@ body: label: Operating System description: What operating system are you using? options: - - Windows - - macOS - - Linux - - iOS - - Android - - Other + - Windows 10 + - Windows 11 + - macOS (Apple Silicon) + - macOS (Intel) + - Linux Ubuntu/Debian-based + - Linux Fedora/RHEL-based + - Linux Arch-based validations: required: true - - type: dropdown - id: browser - attributes: - label: Browser (if applicable) - description: What browser are you using? - options: - - Chrome - - Firefox - - Safari - - Edge - - Opera - - Other - - N/A - - type: textarea id: additional attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 89b6e49..6d1d5d4 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -16,8 +16,10 @@ body: id: problem attributes: label: Is your feature request related to a problem? Please describe. - description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + description: A clear and concise description of what the problem is. placeholder: "Ex. I'm always frustrated when [...]" + validations: + required: true - type: textarea id: solution @@ -34,6 +36,14 @@ body: label: Describe alternatives you've considered description: A clear and concise description of any alternative solutions or features you've considered. placeholder: "Describe any alternative solutions or features you've considered." + validations: + required: true + + - type: screenshots + id: screenshots + attributes: + label: Screenshots (Optional) + description: If applicable, add screenshots to help explain your request. - type: textarea id: additional diff --git a/.github/ISSUE_TEMPLATE/new_translation_request.yml b/.github/ISSUE_TEMPLATE/new_translation_request.yml new file mode 100644 index 0000000..996deed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new_translation_request.yml @@ -0,0 +1,24 @@ +name: New Translation Request +description: Request new language translation for text or content on the launcher +title: "[TRANSLATION REQUEST] " +labels: ["translation request"] +body: + - type: input + id: language + attributes: + label: Request New Language + description: What language do you want our launcher to support? + placeholder: "e.g. German (de-DE), Russian (ru-RU), etc." + validations: + required: true + + - type: dropdown + id: contriution_willingness + attributes: + label: Willingness to Contribute + description: Are you willing to help with the translation effort? + options: + - Yes, I can help translate from English to the requested language! + - No, I just want to request the language. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/support_request.yml b/.github/ISSUE_TEMPLATE/support_request.yml index 1b03828..71d98e1 100644 --- a/.github/ISSUE_TEMPLATE/support_request.yml +++ b/.github/ISSUE_TEMPLATE/support_request.yml @@ -3,6 +3,13 @@ description: Request help or support title: "[SUPPORT] " labels: ["support"] body: + - type: markdown + attributes: + value: | + If you need help or support with using the launcher, please fill out this support request. + Provide as much detail as possible so we can assist you effectively. + **Need a quick assistance?** Please Open-A-Ticket in our [Discord Server](https://discord.gg/gME8rUy3MB)! + - type: textarea id: question attributes: @@ -17,14 +24,18 @@ body: attributes: label: Context description: Provide any relevant context or background information. - placeholder: "I've tried..., I expected..., but got..." + placeholder: "I've tried..., but got..." + validations: + required: true - type: input id: version attributes: label: Version description: What version are you using? - placeholder: "e.g. v1.2.3" + placeholder: "e.g. v2.0.11 stable/pre-release" + validations: + required: true - type: dropdown id: platform @@ -32,13 +43,15 @@ body: label: Platform description: What platform are you using? options: - - Windows - - macOS - - Linux - - iOS - - Android - - Web Browser - - Other + - Windows 10 + - Windows 11 + - macOS (Apple Silicon) + - macOS (Intel) + - Linux Ubuntu/Debian-based + - Linux Fedora/RHEL-based + - Linux Arch-based + validations: + required: true - type: textarea id: logs @@ -46,6 +59,8 @@ body: label: Logs or Error Messages description: If applicable, paste any error messages or logs here. render: shell + validations: + required: true - type: textarea id: additional diff --git a/.github/ISSUE_TEMPLATE/translation_request.yml b/.github/ISSUE_TEMPLATE/translation_fix_request.yml similarity index 68% rename from .github/ISSUE_TEMPLATE/translation_request.yml rename to .github/ISSUE_TEMPLATE/translation_fix_request.yml index fc43333..e8477d0 100644 --- a/.github/ISSUE_TEMPLATE/translation_request.yml +++ b/.github/ISSUE_TEMPLATE/translation_fix_request.yml @@ -1,14 +1,14 @@ -name: Translation Request -description: Request translation for text or content -title: "[TRANSLATION] " -labels: ["translation"] +name: Translation Fix Request +description: Request a fix of translation for text or content in the launcher +title: "[TRANSLATION FIX] " +labels: ["translation fix"] body: - type: input id: language attributes: label: Target Language description: What language do you want to translate to? - placeholder: "e.g. Spanish (es-ES), French (fr-FR)" + placeholder: "e.g. Spanish (es-ES), Portuguese (pt-BR), etc." validations: required: true @@ -28,12 +28,11 @@ body: description: Provide context about where this text appears or how it's used. placeholder: "This text appears in..., It's used for..." - - type: input - id: file_location + - type: textarea + id: screenshots attributes: - label: File Location - description: Where is this text located in the codebase? - placeholder: "e.g. src/components/Button.js:15" + label: Screenshots + description: If applicable, add screenshots to help explain your problem. - type: textarea id: notes diff --git a/GUI/index.html b/GUI/index.html index 1280900..a99aa61 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -778,11 +778,15 @@ @sanasol, @terromur, + class="text-blue-400 hover:text-blue-300 transition-colors">@Terromur, @ericiskoolbeans, @fazrigading + class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading, + @Rahul-Sahani04 + @xSamiVS diff --git a/README.md b/README.md index 3ad3609..6a9f5c4 100644 --- a/README.md +++ b/README.md @@ -295,14 +295,15 @@ This project follows **strict semantic versioning** with **numerical versions on - [**@Relyz1993**](https://github.com/Relyz1993) - *Server Helper & Second Developer & Project Creator* ### 🌟 Contributors -- [**@sanasol**](https://github.com/sanasol) - *Main Issues Fixer | Multiplayer Patcher | Beta Tester | macOS* -- [**@Terromur**](https://github.com/Terromur) - *Main Issues Fixer | Beta Tester | Windows & Linux* -- [**@fazrigading**](https://github.com/fazrigading) - *Main Issues Fixer | Beta Tester | Build Release Manager | Fedora* -- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester | OS* -- [**@chasem-dev**](https://github.com/chasem-dev) - *Issues Fixer | macOS* -- [**@crimera**](https://github.com/crimera) - *Issues Fixer | OS* -- [**@Citeli-py**](https://github.com/Citeli-py) - *Issues Fixer | OS* -- [**@Rahul-Sahani04**](https://github.com/Rahul-Sahani04) - *Issues Fixer | Beta Tester| Windows* +- [**@sanasol**](https://github.com/sanasol) - *Main Issues fixer | Multiplayer Patcher* +- [**@Terromur**](https://github.com/Terromur) - *Main Issues fixer | Beta tester* +- [**@fazrigading**](https://github.com/fazrigading) - *Main Issues fixer | Beta tester* +- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester* +- [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer* +- [**@crimera**](https://github.com/crimera) - *Issues fixer* +- [**@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* --- From 8a87c7c4d90f913a11cc73cd4f2f5dbd0c4cb39e Mon Sep 17 00:00:00 2001 From: Fazri Gading Date: Sat, 24 Jan 2026 16:35:42 +0800 Subject: [PATCH 39/95] docs: add and adjust more info on readme --- README.md | 72 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 6a9f5c4..379f3c5 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,65 @@ -# 🎮 Hytale F2P Launcher | Cross-Platform Multiplayer Support 🪟 🍎 🐧 -
-![Version](https://img.shields.io/badge/Version-2.0.2-green?style=for-the-badge) -![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey?style=for-the-badge) -![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge) +
+

🎮 Hytale F2P Launcher | Cross-Platform Multiplayer Support 🖥

+

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 (all OS supported)** +![Version](https://img.shields.io/badge/Version-2.1.0-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) [![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/stargazers) [![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/network/members) ⭐ **If you find this project useful, please give it a STAR!** ⭐ -🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑 - ⚠️ **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** 🛑 +
--- ## 📸 Screenshots
-

Hytale F2P Launcher

- Hytale F2P Launcher + Hytale F2P Launcher
- View Mods Preview - Hytale F2P Mods -
-
- View Latest News - Hytale F2P News -
-
- View Social & Chat - Hytale F2P Chat + View Hytale F2P Gallery + + + + + + + + + + + + + +
+ Mods Preview
+ Hytale F2P Mods +
+ Latest News
+ Hytale F2P News +
+ Social & Chat
+ Hytale F2P Chat +
+ Settings
+ Hytale F2P Settings +
+ In-Game Screenshot-1
+ Hytale F2P In-Game Screenshot-1 +
+ In-Game Screenshot-2
+ Hytale F2P In-Game Screenshot-2 +
@@ -65,7 +89,9 @@ ### 🎮 Hytale Hardware Requirements -Hytale is designed to be accessible while scaling for high-end performance. Below are the [official system requirements for the Early Access](https://hytale.com/news/2025/12/hytale-hardware-requirements) release. +> [!INFO] +> Hytale is designed to be accessible while scaling for high-end performance. +> Below are the [official system requirements for the Early Access](https://hytale.com/news/2025/12/hytale-hardware-requirements) release.
@@ -134,6 +160,8 @@ Hytale is designed to be accessible while scaling for high-end performance. Belo > [!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. + * 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 @@ -159,7 +187,7 @@ Hytale is designed to be accessible while scaling for high-end performance. Belo 1. Download .DMG file from the from [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/). 2. Run the file. 3. If says "Apple could not verify ...", go to System Settings > Privacy & Security > Scroll to bottom, find "Hytale F2P Launcher" > press Open Anyway. -4. Alterative: You can also use the .zip. // TODO: NEEDS MORE INFORMATION +4. Advanced: You can also use the .zip. // TODO: NEEDS MORE INFORMATION --- From 411d7d8aaf29226f96fa0b75e8d8892e4e53b78f Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sat, 24 Jan 2026 12:00:01 +0100 Subject: [PATCH 40/95] fix --- GUI/js/ui.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/GUI/js/ui.js b/GUI/js/ui.js index bc2b35f..0223054 100644 --- a/GUI/js/ui.js +++ b/GUI/js/ui.js @@ -501,7 +501,10 @@ function setupUI() { setupAnimations(); setupFirstLaunchHandlers(); loadLauncherVersion(); - checkGameInstallation(); + checkGameInstallation().catch(err => { + console.error('Critical error in checkGameInstallation:', err); + lockPlayButton(false); + }); document.body.focus(); } @@ -526,6 +529,13 @@ async function checkGameInstallation() { try { console.log('Checking game installation status...'); + // Verify electronAPI is available + if (!window.electronAPI || !window.electronAPI.isGameInstalled) { + console.error('electronAPI not available, unlocking play button as fallback'); + lockPlayButton(false); + return; + } + // Check if game is installed const isInstalled = await window.electronAPI.isGameInstalled(); @@ -537,7 +547,9 @@ async function checkGameInstallation() { console.log(`Game installed: ${isInstalled}, version_client: ${versionClient}`); - // If version_client is null and game is not installed, trigger installation + lockPlayButton(false); + + // If version_client is null and game is not installed, show install page if (versionClient === null && !isInstalled) { console.log('Game not installed and version_client is null, showing install page...'); @@ -550,13 +562,7 @@ async function checkGameInstallation() { installPage.style.display = 'block'; if (launcher) launcher.style.display = 'none'; if (sidebar) sidebar.style.pointerEvents = 'none'; - - // Unlock play button since we're in install mode - lockPlayButton(false); } - } else { - // Game is installed or version is set, unlock play button - lockPlayButton(false); } } catch (error) { console.error('Error checking game installation:', error); From 61bcdf9413f4d537f9b17e1f91f69c74fc493720 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sat, 24 Jan 2026 12:01:37 +0100 Subject: [PATCH 41/95] curseforge api --- GUI/js/mods.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/GUI/js/mods.js b/GUI/js/mods.js index 631db3f..f1eb4a2 100644 --- a/GUI/js/mods.js +++ b/GUI/js/mods.js @@ -1,5 +1,5 @@ -let API_KEY = null; +let API_KEY = "$2a$10$bqk254NMZOWVTzLVJCcxEOmhcyUujKxA5xk.kQCN9q0KNYFJd5b32"; const CURSEFORGE_API = 'https://api.curseforge.com/v1'; const HYTALE_GAME_ID = 70216; @@ -13,7 +13,6 @@ let modsTotalPages = 1; export async function initModsManager() { try { if (window.electronAPI && window.electronAPI.getEnvVar) { - API_KEY = await window.electronAPI.getEnvVar('CURSEFORGE_API_KEY'); console.log('Loaded API Key:', API_KEY ? 'Yes' : 'No'); } } catch (err) { From 6b75858515745dbd992955379795b1ab3a8df15b Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sat, 24 Jan 2026 12:03:07 +0100 Subject: [PATCH 42/95] Delete .env.example --- .env.example | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index c1c8f4d..0000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -CURSEFORGE_API_KEY=$1234asdxXXXXXXkQCXXXXXXXXXXASDb32 -DISCORD_CLIENT_ID=561263XXXXXX \ No newline at end of file From c900129c1f34afab80794ce85487f3b584b0015f Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sat, 24 Jan 2026 12:05:10 +0100 Subject: [PATCH 43/95] fix patch --- backend/utils/clientPatcher.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/backend/utils/clientPatcher.js b/backend/utils/clientPatcher.js index d239e28..cbbcee3 100644 --- a/backend/utils/clientPatcher.js +++ b/backend/utils/clientPatcher.js @@ -249,13 +249,8 @@ class ClientPatcher { return { success: false, error }; } - if (this.isPatchedAlready(clientPath)) { - console.log(`Client already patched for ${newDomain}, skipping`); - if (progressCallback) { - progressCallback('Client already patched', 100); - } - return { success: true, alreadyPatched: true, patchCount: 0 }; - } + // FORCE PATCHING: Always patch, never skip + console.log(`Force patching client for ${newDomain}`); if (progressCallback) { progressCallback('Preparing to patch client...', 10); @@ -325,13 +320,8 @@ class ClientPatcher { return { success: false, error }; } - if (this.isPatchedAlready(serverPath)) { - console.log(`Server already patched for ${newDomain}, skipping`); - if (progressCallback) { - progressCallback('Server already patched', 100); - } - return { success: true, alreadyPatched: true, patchCount: 0 }; - } + // FORCE PATCHING: Always patch, never skip + console.log(`Force patching server for ${newDomain}`); if (progressCallback) { progressCallback('Preparing to patch server...', 10); From ecae7d2ee53df54371c34feb79db031f8ceba175 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sat, 24 Jan 2026 12:06:45 +0100 Subject: [PATCH 44/95] update --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0514d82..1b1fcf7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hytale-f2p-launcher", - "version": "2.0.11", + "version": "2.1.0", "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 679f065e242cead3dd67024a06520f1b47381ed6 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sat, 24 Jan 2026 12:09:54 +0100 Subject: [PATCH 45/95] delete updateManager --- backend/updateManager.js | 86 ---------------------------------------- main.js | 42 ++++---------------- 2 files changed, 7 insertions(+), 121 deletions(-) delete mode 100644 backend/updateManager.js diff --git a/backend/updateManager.js b/backend/updateManager.js deleted file mode 100644 index e7894e9..0000000 --- a/backend/updateManager.js +++ /dev/null @@ -1,86 +0,0 @@ -const axios = require('axios'); - -const UPDATE_CHECK_URL = 'https://files.hytalef2p.com/api/version_launcher'; -const CURRENT_VERSION = '2.0.2'; -const GITHUB_DOWNLOAD_URL = 'https://github.com/amiayweb/Hytale-F2P/'; - -class UpdateManager { - constructor() { - this.updateAvailable = false; - this.remoteVersion = null; - } - - async checkForUpdates() { - // Disabled: Using electron-updater for automatic updates instead - console.log('Update check skipped - using electron-updater'); - console.log(`Current version: ${CURRENT_VERSION}`); - - return { - updateAvailable: false, - currentVersion: CURRENT_VERSION, - newVersion: CURRENT_VERSION, - message: 'Using electron-updater for automatic updates' - }; - - /* kept for reference - try { - console.log('Checking for updates...'); - console.log(`Local version: ${CURRENT_VERSION}`); - - const response = await axios.get(UPDATE_CHECK_URL, { - timeout: 5000, - headers: { - 'User-Agent': 'Hytale-F2P-Launcher' - } - }); - - if (response.data && response.data.launcher_version) { - this.remoteVersion = response.data.launcher_version; - console.log(`Remote version: ${this.remoteVersion}`); - - if (this.remoteVersion !== CURRENT_VERSION) { - this.updateAvailable = true; - console.log('Update available!'); - return { - updateAvailable: true, - currentVersion: CURRENT_VERSION, - newVersion: this.remoteVersion, - downloadUrl: GITHUB_DOWNLOAD_URL - }; - } else { - console.log('Launcher is up to date'); - return { - updateAvailable: false, - currentVersion: CURRENT_VERSION, - newVersion: this.remoteVersion - }; - } - } else { - throw new Error('Invalid API response'); - } - } catch (error) { - console.error('Error checking for updates:', error.message); - return { - updateAvailable: false, - error: error.message, - currentVersion: CURRENT_VERSION - }; - } - */ - } - - getDownloadUrl() { - return GITHUB_DOWNLOAD_URL; - } - - getUpdateInfo() { - return { - updateAvailable: this.updateAvailable, - currentVersion: CURRENT_VERSION, - remoteVersion: this.remoteVersion, - downloadUrl: this.getDownloadUrl() - }; - } -} - -module.exports = UpdateManager; \ No newline at end of file diff --git a/main.js b/main.js index d47d94c..891f594 100644 --- a/main.js +++ b/main.js @@ -4,7 +4,6 @@ const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const fs = require('fs'); const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); -const UpdateManager = require('./backend/updateManager'); const logger = require('./backend/logger'); const profileManager = require('./backend/managers/profileManager'); @@ -26,7 +25,6 @@ if (!gotTheLock) { } let mainWindow; -let updateManager; let discordRPC = null; // Discord Rich Presence setup @@ -162,13 +160,7 @@ function createWindow() { // Initialize Discord Rich Presence initDiscordRPC(); - updateManager = new UpdateManager(); - setTimeout(async () => { - const updateInfo = await updateManager.checkForUpdates(); - if (updateInfo.updateAvailable) { - mainWindow.webContents.send('show-update-popup', updateInfo); - } - }, 3000); + // Auto-updates handled by electron-updater mainWindow.webContents.on('devtools-opened', () => { mainWindow.webContents.closeDevTools(); @@ -825,34 +817,14 @@ ipcMain.handle('copy-mod-file', async (event, sourcePath, modsPath) => { } }); -ipcMain.handle('check-for-updates', async () => { - try { - return await updateManager.checkForUpdates(); - } catch (error) { - console.error('Error checking for updates:', error); - return { updateAvailable: false, error: error.message }; - } -}); +// Auto-updates handled by electron-updater +// ipcMain.handle('check-for-updates', ...) - removed -ipcMain.handle('open-download-page', async () => { - try { - await shell.openExternal(updateManager.getDownloadUrl()); +// Auto-updates handled by electron-updater +// ipcMain.handle('open-download-page', ...) - removed - setTimeout(() => { - app.quit(); - }, 1000); - - - return { success: true }; - } catch (error) { - console.error('Error opening download page:', error); - return { success: false, error: error.message }; - } -}); - -ipcMain.handle('get-update-info', async () => { - return updateManager.getUpdateInfo(); -}); +// Auto-updates handled by electron-updater +// ipcMain.handle('get-update-info', ...) - removed ipcMain.handle('get-gpu-info', () => { try { From 87b168dd4cb22731f6b9cfe5c6293aed7ff69d3b Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sat, 24 Jan 2026 12:22:15 +0100 Subject: [PATCH 46/95] fix --- backend/managers/gameManager.js | 36 ++++++++++++++++----------------- backend/utils/clientPatcher.js | 35 +++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index c2be5d7..1673704 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -155,7 +155,22 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, try { if (progressCallback) { - progressCallback('Updating game files...', 0, null, null, null); + progressCallback('Backing up user data...', 5, null, null, null); + } + + // Backup UserData AVANT de télécharger/installer (critical for same-branch updates) + 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}`); + } + } catch (backupError) { + console.warn('[UpdateGameFiles] ✗ UserData backup failed:', backupError.message); + } + + if (progressCallback) { + progressCallback('Updating game files...', 10, null, null, null); } console.log(`Updating game files to version: ${newVersion} (branch: ${branch})`); @@ -167,32 +182,17 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, fs.mkdirSync(tempUpdateDir, { recursive: true }); if (progressCallback) { - progressCallback('Downloading new game version...', 10, null, null, null); + progressCallback('Downloading new game version...', 20, null, null, null); } const pwrFile = await downloadPWR(branch, newVersion, progressCallback, cacheDir); if (progressCallback) { - progressCallback('Extracting new files...', 50, null, null, null); + progressCallback('Extracting new files...', 60, null, null, null); } await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir); - if (progressCallback) { - progressCallback('Backing up user data...', 70, null, null, null); - } - - // Backup UserData from OLD branch (before switching) - 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}`); - } - } catch (backupError) { - console.warn('[UpdateGameFiles] ✗ UserData backup failed:', backupError.message); - } - if (progressCallback) { progressCallback('Replacing game files...', 80, null, null, null); } diff --git a/backend/utils/clientPatcher.js b/backend/utils/clientPatcher.js index cbbcee3..7be05df 100644 --- a/backend/utils/clientPatcher.js +++ b/backend/utils/clientPatcher.js @@ -323,19 +323,19 @@ class ClientPatcher { // FORCE PATCHING: Always patch, never skip console.log(`Force patching server for ${newDomain}`); - if (progressCallback) { - progressCallback('Preparing to patch server...', 10); - } - - console.log('Creating backup...'); - this.backupClient(serverPath); - if (progressCallback) { progressCallback('Extracting server JAR...', 20); } console.log('Opening server JAR...'); - const zip = new AdmZip(serverPath); + let zip; + try { + zip = new AdmZip(serverPath); + } catch (zipError) { + console.error('Failed to read server JAR:', zipError.message); + return { success: false, error: `Failed to read JAR: ${zipError.message}` }; + } + const entries = zip.getEntries(); console.log(`JAR contains ${entries.length} entries`); @@ -375,7 +375,24 @@ class ClientPatcher { } console.log('Writing patched JAR...'); - zip.writeZip(serverPath); + const tempPath = serverPath + '.patched.tmp'; + + // Write to temp file first to avoid corruption + try { + zip.writeZip(tempPath); + + // Replace original with patched version + if (fs.existsSync(serverPath)) { + fs.unlinkSync(serverPath); + } + fs.renameSync(tempPath, serverPath); + } catch (writeError) { + // Cleanup temp file if it exists + if (fs.existsSync(tempPath)) { + fs.unlinkSync(tempPath); + } + throw writeError; + } this.markAsPatched(serverPath); From 679799c074976bd670f994a038d7519665dddf30 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sat, 24 Jan 2026 12:33:42 +0100 Subject: [PATCH 47/95] fix installation branch --- GUI/js/install.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/GUI/js/install.js b/GUI/js/install.js index 304948e..ae3e363 100644 --- a/GUI/js/install.js +++ b/GUI/js/install.js @@ -60,18 +60,11 @@ export async function installGame() { const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player'; const installPath = installPathInput ? installPathInput.value.trim() : ''; - // Récupérer la branche sélectionnée const selectedBranchRadio = document.querySelector('input[name="installBranch"]:checked'); const selectedBranch = selectedBranchRadio ? selectedBranchRadio.value : 'release'; console.log(`[Install] Installing game with branch: ${selectedBranch}`); - // Sauvegarder la branche sélectionnée dans le config - if (window.electronAPI && window.electronAPI.saveVersionBranch) { - await window.electronAPI.saveVersionBranch(selectedBranch); - console.log(`[Install] Branch saved to config: ${selectedBranch}`); - } - if (window.LauncherUI) window.LauncherUI.showProgress(); isDownloading = true; if (installBtn) { From ca8ed171d1cd7bb9f1f7170c1d6eaaf5c21cffda Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sat, 24 Jan 2026 13:32:36 +0100 Subject: [PATCH 48/95] removed overlay installation --- GUI/index.html | 22 ++-------------- GUI/js/install.js | 49 +++++++++++++++++++++++------------- GUI/js/ui.js | 18 +------------ GUI/style.css | 64 ----------------------------------------------- 4 files changed, 34 insertions(+), 119 deletions(-) diff --git a/GUI/index.html b/GUI/index.html index a99aa61..0cb2785 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -618,20 +618,6 @@
- - - - + diff --git a/GUI/js/ui.js b/GUI/js/ui.js index b09399e..44182cb 100644 --- a/GUI/js/ui.js +++ b/GUI/js/ui.js @@ -10,6 +10,8 @@ let progressErrorContainer; let progressErrorMessage; let progressRetryInfo; let progressRetryBtn; +let progressJRRetryBtn; +let progressPWRRetryBtn; // Download retry state let currentDownloadState = { @@ -199,7 +201,8 @@ function updateProgress(data) { if ((data.error || (data.message && data.message.includes('failed'))) && !(data.retryState && data.retryState.isAutomaticRetry)) { const errorType = categorizeError(data.message); - showDownloadError(data.message, data.canRetry, errorType); + console.log('[UI] Showing download error:', { message: data.message, canRetry: data.canRetry, errorType }); + showDownloadError(data.message, data.canRetry, errorType, data); } else if (data.percent === 100) { hideDownloadError(); } else if (data.retryState && data.retryState.isAutomaticRetry) { @@ -230,9 +233,17 @@ function updateRetryState(retryState) { } } -function showDownloadError(errorMessage, canRetry = true, errorType = 'general') { - if (!progressErrorContainer || !progressErrorMessage || !progressRetryBtn) return; +function showDownloadError(errorMessage, canRetry = true, errorType = 'general', data = null) { + if (!progressErrorContainer || !progressErrorMessage) return; + console.log('[UI] showDownloadError called with:', { errorMessage, canRetry, errorType, data }); + console.log('[UI] Data properties:', { + hasData: !!data, + hasRetryData: !!(data && data.retryData), + dataErrorType: data && data.errorType, + dataIsJREError: data && data.retryData && data.retryData.isJREError + }); + currentDownloadState.lastError = errorMessage; currentDownloadState.canRetry = canRetry; currentDownloadState.errorType = errorType; @@ -242,13 +253,37 @@ function showDownloadError(errorMessage, canRetry = true, errorType = 'general') currentDownloadState.branch = data.retryData.branch; currentDownloadState.fileName = data.retryData.fileName; currentDownloadState.cacheDir = data.retryData.cacheDir; + // Override errorType if specified in data + if (data.errorType) { + currentDownloadState.errorType = data.errorType; + } } + // Hide all retry buttons first + if (progressRetryBtn) progressRetryBtn.style.display = 'none'; + if (progressJRRetryBtn) progressJRRetryBtn.style.display = 'none'; + if (progressPWRRetryBtn) progressPWRRetryBtn.style.display = 'none'; + // User-friendly error messages const userMessage = getErrorMessage(errorMessage, errorType); progressErrorMessage.textContent = userMessage; progressErrorContainer.style.display = 'block'; - progressRetryBtn.style.display = canRetry ? 'block' : 'none'; + + // Show appropriate retry button based on error type + if (canRetry) { + if (errorType === 'jre') { + if (progressJRRetryBtn) { + console.log('[UI] Showing JRE retry button'); + progressJRRetryBtn.style.display = 'block'; + } + } else { + // All other errors use PWR retry button (game download, butler, etc.) + if (progressPWRRetryBtn) { + console.log('[UI] Showing PWR retry button'); + progressPWRRetryBtn.style.display = 'block'; + } + } + } // Add visual indicators based on error type progressErrorContainer.className = `progress-error-container error-${errorType}`; @@ -261,6 +296,11 @@ function showDownloadError(errorMessage, canRetry = true, errorType = 'general') function hideDownloadError() { if (!progressErrorContainer) return; + // Hide all retry buttons + if (progressRetryBtn) progressRetryBtn.style.display = 'none'; + if (progressJRRetryBtn) progressJRRetryBtn.style.display = 'none'; + if (progressPWRRetryBtn) progressPWRRetryBtn.style.display = 'none'; + progressErrorContainer.style.display = 'none'; currentDownloadState.canRetry = false; currentDownloadState.lastError = null; @@ -589,6 +629,8 @@ function setupUI() { progressErrorMessage = document.getElementById('progressErrorMessage'); progressRetryInfo = document.getElementById('progressRetryInfo'); progressRetryBtn = document.getElementById('progressRetryBtn'); + progressJRRetryBtn = document.getElementById('progressJRRetryBtn'); + progressPWRRetryBtn = document.getElementById('progressPWRRetryBtn'); // Setup draggable progress bar setupProgressDrag(); @@ -784,6 +826,8 @@ function categorizeError(message) { return 'space'; } else if (msg.includes('conflict') || msg.includes('already exists')) { return 'conflict'; + } else if (msg.includes('jre') || msg.includes('java runtime')) { + return 'jre'; } else { return 'general'; } @@ -812,6 +856,8 @@ function getErrorMessage(technicalMessage, errorType) { return 'Insufficient disk space. Free up space and retry.'; case 'conflict': return 'Installation directory conflict. Please retry.'; + case 'jre': + return 'Java runtime download failed. Please retry.'; default: return 'Download failed. Please retry.'; } @@ -839,70 +885,192 @@ function updateConnectionQuality(quality) { // Enhanced retry button setup function setupRetryButton() { - if (!progressRetryBtn) return; - - progressRetryBtn.addEventListener('click', async () => { - if (!currentDownloadState.canRetry || currentDownloadState.isDownloading) { - return; - } - - // Disable retry button during retry - progressRetryBtn.disabled = true; - progressRetryBtn.textContent = '🔄 Retrying...'; - progressRetryBtn.classList.add('retrying'); - currentDownloadState.isDownloading = true; - - try { - // Hide error state during retry - hideDownloadError(); - - // Reset retry info styling for manual retries - if (progressRetryInfo) { - progressRetryInfo.style.background = ''; - progressRetryInfo.style.color = ''; - } - - // Update progress text with context-aware message - if (progressText) { - const contextMessage = getRetryContextMessage(); - progressText.textContent = contextMessage; + // Setup JRE retry button + if (progressJRRetryBtn) { + progressJRRetryBtn.addEventListener('click', async () => { + if (!currentDownloadState.canRetry || currentDownloadState.isDownloading) { + return; } + progressJRRetryBtn.disabled = true; + progressJRRetryBtn.textContent = 'Retrying...'; + progressJRRetryBtn.classList.add('retrying'); + currentDownloadState.isDownloading = true; - // Ensure retry data exists, create defaults if null - if (!currentDownloadState.retryData) { - currentDownloadState.retryData = { - branch: 'release', - fileName: '4.pwr' - }; - console.log('[UI] Created default retry data:', currentDownloadState.retryData); - } - - // Send retry request to backend - if (window.electronAPI && window.electronAPI.retryDownload) { - const result = await window.electronAPI.retryDownload(currentDownloadState.retryData); + try { + hideDownloadError(); - if (!result.success) { - throw new Error(result.error || 'Retry failed'); + if (progressRetryInfo) { + progressRetryInfo.style.background = ''; + progressRetryInfo.style.color = ''; + } + + if (progressText) { + progressText.textContent = 'Re-downloading Java runtime...'; } - } else { - // Fallback for development/testing - console.warn('electronAPI.retryDownload not available, simulating retry...'); - await new Promise(resolve => setTimeout(resolve, 2000)); - throw new Error('Retry API not available'); - } - } catch (error) { - console.error('Retry failed:', error); - const errorType = categorizeError(error.message); - showDownloadError(`Retry failed: ${error.message}`, true, errorType); - - // Reset retry button - progressRetryBtn.disabled = false; - progressRetryBtn.textContent = '🔄 Retry Download'; - progressRetryBtn.classList.remove('retrying'); - currentDownloadState.isDownloading = false; - } - }); + if (!currentDownloadState.retryData || currentDownloadState.errorType !== 'jre') { + currentDownloadState.retryData = { + isJREError: true, + jreUrl: '', + fileName: 'jre.tar.gz', + cacheDir: '', + osName: 'linux', + arch: 'amd64' + }; + console.log('[UI] Created default JRE retry data:', currentDownloadState.retryData); + } + + if (window.electronAPI && window.electronAPI.retryDownload) { + const result = await window.electronAPI.retryDownload(currentDownloadState.retryData); + if (!result.success) { + throw new Error(result.error || 'JRE retry failed'); + } + } else { + console.warn('electronAPI.retryDownload not available, simulating JRE retry...'); + await new Promise(resolve => setTimeout(resolve, 2000)); + throw new Error('JRE retry API not available'); + } + + } catch (error) { + console.error('JRE retry failed:', error); + showDownloadError(`JRE retry failed: ${error.message}`, true, 'jre'); + } finally { + if (progressJRRetryBtn) { + progressJRRetryBtn.disabled = false; + progressJRRetryBtn.textContent = 'Retry Java Download'; + progressJRRetryBtn.classList.remove('retrying'); + } + currentDownloadState.isDownloading = false; + } + }); + } + + // Setup PWR retry button + if (progressPWRRetryBtn) { + progressPWRRetryBtn.addEventListener('click', async () => { + if (!currentDownloadState.canRetry || currentDownloadState.isDownloading) { + return; + } + progressPWRRetryBtn.disabled = true; + progressPWRRetryBtn.textContent = 'Retrying...'; + progressPWRRetryBtn.classList.add('retrying'); + currentDownloadState.isDownloading = true; + + try { + hideDownloadError(); + + if (progressRetryInfo) { + progressRetryInfo.style.background = ''; + progressRetryInfo.style.color = ''; + } + + if (progressText) { + const contextMessage = getRetryContextMessage(); + progressText.textContent = contextMessage; + } + + if (!currentDownloadState.retryData || currentDownloadState.errorType === 'jre') { + currentDownloadState.retryData = { + branch: 'release', + fileName: '4.pwr' + }; + console.log('[UI] Created default PWR retry data:', currentDownloadState.retryData); + } + + if (window.electronAPI && window.electronAPI.retryDownload) { + const result = await window.electronAPI.retryDownload(currentDownloadState.retryData); + if (!result.success) { + throw new Error(result.error || 'Game retry failed'); + } + } else { + console.warn('electronAPI.retryDownload not available, simulating PWR retry...'); + await new Promise(resolve => setTimeout(resolve, 2000)); + throw new Error('Game retry API not available'); + } + + } catch (error) { + console.error('PWR retry failed:', error); + const errorType = categorizeError(error.message); + showDownloadError(`Game retry failed: ${error.message}`, true, errorType, error); + } finally { + if (progressPWRRetryBtn) { + progressPWRRetryBtn.disabled = false; + progressPWRRetryBtn.textContent = error && error.isJREError ? 'Retry Java Download' : 'Retry Game Download'; + progressPWRRetryBtn.classList.remove('retrying'); + } + currentDownloadState.isDownloading = false; + } + }); + } + + // Setup generic retry button (fallback) + if (progressRetryBtn) { + progressRetryBtn.addEventListener('click', async () => { + if (!currentDownloadState.canRetry || currentDownloadState.isDownloading) { + return; + } + progressRetryBtn.disabled = true; + progressRetryBtn.textContent = 'Retrying...'; + progressRetryBtn.classList.add('retrying'); + currentDownloadState.isDownloading = true; + + try { + hideDownloadError(); + + if (progressRetryInfo) { + progressRetryInfo.style.background = ''; + progressRetryInfo.style.color = ''; + } + + if (progressText) { + const contextMessage = getRetryContextMessage(); + progressText.textContent = contextMessage; + } + + if (!currentDownloadState.retryData) { + if (currentDownloadState.errorType === 'jre') { + currentDownloadState.retryData = { + isJREError: true, + jreUrl: '', + fileName: 'jre.tar.gz', + cacheDir: '', + osName: 'linux', + arch: 'amd64' + }; + } else { + currentDownloadState.retryData = { + branch: 'release', + fileName: '4.pwr' + }; + } + console.log('[UI] Created default retry data:', currentDownloadState.retryData); + } + + if (window.electronAPI && window.electronAPI.retryDownload) { + const result = await window.electronAPI.retryDownload(currentDownloadState.retryData); + if (!result.success) { + throw new Error(result.error || 'Retry failed'); + } + } else { + console.warn('electronAPI.retryDownload not available, simulating retry...'); + await new Promise(resolve => setTimeout(resolve, 2000)); + throw new Error('Retry API not available'); + } + + } catch (error) { + console.error('Retry failed:', error); + const errorType = categorizeError(error.message); + showDownloadError(`Retry failed: ${error.message}`, true, errorType); + } finally { + if (progressRetryBtn) { + progressRetryBtn.disabled = false; + progressRetryBtn.textContent = 'Retry Download'; + progressRetryBtn.classList.remove('retrying'); + } + currentDownloadState.isDownloading = false; + } + }); + } } function getRetryContextMessage() { @@ -925,6 +1093,8 @@ function getRetryContextMessage() { return 'Retrying with corrected permissions...'; case 'conflict': return 'Retrying after resolving conflicts...'; + case 'jre': + return 'Re-downloading Java runtime...'; default: return 'Initiating retry download...'; } diff --git a/GUI/style.css b/GUI/style.css index 8e492f1..8e28ef5 100644 --- a/GUI/style.css +++ b/GUI/style.css @@ -1815,6 +1815,12 @@ body { gap: 0.75rem; } +.progress-retry-buttons { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + .progress-retry-info { color: #fbbf24; font-family: 'JetBrains Mono', monospace; diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index 3087147..2fb8b62 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -520,9 +520,17 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver try { await downloadJRE(progressCallback, customCacheDir, customJreDir); } catch (error) { + // Don't immediately fall back to system Java for JRE download errors - let user retry + if (error.isJREError) { + console.error('[Install] JRE download failed, allowing user retry:', error.message); + throw error; // Re-throw JRE errors to trigger retry UI + } + + // For non-download JRE errors, fall back to system Java const fallback = await detectSystemJava(); if (fallback) { javaBin = fallback; + console.log('[Install] Using system Java as fallback'); } else { throw error; } diff --git a/backend/managers/javaManager.js b/backend/managers/javaManager.js index c7b48ac..84af570 100644 --- a/backend/managers/javaManager.js +++ b/backend/managers/javaManager.js @@ -9,7 +9,7 @@ const tar = require('tar'); const { expandHome, JRE_DIR } = require('../core/paths'); const { getOS, getArch } = require('../utils/platformUtils'); const { loadConfig } = require('../core/config'); -const { downloadFile } = require('../utils/fileManager'); +const { downloadFile, retryDownload } = require('../utils/fileManager'); const execFileAsync = promisify(execFile); const JAVA_EXECUTABLE = 'java' + (process.platform === 'win32' ? '.exe' : ''); @@ -188,6 +188,20 @@ async function getJavaDetection() { }; } +// Manual retry function for JRE downloads +async function retryJREDownload(url, cacheFile, progressCallback) { + console.log('Initiating manual JRE retry...'); + + // Ensure cache directory exists before retrying + const cacheDir = path.dirname(cacheFile); + if (!fs.existsSync(cacheDir)) { + console.log('Creating JRE cache directory:', cacheDir); + fs.mkdirSync(cacheDir, { recursive: true }); + } + + return await retryDownload(url, cacheFile, progressCallback); +} + async function downloadJRE(progressCallback, cacheDir, jreDir = JRE_DIR) { if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }); @@ -230,7 +244,40 @@ async function downloadJRE(progressCallback, cacheDir, jreDir = JRE_DIR) { progressCallback('Fetching Java runtime...', null, null, null, null); } console.log('Fetching Java runtime...'); - await downloadFile(platform.url, cacheFile, progressCallback); + let jreFile; + try { + jreFile = await downloadFile(platform.url, cacheFile, progressCallback); + + // If downloadFile returns false or undefined, it means the download failed + // We should retry the download with a manual retry + if (!jreFile || typeof jreFile !== 'string') { + console.log('[JRE Download] JRE file download failed or incomplete, attempting retry...'); + jreFile = await retryJREDownload(platform.url, cacheFile, progressCallback); + } + + // Double-check we have a valid file + if (!jreFile || typeof jreFile !== 'string') { + throw new Error(`JRE download failed: received invalid path ${jreFile}. Please retry download.`); + } + + } catch (downloadError) { + console.error('[JRE Download] JRE download failed:', downloadError.message); + + // Enhance error with retry information for the UI + const enhancedError = new Error(`JRE download failed: ${downloadError.message}`); + enhancedError.originalError = downloadError; + enhancedError.canRetry = downloadError.isConnectionLost ? false : (downloadError.canRetry !== false); + enhancedError.jreUrl = platform.url; + enhancedError.jreDest = cacheFile; + enhancedError.osName = osName; + enhancedError.arch = arch; + enhancedError.fileName = fileName; + enhancedError.cacheDir = cacheDir; + enhancedError.isJREError = true; // Flag to identify JRE errors + enhancedError.isConnectionLost = downloadError.isConnectionLost || false; + + throw enhancedError; + } console.log('Download finished'); } diff --git a/backend/utils/fileManager.js b/backend/utils/fileManager.js index 576cc96..972ca61 100644 --- a/backend/utils/fileManager.js +++ b/backend/utils/fileManager.js @@ -423,7 +423,8 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) { const canRetry = (error.canRetry === false) ? false : isRetryable; if (!canRetry || attempt === maxRetries - 1) { - retryState.canRetry = false; + // Don't set retryState.canRetry to false for max retries - user should still be able to retry manually + retryState.canRetry = error.canRetry === false ? false : true; console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`); break; } @@ -439,6 +440,9 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) { enhancedError.retryState = retryState; enhancedError.lastError = lastError; enhancedError.detailedError = detailedError; + + // Allow manual retry unless it's a connection lost error + enhancedError.canRetry = !lastError?.isConnectionLost && lastError?.canRetry !== false; throw enhancedError; } @@ -543,6 +547,13 @@ async function retryDownload(url, dest, progressCallback, previousError = null) additionalRetries = Math.max(2, 5 - previousError.retryState.attempts); } + // Ensure cache directory exists before retrying + const destDir = path.dirname(dest); + if (!fs.existsSync(destDir)) { + console.log('Creating cache directory:', destDir); + fs.mkdirSync(destDir, { recursive: true }); + } + try { await downloadFile(url, dest, progressCallback, additionalRetries); console.log('Manual retry successful'); diff --git a/main.js b/main.js index 7e67543..e7e2767 100644 --- a/main.js +++ b/main.js @@ -3,6 +3,7 @@ require('dotenv').config({ path: path.join(__dirname, '.env') }); const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const fs = require('fs'); const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); +const { retryPWRDownload } = require('./backend/managers/gameManager'); const logger = require('./backend/logger'); const profileManager = require('./backend/managers/profileManager'); @@ -430,37 +431,41 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath, console.log('[Main] Returning success response for install-game:', successResponse); return successResponse; } catch (error) { - console.error('Install error:', error); + // console.error('Install error:', error); const errorMessage = error.message || error.toString(); // Enhanced error data extraction for both download and Butler errors let errorData = { message: errorMessage, error: true, - canRetry: true, + canRetry: true, // Default to true, will be overridden by specific error props retryData: null }; + // Prioritize JRE errors first + if (error.isJREError) { + console.log('[Main] Processing JRE download error with retry context'); + errorData.retryData = { + isJREError: true, + jreUrl: error.jreUrl, + fileName: error.fileName, + cacheDir: error.cacheDir, + osName: error.osName, + arch: error.arch + }; + // For JRE errors, allow manual retry unless explicitly disabled + errorData.canRetry = error.canRetry !== false; + errorData.errorType = 'jre'; + } // Handle Butler-specific errors - if (error.butlerError) { + else if (error.butlerError) { console.log('[Main] Processing Butler error with retry context'); errorData.retryData = { branch: error.branch || 'release', fileName: error.fileName || '4.pwr', cacheDir: error.cacheDir }; - errorData.canRetry = error.canRetry !== undefined ? error.canRetry : true; - - // Add Butler-specific error details - if (error.stderr) { - console.error('[Main] Butler stderr:', error.stderr); - } - if (error.stdout) { - console.log('[Main] Butler stdout:', error.stdout); - } - if (error.errorCode) { - console.log('[Main] Butler error code:', error.errorCode); - } + errorData.canRetry = error.canRetry !== false; } // Handle PWR download errors else if (error.branch && error.fileName) { @@ -470,7 +475,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath, fileName: error.fileName, cacheDir: error.cacheDir }; - errorData.canRetry = error.canRetry !== undefined ? error.canRetry : true; + errorData.canRetry = error.canRetry !== false; } // Default fallback for other errors else { @@ -479,6 +484,8 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath, branch: 'release', fileName: '4.pwr' }; + // For generic errors, assume it's retryable unless specified + errorData.canRetry = error.canRetry !== false; } // Send enhanced error info for retry UI @@ -636,7 +643,7 @@ ipcMain.handle('uninstall-game', async () => { try { await uninstallGame(); } catch (error) { - console.error('Uninstall error:', error); + // console.error('Uninstall error:', error); return { success: false, error: error.message }; } }); @@ -669,16 +676,7 @@ ipcMain.handle('repair-game', async () => { ipcMain.handle('retry-download', async (event, retryData) => { try { console.log('[IPC] retry-download called with data:', retryData); - - // Handle null retry data gracefully - if (!retryData || !retryData.branch || !retryData.fileName) { - console.log('[IPC] Invalid retry data, using defaults'); - retryData = { - branch: 'release', - fileName: '4.pwr' - }; - } - + const progressCallback = (message, percent, speed, downloaded, total, retryState) => { if (mainWindow && !mainWindow.isDestroyed()) { const data = { @@ -693,14 +691,36 @@ ipcMain.handle('retry-download', async (event, retryData) => { } }; + // Handle JRE download retries + if (retryData && retryData.isJREError) { + console.log(`[IPC] Retrying JRE download: jreUrl=${retryData.jreUrl}, fileName=${retryData.fileName}`); + console.log('[IPC] Full JRE retry data:', JSON.stringify(retryData, null, 2)); + + const { retryJREDownload } = require('./backend/managers/javaManager'); + await retryJREDownload(retryData.jreUrl, jreCacheFile, progressCallback); + const jreCacheFile = path.join(retryData.cacheDir, retryData.fileName); + + return { success: true }; + } + + // Handle PWR download retries (default) + if (!retryData || !retryData.branch || !retryData.fileName) { + console.log('[IPC] Invalid retry data, using PWR defaults'); + retryData = { + branch: 'release', + fileName: '4.pwr' + }; + } + // Extract PWR download info from retryData const branch = retryData.branch; const fileName = retryData.fileName; const cacheDir = retryData.cacheDir; console.log(`[IPC] Retrying PWR download: branch=${branch}, fileName=${fileName}`); + console.log('[IPC] Full PWR retry data:', JSON.stringify(retryData, null, 2)); - // Perform the retry with enhanced context + // Perform retry with enhanced context await retryPWRDownload(branch, fileName, progressCallback, cacheDir); return { success: true }; @@ -710,15 +730,28 @@ ipcMain.handle('retry-download', async (event, retryData) => { // Send error update to frontend with context if (mainWindow && !mainWindow.isDestroyed()) { - const data = { - message: errorMessage, - error: true, - canRetry: true, - retryData: { + const isJreError = retryData?.isJREError; + const errorRetryData = isJreError ? + { + isJREError: true, + jreUrl: retryData?.jreUrl, + fileName: retryData?.fileName, + cacheDir: retryData?.cacheDir, + osName: retryData?.osName, + arch: retryData?.arch + } : + { branch: retryData?.branch || 'release', fileName: retryData?.fileName || '4.pwr', cacheDir: retryData?.cacheDir - } + }; + + const data = { + message: errorMessage, + error: true, + canRetry: error.canRetry !== false, // Respect canRetry from the thrown error + retryData: errorRetryData, + errorType: isJreError ? 'jre' : 'general' // Add errorType for the UI }; mainWindow.webContents.send('progress-update', data); } @@ -846,7 +879,6 @@ ipcMain.handle('load-settings', async () => { }); const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher'); -const { retryPWRDownload } = require('./backend/managers/gameManager'); const os = require('os'); ipcMain.handle('get-local-app-data', async () => { From f974d9c767bb0cf7ae9e08146c14c9e15be6c225 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sat, 24 Jan 2026 22:33:18 +0100 Subject: [PATCH 55/95] Update package-lock.json --- package-lock.json | 156 ++++++++++++++-------------------------------- 1 file changed, 47 insertions(+), 109 deletions(-) diff --git a/package-lock.json b/package-lock.json index 346839d..f7ad214 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "discord-rpc": "^4.0.1", "dotenv": "^17.2.3", "electron-updater": "^6.7.3", + "fs-extra": "^11.3.3", "tar": "^6.2.1", "uuid": "^9.0.1" }, @@ -147,6 +148,21 @@ "global-agent": "^3.0.0" } }, + "node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/@electron/notarize": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", @@ -345,34 +361,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/@electron/universal/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -389,16 +377,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@electron/universal/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/@electron/windows-sign": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", @@ -406,7 +384,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -421,50 +398,6 @@ "node": ">=14.14" } }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/windows-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -916,6 +849,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1679,8 +1613,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -1897,6 +1830,7 @@ "integrity": "sha512-ce4Ogns4VMeisIuCSK0C62umG0lFy012jd8LMZ6w/veHUeX4fqfDrGe+HTWALAEwK6JwKP+dhPvizhArSOsFbg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.4.0", "builder-util": "26.3.4", @@ -2272,7 +2206,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -2293,7 +2226,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -2310,16 +2242,6 @@ "dev": true, "license": "MIT" }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -2612,18 +2534,38 @@ } }, "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=14.14" + } + }, + "node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" } }, "node_modules/fs-minipass": { @@ -3561,7 +3503,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -3914,6 +3855,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3943,7 +3885,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -3961,7 +3902,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -4158,7 +4098,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -4619,7 +4558,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" From 127c38f98b79d4d2c748488dd3180efcd1567a60 Mon Sep 17 00:00:00 2001 From: xSamiVS Date: Sat, 24 Jan 2026 23:01:42 +0100 Subject: [PATCH 56/95] Update Spanish locale, add missing CurseForge API Key translation, implement Turkish translation, and fix contributor links comma. (#135) * Update Spanish locale and add missing CurseForge API Key translation - Updated the Spanish locale name to distinguish between multiple locale types. - Added missing translation for the page indicating the missing CurseForge API Key. * Implemented Turkish locale support * Add Turkish locale to available languages * Add missing comma in contributor links * Correct Portuguese language name in available languages --------- Co-authored-by: Fazri Gading --- GUI/index.html | 70 ++++---- GUI/js/i18n.js | 5 +- GUI/js/mods.js | 9 +- GUI/locales/en.json | 4 +- GUI/locales/{es.json => es-ES.json} | 10 +- GUI/locales/pt-BR.json | 4 +- GUI/locales/tr-TR.json | 246 ++++++++++++++++++++++++++++ 7 files changed, 306 insertions(+), 42 deletions(-) rename GUI/locales/{es.json => es-ES.json} (96%) create mode 100644 GUI/locales/tr-TR.json diff --git a/GUI/index.html b/GUI/index.html index fa94706..57a883e 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -602,38 +602,38 @@ - + - +