Compare commits

...

29 Commits

Author SHA1 Message Date
Fazri Gading
70fe4203ef Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop 2026-01-23 13:11:43 +08:00
Fazri Gading
f433120084 fix: adds EGL env var to detect installed NVIDIA GPU 2026-01-23 13:11:19 +08:00
AMIAY
f4099acbed Merge pull request #104 from chasem-dev/v2.0.11
V2.0.11 - Auto Updater
2026-01-23 02:04:15 +01:00
AMIAY
1ba6b22b74 fixed imgur restriction for UK 2026-01-22 20:40:28 +01:00
chasem-dev
a1bc88b754 Update package-lock.json to include new dependencies and versions, enhancing project stability and compatibility. 2026-01-22 14:07:04 -05:00
chasem-dev
24c2371b50 Add semantic versioning policy documentation - numerical versions only 2026-01-22 13:10:01 -05:00
chasem-dev
4c6e1a616e Merge upstream/develop into v2.0.11 - sync with main repository 2026-01-22 13:07:34 -05:00
chasem-dev
b54eb4e834 Remove outdated documentation files related to auto-updates, build instructions, and testing updates. Update dev-app-update.yml and package.json to reflect the correct GitHub owner. This cleanup streamlines the project and ensures accurate configuration for future updates. 2026-01-22 13:05:34 -05:00
chasem-dev
a1c74e4175 Fix: Remove portable target to fix SHA512 checksum mismatch
The portable and nsis targets both produced x64.exe files with the same
name, causing one to overwrite the other. The latest.yml contained the
checksum from one build while the actual file was from the other build.

Removed portable target - nsis installer is sufficient.
Bump version to 2.0.11

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-22 12:50:10 -05:00
chasem-dev
260e6c1126 Bump version to 2.0.10
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-22 11:53:34 -05:00
chasem-dev
6eb628559b Fix: Use explicit latest-linux.yml to prevent yml file collision
The glob pattern latest*.yml was matching both latest-linux.yml AND
latest.yml from the Linux build, causing the Windows latest.yml to be
overwritten with incorrect checksums.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-22 11:53:15 -05:00
chasem-dev
052b5dc7dc Bump version to 2.0.9 2026-01-22 11:30:59 -05:00
chasem-dev
7e9b5046df Bump version to 2.0.8 2026-01-22 11:13:01 -05:00
chasem-dev
204d6b21f6 Fix SHA512 checksum mismatch handling - clear cache and retry automatically 2026-01-22 11:12:59 -05:00
chasem-dev
740d516cfe Bump version to 2.0.7 2026-01-22 10:36:19 -05:00
chasem-dev
ce052add0d Fix update popup showing for same version - add version comparison checks 2026-01-22 10:36:18 -05:00
chasem-dev
d7a904c641 Bump version to 2.0.6 2026-01-22 10:18:33 -05:00
chasem-dev
d5d2f60c97 Bump version to 2.0.5 2026-01-22 10:18:30 -05:00
chasem-dev
61433bfeea Fix Linux metadata files in workflow and improve error handling 2026-01-22 10:18:28 -05:00
chasem-dev
9eb5d1759c Update publish config to point to chasem-dev fork 2026-01-22 09:11:10 -05:00
Arnav Singh
68d697576a 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 <fazrigading@gmail.com>
Co-authored-by: Arnav Singh <hi.arnavsingh3@gmail.com>
2026-01-22 18:11:16 +08:00
Rahul Sahani
a8da559e93 Custom Mod loading fix (#92)
* feat: Add Repair Game functionality including UserData backup and cache clearing

* feat: Add In-App Logs Viewer and Logs Folder shortcut

* feat: Add Open Logs feature

* disable dev tools

* Fix Settings UI

* Implement custom mod loading, autoimport, auto repair

* Fixed Custom Mod loading issues and merge issues

* feat: Externalize sensitive API keys and Discord client ID into environment variables using dotenv.

* feat(mods): add profile-based mod management and auto-repair
2026-01-22 18:01:57 +08:00
AMIAY
75f9403888 Display launcher version in UI
Adds a version display element to the bottom right of the UI, fetching the version from package.json via a new IPC handler. Updates main.js, preload.js, and ui.js to support retrieving and displaying the version, and adds relevant styles in style.css.
2026-01-22 08:07:32 +01:00
AMIAY
b61c94d348 Add splash screen to launcher startup
Introduced a new splash screen (splash.html) and updated main.js to display it on startup before loading the main window. The splash screen is shown for 2.5 seconds as a placeholder for future loading logic, improving user experience during application launch.
2026-01-22 07:59:27 +01:00
AMIAY
c0109575d6 Adjust news card aspect ratio and add Play tab style
Set a default aspect ratio for .news-card and add a specific style for the LATEST NEWS section in the Play tab to override the aspect ratio and use full height.
2026-01-22 07:43:39 +01:00
AMIAY
2a024b61dd Add installation effects and draggable progress bar
Introduces animated installation effects overlay and makes the progress bar draggable. Adds maximize window support, improves window controls styling, and enforces a single app instance. Removes the unused Skins page and related translations. Refines  various UI details for a more polished user experience.
2026-01-22 07:41:35 +01:00
chasem-dev
1c39e8e4c6 Update auto-update UI and configuration
- Fix version display (newVersion field)
- Add download progress bar with real-time updates
- Reorder buttons: Install & Restart (primary), Manually Download (secondary)
- Update dev-app-update.yml to point to fork
- Update package.json version to 2.0.2
2026-01-22 00:26:46 -05:00
chasem-dev
753bd4fd61 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.
2026-01-22 00:26:01 -05:00
chasem-dev
cefb4c5575 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
2026-01-22 00:03:02 -05:00
31 changed files with 7620 additions and 5364 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
CURSEFORGE_API_KEY=$1234asdxXXXXXXkQCXXXXXXXXXXASDb32
DISCORD_CLIENT_ID=561263XXXXXX

18
.github/README1.md vendored
View File

@@ -22,13 +22,25 @@ All builds run in parallel:
### Creating a Release
1. Update version in `package.json`
**⚠️ IMPORTANT: Semantic Versioning Required**
This project uses **strict semantic versioning with numerical versions only**:
-**Valid**: `2.0.1`, `2.0.11`, `2.1.0`, `3.0.0`
-**Invalid**: `2.0.2b`, `2.0.2a`, `2.0.1-beta`
**Format**: `MAJOR.MINOR.PATCH` (e.g., `2.0.11`)
The auto-update system requires semantic versioning for proper version comparison. Letter suffixes are not supported.
**Steps:**
1. Update version in `package.json` (use numerical format only, e.g., `2.0.11`)
2. Commit and push to `main`
3. Create and push a version tag:
```bash
git tag v2.0.1
git push origin v2.0.1
git tag v2.0.11
git push origin v2.0.11
```
The workflow will:

View File

@@ -33,10 +33,11 @@ jobs:
name: linux-builds
path: |
dist/*.AppImage
dist/*.AppImage.blockmap
dist/*.deb
dist/*.rpm
dist/*.pacman
dist/latest.yml
dist/latest-linux.yml
build-windows:
runs-on: windows-latest
@@ -53,6 +54,7 @@ jobs:
name: windows-builds
path: |
dist/*.exe
dist/*.exe.blockmap
dist/latest.yml
build-macos:

3
.gitignore vendored
View File

@@ -8,4 +8,7 @@ pkg/
# Package files
*.tar.zst
*.zst.DS_Store
*.zst
bun.lockb
.env

View File

@@ -15,7 +15,7 @@
<body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1">
<div class="absolute inset-0 z-0">
<img src="https://i.imgur.com/Visrk66.png" alt="Background" class="w-full h-full object-cover" />
<img src="https://assets.authbp.xyz/bg.png" alt="Background" class="w-full h-full object-cover" />
<div class="absolute inset-0 bg-black/60"></div>
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg viewBox=" 0 0 256 256"
xmlns="http://www.w3.org/2000/svg" %3E%3Cfilter id="noiseFilter" %3E%3CfeTurbulence type="fractalNoise"
@@ -51,11 +51,7 @@
<i class="fas fa-cog"></i>
<span class="nav-tooltip" data-i18n="nav.settings">Settings</span>
</div>
<div class="nav-item" data-page="skins">
<i class="fas fa-user"></i>
<span class="nav-tooltip" data-i18n="nav.skins">Skins</span>
</div>
<div class="nav-item" data-page="logs" id="openLogsBtn" onclick="openLogs()">
<div class="nav-item logs-nav-item" data-page="logs" id="openLogsBtn" onclick="openLogs()">
<i class="fas fa-terminal"></i>
<span class="nav-tooltip">Logs</span>
</div>
@@ -94,6 +90,9 @@
<button class="control-btn minimize" onclick="window.electronAPI?.minimizeWindow()">
<i class="fas fa-minus"></i>
</button>
<button class="control-btn maximize" onclick="toggleMaximize()">
<i class="fas fa-square"></i>
</button>
<button class="control-btn close" onclick="window.electronAPI?.closeWindow()">
<i class="fas fa-times"></i>
</button>
@@ -104,9 +103,6 @@
<h1 class="game-title">
HY<span class="title-accent">TALE</span>
</h1>
<div class="game-tags">
<span class="tag" data-i18n="header.f2p">FREE TO PLAY</span>
</div>
</div>
<div class="content-pages">
@@ -114,7 +110,7 @@
<div class="install-content">
<div class="install-header">
<h1 class="install-title">
HYTA<span class="title-accent">LE</span>
HY<span class="title-accent">TALE</span>
</h1>
<p class="install-subtitle" data-i18n="install.title">FREE TO PLAY LAUNCHER</p>
</div>
@@ -122,22 +118,26 @@
<div class="install-form">
<div class="form-group">
<label class="form-label" data-i18n="install.playerName">Player Name</label>
<input type="text" id="installPlayerName" data-i18n-placeholder="install.playerNamePlaceholder"
class="form-input" value="Player" />
<input type="text" id="installPlayerName"
data-i18n-placeholder="install.playerNamePlaceholder" class="form-input"
value="Player" />
</div>
<div class="form-group">
<label class="checkbox-group">
<input type="checkbox" id="installCustomCheck" class="custom-checkbox">
<span class="checkbox-label" data-i18n="install.customInstallation">Custom Installation</span>
<span class="checkbox-label" data-i18n="install.customInstallation">Custom
Installation</span>
</label>
<div id="installCustomOptions" class="custom-options">
<div class="form-subgroup">
<label class="form-label" data-i18n="install.installationFolder">Installation Folder</label>
<label class="form-label" data-i18n="install.installationFolder">Installation
Folder</label>
<div class="input-with-button">
<input type="text" id="installPath" data-i18n-placeholder="install.pathPlaceholder"
class="form-input" readonly />
<input type="text" id="installPath"
data-i18n-placeholder="install.pathPlaceholder" class="form-input"
readonly />
<button onclick="browseInstallPath()" class="browse-btn">
<i class="fas fa-folder-open"></i>
</button>
@@ -163,7 +163,8 @@
<i class="fas fa-play-circle mr-2"></i>
<span data-i18n="play.ready">READY TO PLAY</span>
</h2>
<p class="play-subtitle" data-i18n="play.subtitle">Launch Hytale and enter the adventure</p>
<p class="play-subtitle" data-i18n="play.subtitle">Launch Hytale and enter the
adventure</p>
</div>
<button id="homePlayBtn" class="home-play-button" onclick="launch()">
@@ -180,7 +181,8 @@
<span data-i18n="play.latestNews">LATEST NEWS</span>
</h2>
<button class="view-all-btn" onclick="navigateToPage('news')">
<span data-i18n="play.viewAll">VIEW ALL</span> <i class="fas fa-arrow-right ml-1"></i>
<span data-i18n="play.viewAll">VIEW ALL</span> <i
class="fas fa-arrow-right ml-1"></i>
</button>
</div>
<div id="newsGrid" class="news-grid-horizontal"></div>
@@ -191,7 +193,8 @@
<div class="mods-header">
<div class="mods-search-container">
<i class="fas fa-search"></i>
<input type="text" id="modsSearch" data-i18n-placeholder="mods.searchPlaceholder" class="mods-search" />
<input type="text" id="modsSearch" data-i18n-placeholder="mods.searchPlaceholder"
class="mods-search" />
</div>
<div class="mods-actions">
<button id="myModsBtn" class="mods-btn-primary">
@@ -210,7 +213,8 @@
<span data-i18n="mods.previous">PREVIOUS</span>
</button>
<span class="pagination-info">
<span data-i18n="mods.page">Page</span> <span id="currentPage">1</span> <span data-i18n="mods.of">of</span> <span id="totalPages">1</span>
<span data-i18n="mods.page">Page</span> <span id="currentPage">1</span> <span
data-i18n="mods.of">of</span> <span id="totalPages">1</span>
</span>
<button id="nextPage" class="pagination-btn">
<span data-i18n="mods.next">NEXT</span>
@@ -291,12 +295,14 @@
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.playerName">Player Name</label>
<label class="settings-input-label" data-i18n="settings.playerName">Player
Name</label>
<input type="text" id="settingsPlayerName" class="settings-input"
data-i18n-placeholder="settings.playerNamePlaceholder" maxlength="16" />
<p class="settings-hint">
<i class="fas fa-user"></i>
<span data-i18n="settings.playerNameHint">This name will be used in-game (1-16 characters)</span>
<span data-i18n="settings.playerNameHint">This name will be used in-game
(1-16 characters)</span>
</p>
</div>
</div>
@@ -307,8 +313,11 @@
onclick="openGameLocation()">
<i class="fas fa-folder-open"></i>
<div class="btn-content">
<div class="btn-title" data-i18n="settings.openGameLocation">Open Game Location</div>
<div class="btn-description" data-i18n="settings.openGameLocationDesc">Open the game installation folder</div>
<div class="btn-title" data-i18n="settings.openGameLocation">Open
Game Location</div>
<div class="btn-description"
data-i18n="settings.openGameLocationDesc">Open the game
installation folder</div>
</div>
</button>
</div>
@@ -320,8 +329,10 @@
onclick="repairGame()">
<i class="fas fa-tools"></i>
<div class="btn-content">
<div class="btn-title" data-i18n="settings.repairGame">Repair Game</div>
<div class="btn-description" data-i18n="settings.reinstallGame">Reinstall game files (preserves data)
<div class="btn-title" data-i18n="settings.repairGame">Repair Game
</div>
<div class="btn-description" data-i18n="settings.reinstallGame">
Reinstall game files (preserves data)
</div>
</div>
</button>
@@ -329,18 +340,25 @@
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.gpuPreference">GPU Preference</label>
<label class="settings-input-label" data-i18n="settings.gpuPreference">GPU
Preference</label>
<div class="segmented-control">
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto" checked>
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto"
checked>
<label for="gpu-auto" data-i18n="settings.gpuAuto">Auto</label>
<input type="radio" id="gpu-integrated" name="gpuPreference" value="integrated">
<label for="gpu-integrated" data-i18n="settings.gpuIntegrated">Integrated</label>
<input type="radio" id="gpu-dedicated" name="gpuPreference" value="dedicated">
<label for="gpu-dedicated" data-i18n="settings.gpuDedicated">Dedicated</label>
<input type="radio" id="gpu-integrated" name="gpuPreference"
value="integrated">
<label for="gpu-integrated"
data-i18n="settings.gpuIntegrated">Integrated</label>
<input type="radio" id="gpu-dedicated" name="gpuPreference"
value="dedicated">
<label for="gpu-dedicated"
data-i18n="settings.gpuDedicated">Dedicated</label>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.gpuHint">Select your preferred GPU (Linux: affects DRI_PRIME)</span>
<span data-i18n="settings.gpuHint">Select your preferred GPU (Linux:
affects DRI_PRIME)</span>
</p>
<div id="gpu-detection-info" class="gpu-detection-info"></div>
</div>
@@ -355,7 +373,8 @@
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.currentUUID">Current UUID</label>
<label class="settings-input-label" data-i18n="settings.currentUUID">Current
UUID</label>
<div class="uuid-display-container">
<input type="text" id="currentUuid" class="settings-input uuid-input"
readonly data-i18n-placeholder="settings.uuidPlaceholder" />
@@ -369,7 +388,8 @@
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.uuidHint">Your unique player identifier for this username</span>
<span data-i18n="settings.uuidHint">Your unique player identifier for
this username</span>
</p>
</div>
</div>
@@ -379,8 +399,10 @@
<button id="manageUuidsBtn" class="settings-action-btn">
<i class="fas fa-list"></i>
<div class="btn-content">
<div class="btn-title" data-i18n="settings.manageUUIDs">Manage All UUIDs</div>
<div class="btn-description" data-i18n="settings.manageUUIDsDesc">View and manage all player UUIDs</div>
<div class="btn-title" data-i18n="settings.manageUUIDs">Manage All
UUIDs</div>
<div class="btn-description" data-i18n="settings.manageUUIDsDesc">
View and manage all player UUIDs</div>
</div>
</button>
</div>
@@ -398,14 +420,38 @@
<input type="checkbox" id="discordRPCCheck" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.enableRPC">Enable Discord Rich Presence</div>
<div class="checkbox-description" data-i18n="settings.discordDescription">Show your launcher activity on Discord
<div class="checkbox-title" data-i18n="settings.enableRPC">Enable
Discord Rich Presence</div>
<div class="checkbox-description"
data-i18n="settings.discordDescription">Show your launcher activity
on Discord
</div>
</div>
</label>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-window-close"></i>
<span data-i18n="settings.closeLauncher">Launcher Behavior</span>
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="closeLauncherCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.closeOnStart">Close Launcher on game start</div>
<div class="checkbox-description" data-i18n="settings.closeOnStartDescription">
Automatically close the launcher after Hytale has launched
</div>
</div>
</label>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-coffee"></i>
@@ -417,8 +463,10 @@
<input type="checkbox" id="customJavaCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.useCustomJava">Use Custom Java Path</div>
<div class="checkbox-description" data-i18n="settings.javaDescription">Override the bundled Java runtime with
<div class="checkbox-title" data-i18n="settings.useCustomJava">Use
Custom Java Path</div>
<div class="checkbox-description" data-i18n="settings.javaDescription">
Override the bundled Java runtime with
your own installation</div>
</div>
</label>
@@ -426,7 +474,8 @@
<div id="customJavaOptions" class="custom-java-options" style="display: none;">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.javaPath">Java Executable Path</label>
<label class="settings-input-label" data-i18n="settings.javaPath">Java
Executable Path</label>
<div class="settings-input-with-button">
<input type="text" id="customJavaPath" class="settings-input"
data-i18n-placeholder="settings.javaPathPlaceholder" readonly />
@@ -437,7 +486,8 @@
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.javaHint">Select the Java installation folder (supports Windows, Mac, Linux)</span>
<span data-i18n="settings.javaHint">Select the Java installation folder
(supports Windows, Mac, Linux)</span>
</p>
</div>
</div>
@@ -451,7 +501,8 @@
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.selectLanguage">Select Language</label>
<label class="settings-input-label"
data-i18n="settings.selectLanguage">Select Language</label>
<select id="languageSelect" class="settings-input">
<!-- Options populated by i18n.js -->
</select>
@@ -462,14 +513,6 @@
</div>
</div>
<div id="skins-page" class="page">
<div class="placeholder-content">
<i class="fas fa-user text-6xl mb-4 text-purple-500"></i>
<h2 data-i18n="skins.title">Skins</h2>
<p data-i18n="skins.comingSoon">Skin customization coming soon...</p>
</div>
</div>
<div id="logs-page" class="page">
<div class="logs-container">
<div class="logs-header">
@@ -482,15 +525,18 @@
<i class="fas fa-copy"></i> <span data-i18n="settings.logsCopy">Copy</span>
</button>
<button class="logs-action-btn" onclick="refreshLogs()">
<i class="fas fa-sync-alt"></i> <span data-i18n="settings.logsRefresh">Refresh</span>
<i class="fas fa-sync-alt"></i> <span
data-i18n="settings.logsRefresh">Refresh</span>
</button>
<button class="logs-action-btn" onclick="openLogsFolder()">
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open Folder</span>
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open
Folder</span>
</button>
</div>
</div>
<div id="logsTerminal" class="logs-terminal">
<div class="text-gray-500 text-center mt-10" data-i18n="settings.logsLoading">Loading logs...</div>
<div class="text-gray-500 text-center mt-10" data-i18n="settings.logsLoading">Loading
logs...</div>
</div>
</div>
</div>
@@ -532,6 +578,20 @@
</div>
</div>
<!-- Installation effects overlay -->
<div id="installationEffects" class="installation-effects" style="display: none;">
<div class="space-effects">
<div class="warp-line"></div>
<div class="warp-line"></div>
<div class="warp-line"></div>
<div class="warp-line"></div>
<div class="warp-line"></div>
<div class="warp-line"></div>
<div class="warp-line"></div>
<div class="warp-line"></div>
</div>
</div>
<div id="chatUsernameModal" class="chat-username-modal" style="display: none;">
<div class="chat-username-modal-content">
<div class="chat-username-modal-header">
@@ -545,10 +605,12 @@
Choose a username to join the Players Chat
</p>
<div class="chat-username-input-group">
<label for="chatUsernameInput" class="chat-username-label" data-i18n="chat.username">Username</label>
<label for="chatUsernameInput" class="chat-username-label"
data-i18n="chat.username">Username</label>
<input type="text" id="chatUsernameInput" class="chat-username-input"
data-i18n-placeholder="chat.usernamePlaceholder" maxlength="20" autocomplete="off" />
<span class="chat-username-hint" data-i18n="chat.usernameHint">3-20 characters, letters, numbers, - and _ only</span>
<span class="chat-username-hint" data-i18n="chat.usernameHint">3-20 characters, letters, numbers, -
and _ only</span>
<span id="chatUsernameError" class="chat-username-error"></span>
</div>
</div>
@@ -613,8 +675,7 @@
<h3 class="uuid-section-title" data-i18n="uuid.setCustomUUID">Set Custom UUID</h3>
<div class="uuid-custom-form">
<input type="text" id="customUuidInput" class="uuid-input"
data-i18n-placeholder="uuid.customPlaceholder"
maxlength="36" />
data-i18n-placeholder="uuid.customPlaceholder" maxlength="36" />
<button id="setCustomUuidBtn" class="uuid-set-btn">
<i class="fas fa-check"></i>
<span data-i18n="uuid.setUUID">Set UUID</span>
@@ -622,7 +683,8 @@
</div>
<p class="uuid-custom-hint">
<i class="fas fa-exclamation-triangle"></i>
<span data-i18n="uuid.warning">Warning: Setting a custom UUID will change your current player identity</span>
<span data-i18n="uuid.warning">Warning: Setting a custom UUID will change your current player
identity</span>
</p>
</div>
</div>
@@ -646,8 +708,8 @@
<!-- Populated by JS -->
</div>
<div class="profile-create-section">
<input type="text" id="newProfileName" data-i18n-placeholder="profiles.newProfilePlaceholder" class="profile-input"
maxlength="20">
<input type="text" id="newProfileName" data-i18n-placeholder="profiles.newProfilePlaceholder"
class="profile-input" maxlength="20">
<button class="profile-create-btn" onclick="createNewProfile()">
<i class="fas fa-plus"></i> <span data-i18n="profiles.createProfile">Create Profile</span>
</button>
@@ -656,6 +718,11 @@
</div>
</div>
<div class="version-display-bottom">
<i class="fas fa-code-branch"></i>
<span id="launcherVersion">Loading...</span>
</div>
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
<div class="flex items-center justify-center text-xs text-gray-400">
<span>Made by <a href="https://github.com/amiayweb" target="_blank"
@@ -728,12 +795,15 @@
<div class="color-preview">
<h4 data-i18n="chat.colorModal.preview">Preview:</h4>
<div id="colorPreview" class="preview-username" data-i18n="chat.colorModal.previewUsername">YourUsername</div>
<div id="colorPreview" class="preview-username" data-i18n="chat.colorModal.previewUsername">
YourUsername</div>
</div>
</div>
<div class="chat-color-modal-footer">
<button class="btn-secondary" onclick="closeChatColorModal()"><span data-i18n="common.cancel">Cancel</span></button>
<button class="btn-primary" onclick="applyChatColor()"><span data-i18n="chat.colorModal.apply">Apply Color</span></button>
<button class="btn-secondary" onclick="closeChatColorModal()"><span
data-i18n="common.cancel">Cancel</span></button>
<button class="btn-primary" onclick="applyChatColor()"><span data-i18n="chat.colorModal.apply">Apply
Color</span></button>
</div>
</div>
</div>

View File

@@ -39,6 +39,19 @@ export function setupInstallation() {
}
});
}
// Setup installation effects listeners
if (window.electronAPI && window.electronAPI.onInstallationStart) {
window.electronAPI.onInstallationStart(() => {
showInstallationEffects();
});
}
if (window.electronAPI && window.electronAPI.onInstallationEnd) {
window.electronAPI.onInstallationEnd(() => {
hideInstallationEffects();
});
}
}
export async function installGame() {
@@ -78,12 +91,19 @@ export async function installGame() {
}
} catch (error) {
const errorMsg = window.i18n ? window.i18n.t('progress.installationFailed').replace('{error}', error.message) : `Installation failed: ${error.message}`;
// Hide installation effects on error
if (window.hideInstallationEffects) {
window.hideInstallationEffects();
}
// Reset button state on error
resetInstallButton();
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: errorMsg });
setTimeout(() => {
window.LauncherUI.hideProgress();
resetInstallButton();
}, 3000);
// Don't hide progress bar, just update the message
// User can see the error and close it manually
}
}
}

View File

@@ -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();

View File

@@ -5,8 +5,10 @@ let customJavaPath;
let browseJavaBtn;
let settingsPlayerName;
let discordRPCCheck;
let closeLauncherCheck;
let gpuPreferenceRadios;
// UUID Management elements
let currentUuidDisplay;
let copyUuidBtn;
@@ -161,8 +163,10 @@ function setupSettingsElements() {
browseJavaBtn = document.getElementById('browseJavaBtn');
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');
copyUuidBtn = document.getElementById('copyUuidBtn');
@@ -194,6 +198,11 @@ function setupSettingsElements() {
discordRPCCheck.addEventListener('change', saveDiscordRPC);
}
if (closeLauncherCheck) {
closeLauncherCheck.addEventListener('change', saveCloseLauncher);
}
// UUID event listeners
if (copyUuidBtn) {
copyUuidBtn.addEventListener('click', copyCurrentUuid);
@@ -348,6 +357,31 @@ async function loadDiscordRPC() {
}
}
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 {
if (!window.electronAPI || !settingsPlayerName) return;
@@ -462,9 +496,11 @@ async function loadAllSettings() {
await loadPlayerName();
await loadCurrentUuid();
await loadDiscordRPC();
await loadCloseLauncher();
await loadGpuPreference();
}
async function openGameLocation() {
try {
if (window.electronAPI && window.electronAPI.openGameLocation) {

View File

@@ -479,6 +479,9 @@ function setupUI() {
progressSpeed = document.getElementById('progressSpeed');
progressSize = document.getElementById('progressSize');
// Setup draggable progress bar
setupProgressDrag();
lockPlayButton(true);
setTimeout(() => {
@@ -497,10 +500,26 @@ function setupUI() {
setupSidebarLogo();
setupAnimations();
setupFirstLaunchHandlers();
loadLauncherVersion();
document.body.focus();
}
// Load launcher version from package.json
async function loadLauncherVersion() {
try {
if (window.electronAPI && window.electronAPI.getVersion) {
const version = await window.electronAPI.getVersion();
const versionElement = document.getElementById('launcherVersion');
if (versionElement) {
versionElement.textContent = `v${version}`;
}
}
} catch (error) {
console.error('Failed to load launcher version:', error);
}
}
window.LauncherUI = {
showPage,
setActiveNav,
@@ -510,4 +529,91 @@ window.LauncherUI = {
updateProgress
};
// Make installation effects globally available
window.showInstallationEffects = showInstallationEffects;
window.hideInstallationEffects = hideInstallationEffects;
// Draggable progress bar functionality
function setupProgressDrag() {
if (!progressOverlay) return;
let isDragging = false;
let offsetX;
let offsetY;
progressOverlay.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
// Only drag if clicking on the overlay itself, not on buttons or inputs
if (e.target.closest('.progress-bar-fill')) return;
if (e.target === progressOverlay || e.target.closest('.progress-content')) {
isDragging = true;
progressOverlay.classList.add('dragging');
// Get the current position of the progress overlay
const rect = progressOverlay.getBoundingClientRect();
offsetX = e.clientX - rect.left - progressOverlay.offsetWidth / 2;
offsetY = e.clientY - rect.top;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
// Calculate new position
const newX = e.clientX - offsetX - progressOverlay.offsetWidth / 2;
const newY = e.clientY - offsetY;
// Get window bounds
const maxX = window.innerWidth - progressOverlay.offsetWidth;
const maxY = window.innerHeight - progressOverlay.offsetHeight;
const minX = 0;
const minY = 0;
// Constrain to window bounds
const constrainedX = Math.max(minX, Math.min(newX, maxX));
const constrainedY = Math.max(minY, Math.min(newY, maxY));
progressOverlay.style.left = constrainedX + 'px';
progressOverlay.style.bottom = 'auto';
progressOverlay.style.top = constrainedY + 'px';
progressOverlay.style.transform = 'none';
}
}
function dragEnd() {
isDragging = false;
progressOverlay.classList.remove('dragging');
}
}
// Show/hide installation effects
function showInstallationEffects() {
const installationEffects = document.getElementById('installationEffects');
if (installationEffects) {
installationEffects.style.display = 'block';
}
}
function hideInstallationEffects() {
const installationEffects = document.getElementById('installationEffects');
if (installationEffects) {
installationEffects.style.display = 'none';
}
}
// Toggle maximize/restore window function
function toggleMaximize() {
if (window.electronAPI && window.electronAPI.maximizeWindow) {
window.electronAPI.maximizeWindow();
}
}
// Make toggleMaximize globally available
window.toggleMaximize = toggleMaximize;
document.addEventListener('DOMContentLoaded', setupUI);

View File

@@ -10,6 +10,23 @@ 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);
});
window.electronAPI.onUpdateError((errorInfo) => {
this.handleUpdateError(errorInfo);
});
this.checkForUpdatesOnDemand();
}
@@ -33,23 +50,46 @@ class ClientUpdateManager {
<div class="update-popup-versions">
<div class="version-row">
<span class="version-label">Current Version:</span>
<span class="version-current">${updateInfo.currentVersion}</span>
<span class="version-current">${updateInfo.currentVersion || updateInfo.version || 'Unknown'}</span>
</div>
<div class="version-row">
<span class="version-label">New Version:</span>
<span class="version-new">${updateInfo.newVersion}</span>
<span class="version-new">${updateInfo.newVersion || updateInfo.version || 'Unknown'}</span>
</div>
</div>
<div class="update-popup-message">
A new version of Hytale F2P Launcher is available.<br>
Please download the latest version to continue using the launcher.
<span id="update-status-text">Downloading update automatically...</span>
<div id="update-error-message" style="display: none; margin-top: 0.75rem; padding: 0.75rem; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 0.5rem; color: #fca5a5; font-size: 0.875rem;">
<i class="fas fa-exclamation-triangle" style="margin-right: 0.5rem;"></i>
<span id="update-error-text"></span>
</div>
</div>
<button id="update-download-btn" class="update-download-btn">
<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>
Download Update
</button>
<div id="update-progress-container" style="display: none; margin-bottom: 1rem;">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.75rem; color: #9ca3af;">
<span id="update-progress-percent">0%</span>
<span id="update-progress-speed">0 KB/s</span>
</div>
<div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden;">
<div id="update-progress-bar" style="width: 0%; height: 100%; background: linear-gradient(90deg, #3b82f6, #9333ea); transition: width 0.3s ease;"></div>
</div>
<div style="margin-top: 0.5rem; font-size: 0.75rem; color: #9ca3af; text-align: center;">
<span id="update-progress-size">0 MB / 0 MB</span>
</div>
</div>
<div id="update-buttons-container" style="display: none;">
<button id="update-install-btn" class="update-download-btn">
<i class="fas fa-check" style="margin-right: 0.5rem;"></i>
Install & Restart
</button>
<button id="update-download-btn" class="update-download-btn update-download-btn-secondary" style="margin-top: 0.75rem;">
<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>
Manually Download
</button>
</div>
<div class="update-popup-footer">
This popup cannot be closed until you update the launcher
@@ -62,6 +102,31 @@ class ClientUpdateManager {
this.blockInterface();
// Show progress container immediately (auto-download is enabled)
const progressContainer = document.getElementById('update-progress-container');
if (progressContainer) {
progressContainer.style.display = 'block';
}
const installBtn = document.getElementById('update-install-btn');
if (installBtn) {
installBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
installBtn.disabled = true;
installBtn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right: 0.5rem;"></i>Installing...';
try {
await window.electronAPI.quitAndInstallUpdate();
} catch (error) {
console.error('❌ Error installing update:', error);
installBtn.disabled = false;
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Install & Restart';
}
});
}
const downloadBtn = document.getElementById('update-download-btn');
if (downloadBtn) {
downloadBtn.addEventListener('click', async (e) => {
@@ -80,7 +145,7 @@ class ClientUpdateManager {
} catch (error) {
console.error('❌ Error opening download page:', error);
downloadBtn.disabled = false;
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Update';
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Manually Download';
}
});
}
@@ -99,6 +164,134 @@ class ClientUpdateManager {
console.log('🔔 Update popup displayed with new style');
}
updateDownloadProgress(progress) {
const progressBar = document.getElementById('update-progress-bar');
const progressPercent = document.getElementById('update-progress-percent');
const progressSpeed = document.getElementById('update-progress-speed');
const progressSize = document.getElementById('update-progress-size');
if (progressBar && progress) {
const percent = Math.round(progress.percent || 0);
progressBar.style.width = `${percent}%`;
if (progressPercent) {
progressPercent.textContent = `${percent}%`;
}
if (progressSpeed && progress.bytesPerSecond) {
const speedMBps = (progress.bytesPerSecond / 1024 / 1024).toFixed(2);
progressSpeed.textContent = `${speedMBps} MB/s`;
}
if (progressSize && progress.transferred && progress.total) {
const transferredMB = (progress.transferred / 1024 / 1024).toFixed(2);
const totalMB = (progress.total / 1024 / 1024).toFixed(2);
progressSize.textContent = `${transferredMB} MB / ${totalMB} MB`;
}
// Don't update status text here - it's already set and the progress bar shows the percentage
}
}
showUpdateDownloaded(updateInfo) {
const statusText = document.getElementById('update-status-text');
const progressContainer = document.getElementById('update-progress-container');
const buttonsContainer = document.getElementById('update-buttons-container');
if (statusText) {
statusText.textContent = 'Update downloaded! Ready to install.';
}
if (progressContainer) {
progressContainer.style.display = 'none';
}
if (buttonsContainer) {
buttonsContainer.style.display = 'block';
}
console.log('✅ Update downloaded, ready to install');
}
handleUpdateError(errorInfo) {
console.error('Update error:', errorInfo);
// If manual download is required, update the UI (this will handle status text)
if (errorInfo.requiresManualDownload) {
this.showManualDownloadRequired(errorInfo);
return; // Don't do anything else, showManualDownloadRequired handles everything
}
// For non-critical errors, just show error message without changing status
const errorMessage = document.getElementById('update-error-message');
const errorText = document.getElementById('update-error-text');
if (errorMessage && errorText) {
let message = errorInfo.message || 'An error occurred during the update process.';
if (errorInfo.isMacSigningError) {
message = 'Auto-update requires code signing. Please download manually.';
}
errorText.textContent = message;
errorMessage.style.display = 'block';
}
}
showManualDownloadRequired(errorInfo) {
const statusText = document.getElementById('update-status-text');
const progressContainer = document.getElementById('update-progress-container');
const buttonsContainer = document.getElementById('update-buttons-container');
const installBtn = document.getElementById('update-install-btn');
const downloadBtn = document.getElementById('update-download-btn');
const errorMessage = document.getElementById('update-error-message');
const errorText = document.getElementById('update-error-text');
// Hide progress and install button
if (progressContainer) {
progressContainer.style.display = 'none';
}
if (installBtn) {
installBtn.style.display = 'none';
}
// Update status message (only once, don't change it again)
if (statusText && !statusText.dataset.manualMode) {
statusText.textContent = 'Please download and install the update manually.';
statusText.dataset.manualMode = 'true'; // Mark that we've set manual mode
}
// Show error message with details
if (errorMessage && errorText) {
let message = 'Auto-update is not available. ';
if (errorInfo.isMacSigningError) {
message = 'This app requires code signing for automatic updates.';
} else if (errorInfo.isLinuxInstallError) {
message = 'Auto-installation requires root privileges. Please download and install the update manually using your package manager.';
} else if (errorInfo.message) {
message = errorInfo.message;
} else {
message = 'An error occurred during the update process.';
}
errorText.textContent = message;
errorMessage.style.display = 'block';
}
// Show and enable the manual download button (make it primary since it's the only option)
if (downloadBtn) {
downloadBtn.style.display = 'block';
downloadBtn.disabled = false;
downloadBtn.classList.remove('update-download-btn-secondary');
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Update Manually';
}
// Show buttons container if not already visible
if (buttonsContainer) {
buttonsContainer.style.display = 'block';
}
console.log('⚠️ Manual download required due to update error');
}
blockInterface() {
const mainContent = document.querySelector('.flex.w-full.h-screen');
if (mainContent) {
@@ -144,7 +337,12 @@ class ClientUpdateManager {
async checkForUpdatesOnDemand() {
try {
const updateInfo = await window.electronAPI.checkForUpdates();
if (updateInfo.updateAvailable) {
// Double-check that versions are actually different before showing popup
if (updateInfo.updateAvailable &&
updateInfo.newVersion &&
updateInfo.currentVersion &&
updateInfo.newVersion !== updateInfo.currentVersion) {
this.showUpdatePopup(updateInfo);
}
return updateInfo;

View File

@@ -4,14 +4,12 @@
"mods": "Mods",
"news": "News",
"chat": "Players Chat",
"settings": "Settings",
"skins": "Skins"
"settings": "Settings"
},
"header": {
"playersLabel": "Players:",
"manageProfiles": "Manage Profiles",
"defaultProfile": "Default",
"f2p": "FREE TO PLAY"
"defaultProfile": "Default"
},
"install": {
"title": "FREE TO PLAY LAUNCHER",
@@ -124,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",
@@ -148,10 +149,6 @@
"notificationText": "Join our Discord community!",
"joinButton": "Join Discord"
},
"skins": {
"title": "Skins",
"comingSoon": "Skin customization coming soon..."
},
"common": {
"confirm": "Confirm",
"cancel": "Cancel",

View File

@@ -4,14 +4,12 @@
"mods": "Mods",
"news": "Noticias",
"chat": "Chat de Jugadores",
"settings": "Configuración",
"skins": "Aspectos"
"settings": "Configuración"
},
"header": {
"playersLabel": "Jugadores:",
"manageProfiles": "Gestionar Perfiles",
"defaultProfile": "Predeterminado",
"f2p": "FREE TO PLAY"
"defaultProfile": "Predeterminado"
},
"install": {
"title": "LAUNCHER GRATUITO",
@@ -124,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",
@@ -148,10 +149,6 @@
"notificationText": "¡Únete a nuestra comunidad de Discord!",
"joinButton": "Unirse a Discord"
},
"skins": {
"title": "Aspectos",
"comingSoon": "Personalización de aspectos próximamente..."
},
"common": {
"confirm": "Confirmar",
"cancel": "Cancelar",

View File

@@ -4,14 +4,12 @@
"mods": "Mods",
"news": "Notícias",
"chat": "Chat de Jogadores",
"settings": "Configurações",
"skins": "Aparências"
"settings": "Configurações"
},
"header": {
"playersLabel": "Jogadores:",
"manageProfiles": "Gerenciar Perfis",
"defaultProfile": "Padrão",
"f2p": "FREE TO PLAY"
"defaultProfile": "Padrão"
},
"install": {
"title": "LANÇADOR JOGO GRATUITO",
@@ -124,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",
@@ -148,10 +149,7 @@
"notificationText": "Junte-se à nossa comunidade do Discord!",
"joinButton": "Entrar no Discord"
},
"skins": {
"title": "Aparências",
"comingSoon": "Personalização de aparências em breve..."
},
"common": {
"confirm": "Confirmar",
"cancel": "Cancelar",

178
GUI/splash.html Normal file
View File

@@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hytale F2P</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100%;
height: 100vh;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Space Grotesk', sans-serif;
overflow: hidden;
position: relative;
border-radius: 16px;
}
.background {
position: absolute;
inset: 0;
z-index: 0;
border-radius: 16px;
overflow: hidden;
}
.background img {
width: 100%;
height: 100%;
object-fit: cover;
}
.background::after {
content: '';
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.7);
}
.splash-container {
position: relative;
z-index: 10;
text-align: center;
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.logo {
width: 120px;
height: 120px;
margin: 0 auto 2rem;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
.logo img {
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0 0 30px rgba(147, 51, 234, 0.5));
}
.title {
font-size: 3rem;
font-weight: 700;
color: white;
margin-bottom: 1rem;
letter-spacing: 0.1em;
}
.title-accent {
background: linear-gradient(135deg, #9333ea, #a855f7, #c084fc);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
font-size: 0.875rem;
color: #9ca3af;
margin-bottom: 2rem;
text-transform: uppercase;
letter-spacing: 0.2em;
}
.loader {
width: 200px;
height: 4px;
background: rgba(147, 51, 234, 0.2);
border-radius: 2px;
margin: 0 auto;
overflow: hidden;
position: relative;
}
.loader::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, #9333ea, #a855f7, #c084fc);
animation: loading 1.5s ease-in-out infinite;
box-shadow: 0 0 20px rgba(147, 51, 234, 0.6);
}
@keyframes loading {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
.loading-text {
margin-top: 1rem;
font-size: 0.75rem;
color: #6b7280;
animation: blink 1.5s ease-in-out infinite;
}
@keyframes blink {
0%, 100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
</style>
</head>
<body>
<div class="background">
<img src="https://assets.authbp.xyz/bg.png" alt="Background">
</div>
<div class="splash-container">
<div class="logo">
<img src="./icon.png" alt="Hytale Logo">
</div>
<h1 class="title">
HY<span class="title-accent">TALE</span>
</h1>
<p class="subtitle">FREE TO PLAY LAUNCHER</p>
<div class="loader"></div>
<p class="loading-text">Loading...</p>
</div>
</body>
</html>

View File

@@ -26,7 +26,7 @@ body {
backdrop-filter: blur(20px);
border-right: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
z-index: 20;
z-index: 45;
}
.sidebar-logo {
@@ -109,6 +109,12 @@ body {
transform: scale(1.1);
}
/* Allow logs navigation during installation */
.logs-nav-item {
z-index: 100;
position: relative;
}
.nav-tooltip {
position: absolute;
left: 100%;
@@ -210,6 +216,63 @@ body {
border-color: rgba(147, 51, 234, 0.3);
}
.version-display {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
color: #9ca3af;
pointer-events: auto;
transition: all 0.3s ease;
}
.version-display i {
color: #9333ea;
font-size: 0.875rem;
}
.version-display:hover {
background: rgba(0, 0, 0, 0.6);
border-color: rgba(147, 51, 234, 0.3);
color: #ffffff;
}
.version-display-bottom {
position: fixed;
bottom: 3rem;
right: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
color: #9ca3af;
z-index: 45;
transition: all 0.3s ease;
}
.version-display-bottom i {
color: #9333ea;
font-size: 0.875rem;
}
.version-display-bottom:hover {
background: rgba(0, 0, 0, 0.8);
border-color: rgba(147, 51, 234, 0.3);
color: #ffffff;
}
.user-info {
display: flex;
@@ -374,10 +437,10 @@ body {
}
.control-btn {
width: 20px;
height: 20px;
border-radius: 50%;
border: none;
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer !important;
transition: all 0.3s ease;
display: flex !important;
@@ -386,24 +449,36 @@ body {
position: relative;
z-index: 100000 !important;
pointer-events: auto !important;
backdrop-filter: blur(10px);
}
.control-btn i {
font-size: 0.5rem;
opacity: 0;
font-size: 0.75rem;
opacity: 0.7;
transition: opacity 0.3s ease;
color: white;
}
.control-btn:hover i {
opacity: 1;
}
.maximize {
background: rgba(34, 197, 94, 0.2);
}
.maximize:hover {
background: rgba(34, 197, 94, 0.4);
border-color: rgba(34, 197, 94, 0.5);
}
.minimize {
background: rgba(251, 191, 36, 0.2);
}
.minimize:hover {
background: #fbbf24;
background: rgba(251, 191, 36, 0.4);
border-color: rgba(251, 191, 36, 0.5);
}
.close {
@@ -411,7 +486,8 @@ body {
}
.close:hover {
background: #ef4444;
background: rgba(239, 68, 68, 0.4);
border-color: rgba(239, 68, 68, 0.5);
}
@@ -429,7 +505,7 @@ body {
}
.title-accent {
color: #9333ea;
color: #bf84f7;
text-shadow: 0 0 20px rgba(147, 51, 234, 0.5);
}
@@ -928,15 +1004,22 @@ body {
.news-grid-horizontal {
display: flex;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-auto-rows: minmax(200px, 1fr);
gap: 1rem;
overflow-x: auto;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 1rem;
scrollbar-width: thin;
scrollbar-color: rgba(147, 51, 234, 0.3) transparent;
flex: 1;
min-height: 0;
align-content: start;
}
.news-grid-horizontal::-webkit-scrollbar {
width: 6px;
height: 6px;
}
@@ -954,9 +1037,11 @@ body {
}
.news-grid-horizontal .news-item {
min-width: 300px;
max-width: 300px;
height: 200px;
width: 100%;
min-width: 0;
max-width: none;
height: auto;
aspect-ratio: 16 / 9;
flex-shrink: 0;
}
@@ -997,6 +1082,12 @@ body {
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* Style spécifique pour LATEST NEWS (Play tab) */
.news-grid-horizontal .news-card {
aspect-ratio: unset;
height: 100%;
}
.news-card:hover {
box-shadow: 0 8px 40px rgba(147, 51, 234, 0.2);
border-color: rgba(147, 51, 234, 0.3);
@@ -1500,44 +1591,55 @@ body {
.progress-overlay {
position: fixed;
bottom: 1rem;
left: 1rem;
right: 1rem;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(30px);
border: 2px solid rgba(147, 51, 234, 0.3);
border-radius: 16px;
padding: 2rem;
z-index: 50;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
width: 400px;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(147, 51, 234, 0.3);
border-radius: 12px;
padding: 1.25rem;
z-index: 60;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.6),
0 0 40px rgba(147, 51, 234, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
0 4px 16px rgba(0, 0, 0, 0.5),
0 0 30px rgba(147, 51, 234, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
animation: progressGlow 3s ease-in-out infinite alternate;
cursor: move;
user-select: none;
}
.progress-overlay.dragging {
cursor: grabbing;
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.7),
0 0 50px rgba(147, 51, 234, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
@keyframes progressGlow {
0% {
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.6),
0 0 40px rgba(147, 51, 234, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
0 4px 16px rgba(0, 0, 0, 0.5),
0 0 30px rgba(147, 51, 234, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
border-color: rgba(147, 51, 234, 0.3);
}
100% {
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.6),
0 0 60px rgba(147, 51, 234, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
border-color: rgba(147, 51, 234, 0.5);
0 4px 16px rgba(0, 0, 0, 0.5),
0 0 40px rgba(147, 51, 234, 0.25),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
border-color: rgba(147, 51, 234, 0.4);
}
}
.progress-content {
display: flex;
flex-direction: column;
gap: 1.5rem;
gap: 0.75rem;
}
.progress-info {
@@ -1548,7 +1650,7 @@ body {
.progress-info span {
font-family: 'JetBrains Mono', monospace;
font-size: 0.875rem;
font-size: 0.8rem;
}
#progressText {
@@ -1572,8 +1674,8 @@ body {
#progressPercent {
color: #9333ea;
font-weight: 700;
font-size: 2rem;
text-shadow: 0 0 20px rgba(147, 51, 234, 0.8);
font-size: 1.25rem;
text-shadow: 0 0 15px rgba(147, 51, 234, 0.6);
animation: percentGlow 1.5s ease-in-out infinite;
}
@@ -1592,15 +1694,15 @@ body {
}
.progress-bar-container {
height: 16px;
height: 10px;
background: linear-gradient(90deg, #1f2937, #374151);
border: 2px solid rgba(147, 51, 234, 0.2);
border-radius: 12px;
border: 1px solid rgba(147, 51, 234, 0.2);
border-radius: 8px;
overflow: hidden;
position: relative;
box-shadow:
inset 0 2px 4px rgba(0, 0, 0, 0.5),
0 0 20px rgba(147, 51, 234, 0.1);
0 0 15px rgba(147, 51, 234, 0.1);
}
.progress-bar-container::before {
@@ -1636,15 +1738,15 @@ body {
#06b6d4 75%,
#10b981 100%);
background-size: 200% 100%;
border-radius: 10px;
border-radius: 6px;
width: 0%;
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
animation: progressFlow 3s linear infinite;
box-shadow:
0 0 30px rgba(147, 51, 234, 0.6),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
0 0 20px rgba(147, 51, 234, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
@keyframes progressFlow {
@@ -1692,6 +1794,71 @@ body {
text-shadow: 0 0 5px rgba(156, 163, 175, 0.3);
}
/* Installation effects */
.installation-effects {
position: fixed;
top: 0;
left: 80px;
width: calc(100% - 80px);
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
z-index: 40;
pointer-events: auto;
overflow: hidden;
}
.space-effects {
position: absolute;
width: 100%;
height: 100%;
perspective: 1000px;
}
.warp-line {
position: absolute;
width: 2px;
height: 100%;
background: linear-gradient(180deg,
transparent 0%,
rgba(147, 51, 234, 0.8) 50%,
transparent 100%);
box-shadow: 0 0 10px rgba(147, 51, 234, 0.8),
0 0 20px rgba(147, 51, 234, 0.4);
animation: warpSpeed 1.5s linear infinite;
opacity: 0;
}
.warp-line:nth-child(1) { left: 10%; animation-delay: 0s; }
.warp-line:nth-child(2) { left: 25%; animation-delay: 0.2s; }
.warp-line:nth-child(3) { left: 40%; animation-delay: 0.4s; }
.warp-line:nth-child(4) { left: 55%; animation-delay: 0.6s; }
.warp-line:nth-child(5) { left: 70%; animation-delay: 0.8s; }
.warp-line:nth-child(6) { left: 85%; animation-delay: 1s; }
.warp-line:nth-child(7) { left: 15%; animation-delay: 0.3s; }
.warp-line:nth-child(8) { left: 60%; animation-delay: 0.7s; }
@keyframes warpSpeed {
0% {
transform: translateY(-100%) scaleY(0);
opacity: 0;
}
10% {
opacity: 1;
}
50% {
opacity: 1;
transform: translateY(0%) scaleY(1);
}
90% {
opacity: 1;
}
100% {
transform: translateY(100%) scaleY(2);
opacity: 0;
}
}
.mods-manager {
display: flex;
@@ -4404,6 +4571,27 @@ select.settings-input option {
0 0 0 1px rgba(255, 255, 255, 0.05) !important;
}
.update-download-btn-secondary {
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(255, 255, 255, 0.05) !important;
}
.update-download-btn-secondary:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.15) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
transform: translateY(-1px) !important;
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.1) !important;
}
.update-download-btn-secondary:active:not(:disabled) {
transform: translateY(0) !important;
}
.update-popup-footer {
text-align: center !important;

View File

@@ -76,6 +76,25 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
---
## 📌 Versioning Policy
**⚠️ Important: Semantic Versioning Required**
This project follows **strict semantic versioning** with **numerical versions only**:
-**Valid**: `2.0.1`, `2.0.11`, `2.1.0`, `3.0.0`
-**Invalid**: `2.0.2b`, `2.0.2a`, `2.0.1-beta`, `v2.0.2b`
**Format**: `MAJOR.MINOR.PATCH` (e.g., `2.0.11`)
- **MAJOR**: Breaking changes
- **MINOR**: New features (backward compatible)
- **PATCH**: Bug fixes (backward compatible)
**Why?** The auto-update system requires semantic versioning for proper version comparison. Letter suffixes (like `2.0.2b`) are not supported and will cause update detection issues.
---
## 📋 Changelog
### 🆕 v2.0.2b *(Minor Update: Performance & Utilities)*

379
backend/appUpdater.js Normal file
View File

@@ -0,0 +1,379 @@
const { autoUpdater } = require('electron-updater');
const { app } = require('electron');
const logger = require('./logger');
const fs = require('fs');
const path = require('path');
const os = require('os');
class AppUpdater {
constructor(mainWindow) {
this.mainWindow = mainWindow;
this.autoUpdateAvailable = true; // Track if auto-update is possible
this.updateAvailable = false; // Track if an update was detected
this.updateVersion = null; // Store the available update version
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);
const currentVersion = app.getVersion();
const newVersion = info.version;
// Only proceed if the new version is actually different from current
if (newVersion === currentVersion) {
console.log('Update version matches current version, ignoring update-available event');
return;
}
this.updateAvailable = true;
this.updateVersion = newVersion;
this.autoUpdateAvailable = true; // Reset flag when new update is available
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-available', {
version: newVersion,
newVersion: newVersion,
currentVersion: currentVersion,
releaseName: info.releaseName,
releaseNotes: info.releaseNotes
});
// Also send to the old popup handler for compatibility
this.mainWindow.webContents.send('show-update-popup', {
currentVersion: currentVersion,
newVersion: newVersion,
version: newVersion
});
}
});
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);
// Check if this is a network error (not critical, don't show UI)
const errorMessage = err.message?.toLowerCase() || '';
const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
errorMessage.includes('network') ||
errorMessage.includes('connection') ||
errorMessage.includes('timeout') ||
errorMessage.includes('enotfound');
if (isNetworkError) {
console.warn('Network error in auto-updater - will retry later. Not showing error UI.');
return; // Don't show error UI for network issues
}
// Handle SHA512 checksum mismatch - this can happen during updates, just retry
const isChecksumError = err.code === 'ERR_CHECKSUM_MISMATCH' ||
errorMessage.includes('sha512') ||
errorMessage.includes('checksum') ||
errorMessage.includes('mismatch');
if (isChecksumError) {
console.warn('SHA512 checksum mismatch detected - clearing cache and will retry automatically. This is normal during updates.');
// Clear the update cache and let it re-download
this.clearUpdateCache();
// Don't show error UI - just log and let it retry automatically on next check
return;
}
// Determine if this is a critical error that prevents auto-update
const isCriticalError = this.isCriticalUpdateError(err);
if (isCriticalError) {
this.autoUpdateAvailable = false;
console.warn('Auto-update failed. Manual download required.');
}
// Handle missing metadata files (platform-specific builds)
if (err.code === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND') {
const platform = process.platform === 'darwin' ? 'macOS' :
process.platform === 'win32' ? 'Windows' : 'Linux';
const missingFile = process.platform === 'darwin' ? 'latest-mac.yml' :
process.platform === 'win32' ? 'latest.yml' : 'latest-linux.yml';
console.warn(`${platform} update metadata file (${missingFile}) not found in release.`);
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-error', {
message: `Update metadata file for ${platform} not found in release. Please download manually.`,
code: err.code,
requiresManualDownload: true,
updateVersion: this.updateVersion,
isMissingMetadata: true
});
}
return;
}
// Linux-specific: Handle installation permission errors
if (process.platform === 'linux') {
const errorMessage = err.message?.toLowerCase() || '';
const errorStack = err.stack?.toLowerCase() || '';
const isInstallError = errorMessage.includes('pkexec') ||
errorMessage.includes('gksudo') ||
errorMessage.includes('kdesudo') ||
errorMessage.includes('setuid root') ||
errorMessage.includes('exited with code 127') ||
errorStack.includes('pacmanupdater') ||
errorStack.includes('doinstall') ||
errorMessage.includes('installation failed');
if (isInstallError) {
console.warn('Linux installation error: Package installation requires root privileges. Manual installation required.');
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-error', {
message: 'Auto-installation requires root privileges. Please download and install the update manually.',
code: err.code || 'ERR_LINUX_INSTALL_PERMISSION',
isLinuxInstallError: true,
requiresManualDownload: true,
updateVersion: this.updateVersion
});
}
return;
}
}
// macOS-specific: Handle unsigned app errors gracefully
if (process.platform === 'darwin' && err.code === 2) {
console.warn('macOS update error: App may not be code-signed. Auto-update requires code signing.');
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-error', {
message: 'Auto-update requires code signing. Please download manually from GitHub.',
code: err.code,
isMacSigningError: true,
requiresManualDownload: true,
updateVersion: this.updateVersion
});
}
return;
}
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-error', {
message: err.message,
code: err.code,
requiresManualDownload: isCriticalError,
updateVersion: this.updateVersion
});
}
});
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);
// Network errors are not critical - just log and continue
const errorMessage = err.message?.toLowerCase() || '';
const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
errorMessage.includes('network') ||
errorMessage.includes('connection') ||
errorMessage.includes('timeout') ||
errorMessage.includes('enotfound');
if (isNetworkError) {
console.warn('Network error checking for updates - will retry later. This is not critical.');
return; // Don't show error UI for network issues
}
const isCritical = this.isCriticalUpdateError(err);
if (this.mainWindow && !this.mainWindow.isDestroyed() && isCritical) {
this.mainWindow.webContents.send('update-error', {
message: err.message || 'Failed to check for updates',
code: err.code,
requiresManualDownload: true
});
}
});
}
checkForUpdates() {
// Manual check for updates (returns promise)
return autoUpdater.checkForUpdates().catch(err => {
console.error('Failed to check for updates:', err);
// Network errors are not critical - just return no update available
const errorMessage = err.message?.toLowerCase() || '';
const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
errorMessage.includes('network') ||
errorMessage.includes('connection') ||
errorMessage.includes('timeout') ||
errorMessage.includes('enotfound');
if (isNetworkError) {
console.warn('Network error - update check unavailable');
return { updateInfo: null }; // Return empty result for network errors
}
const isCritical = this.isCriticalUpdateError(err);
if (isCritical) {
this.autoUpdateAvailable = false;
}
throw err;
});
}
quitAndInstall() {
// Quit and install the update
autoUpdater.quitAndInstall(false, true);
}
getUpdateInfo() {
return {
currentVersion: app.getVersion(),
updateAvailable: false
};
}
clearUpdateCache() {
try {
// Get the cache directory based on platform
const cacheDir = process.platform === 'darwin'
? path.join(os.homedir(), 'Library', 'Caches', `${app.getName()}-updater`)
: process.platform === 'win32'
? path.join(os.homedir(), 'AppData', 'Local', `${app.getName()}-updater`)
: path.join(os.homedir(), '.cache', `${app.getName()}-updater`);
if (fs.existsSync(cacheDir)) {
fs.rmSync(cacheDir, { recursive: true, force: true });
console.log('Update cache cleared successfully');
} else {
console.log('Update cache directory does not exist');
}
} catch (cacheError) {
console.warn('Could not clear update cache:', cacheError.message);
}
}
isCriticalUpdateError(err) {
// Check for errors that prevent auto-update
const errorMessage = err.message?.toLowerCase() || '';
const errorCode = err.code;
// Missing update metadata files (platform-specific)
if (errorCode === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND' ||
errorMessage.includes('cannot find latest') ||
errorMessage.includes('latest-linux.yml') ||
errorMessage.includes('latest-mac.yml') ||
errorMessage.includes('latest.yml')) {
return true;
}
// macOS code signing errors
if (process.platform === 'darwin' && (errorCode === 2 || errorMessage.includes('shipit'))) {
return true;
}
// Download failures
if (errorMessage.includes('download') && errorMessage.includes('fail')) {
return true;
}
// Network errors that prevent download (but we handle these separately as non-critical)
// Installation errors
if (errorMessage.includes('install') && errorMessage.includes('fail')) {
return true;
}
// Permission errors
if (errorMessage.includes('permission') || errorMessage.includes('access denied')) {
return true;
}
// Linux installation errors (pkexec, sudo issues)
if (process.platform === 'linux' && (
errorMessage.includes('pkexec') ||
errorMessage.includes('setuid root') ||
errorMessage.includes('exited with code 127') ||
errorMessage.includes('gksudo') ||
errorMessage.includes('kdesudo'))) {
return true;
}
// File system errors (but not "not found" for metadata files - handled above)
if (errorMessage.includes('enoent') || errorMessage.includes('cannot find')) {
// Only if it's not about metadata files
if (!errorMessage.includes('latest') && !errorMessage.includes('.yml')) {
return true;
}
}
// Generic critical error codes (but not checksum errors - those are handled separately)
if (errorCode && (errorCode >= 100 ||
errorCode === 'ERR_UPDATER_INVALID_RELEASE_FEED' ||
errorCode === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND')) {
// Don't treat checksum errors as critical - they're handled separately
if (errorCode === 'ERR_CHECKSUM_MISMATCH') {
return false;
}
return true;
}
return false;
}
}
module.exports = AppUpdater;

View File

@@ -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
};

View File

@@ -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
};

View File

@@ -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,

View File

@@ -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/)
// Use profile-specific paths
const profileModsPath = getProfileModsPath(activeProfile.id);
const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
const physicalModsPath = modsPath; // .../mods
const disabledModsPath = path.join(path.dirname(modsPath), '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) {

View File

@@ -1,4 +1,5 @@
const { execSync } = require('child_process');
const fs = require('fs');
function getOS() {
if (process.platform === 'win32') return 'windows';
@@ -217,12 +218,17 @@ function setupGpuEnvironment(gpuPreference) {
const envVars = {};
if (finalPreference === 'dedicated') {
envVars.DRI_PRIME = '1';
if (detected.vendor === 'nvidia') {
envVars.__NV_PRIME_RENDER_OFFLOAD = '1';
envVars.__GLX_VENDOR_LIBRARY_NAME = 'nvidia';
envVars.__GL_SHADER_DISK_CACHE = '1';
envVars.__GL_SHADER_DISK_CACHE_PATH = '/tmp';
const nvidiaEglFile = '/usr/share/glvnd/egl_vendor.d/10_nvidia.json';
if (fs.existsSync(nvidiaEglFile)) {
envVars.__EGL_VENDOR_LIBRARY_FILENAMES = nvidiaEglFile;
} else {
console.warn('NVIDIA EGL vendor library file not found, not setting __EGL_VENDOR_LIBRARY_FILENAMES');
}
} else {
envVars.DRI_PRIME = '1';
}
console.log('GPU environment variables:', envVars);
} else {

3
dev-app-update.yml Normal file
View File

@@ -0,0 +1,3 @@
provider: github
owner: amiayweb # Change to your own GitHub username
repo: Hytale-F2P

284
docs/AUTO-UPDATES.md Normal file
View File

@@ -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)

View File

@@ -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');
}
}
```

196
docs/TESTING-UPDATES.md Normal file
View File

@@ -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

144
main.js
View File

@@ -1,19 +1,36 @@
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 { 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');
logger.interceptConsole();
// Single instance lock
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
console.log('Another instance is already running. Quitting...');
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}
let mainWindow;
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 {
@@ -80,19 +97,47 @@ function toggleDiscordRPC(enabled) {
console.log('Discord RPC disconnected successfully');
} catch (error) {
console.error('Error disconnecting Discord RPC:', error.message);
discordRPC = null; // Force null même en cas d'erreur
discordRPC = null;
}
}
}
function createSplashScreen() {
const splashWindow = new BrowserWindow({
width: 500,
height: 350,
frame: false,
transparent: true,
alwaysOnTop: true,
resizable: false,
skipTaskbar: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
});
splashWindow.loadFile('GUI/splash.html');
splashWindow.center();
// close splash after 2.5s , need to implement a files check or whatever. just mock for now
setTimeout(() => {
splashWindow.close();
createWindow();
}, 2500);
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 720,
minWidth: 900,
minHeight: 600,
frame: false,
resizable: false,
resizable: true,
alwaysOnTop: false,
backgroundColor: '#090909',
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
@@ -104,6 +149,10 @@ function createWindow() {
mainWindow.loadFile('GUI/index.html');
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
// Cleanup Discord RPC when window is closed
mainWindow.on('closed', () => {
console.log('Main window closed, cleaning up Discord RPC...');
@@ -141,9 +190,20 @@ function createWindow() {
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) => {
e.preventDefault();
});
@@ -152,7 +212,9 @@ function createWindow() {
}
app.whenReady().then(async () => {
const packageJson = require('./package.json');
console.log('=== HYTALE F2P LAUNCHER STARTED ===');
console.log('Launcher version:', packageJson.version);
console.log('Platform:', process.platform);
console.log('Architecture:', process.arch);
console.log('Electron version:', process.versions.electron);
@@ -177,7 +239,7 @@ app.whenReady().then(async () => {
// Initialize Profile Manager (runs migration if needed)
profileManager.init();
createWindow();
createSplashScreen();
setTimeout(async () => {
let timeoutReached = false;
@@ -288,11 +350,10 @@ app.on('window-all-closed', () => {
cleanupDiscordRPC();
if (process.platform !== 'darwin') {
app.quit();
}
app.quit();
});
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
try {
const progressCallback = (message, percent, speed, downloaded, total) => {
@@ -310,7 +371,18 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g
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();
@@ -327,6 +399,11 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g
ipcMain.handle('install-game', async (event, playerName, javaPath, installPath) => {
try {
// Signal installation start
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('installation-start');
}
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
const data = {
@@ -342,11 +419,21 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath)
const result = await installGame(playerName, progressCallback, javaPath, installPath);
// Signal installation end
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('installation-end');
}
return result;
} catch (error) {
console.error('Install error:', error);
const errorMessage = error.message || error.toString();
// Signal installation end on error too
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('installation-end');
}
return { success: false, error: errorMessage };
}
});
@@ -414,7 +501,17 @@ 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'
@@ -626,6 +723,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');
@@ -736,11 +837,10 @@ ipcMain.handle('open-download-page', async () => {
await shell.openExternal(updateManager.getDownloadUrl());
setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
app.quit();
}, 1000);
return { success: true };
} catch (error) {
console.error('Error opening download page:', error);
@@ -782,17 +882,31 @@ ipcMain.handle('get-detected-gpu', () => {
});
ipcMain.handle('window-close', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
app.quit();
});
ipcMain.handle('window-minimize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.minimize();
}
});
ipcMain.handle('window-maximize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
}
});
ipcMain.handle('get-version', () => {
const packageJson = require('./package.json');
return packageJson.version;
});
ipcMain.handle('get-log-directory', () => {
return logger.getLogDirectory();
});

133
package-lock.json generated
View File

@@ -1,17 +1,19 @@
{
"name": "hytale-f2p-launcherv2",
"version": "2.0.2b",
"name": "hytale-f2p-launcher",
"version": "2.0.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hytale-f2p-launcherv2",
"version": "2.0.2b",
"name": "hytale-f2p-launcher",
"version": "2.0.11",
"license": "MIT",
"dependencies": {
"adm-zip": "^0.5.10",
"axios": "^1.6.0",
"discord-rpc": "^4.0.1",
"dotenv": "^17.2.3",
"electron-updater": "^6.7.3",
"tar": "^6.2.1",
"uuid": "^9.0.1"
},
@@ -1018,6 +1020,19 @@
"electron-builder-squirrel-windows": "26.4.0"
}
},
"node_modules/app-builder-lib/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/app-builder-lib/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -1073,7 +1088,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 +1297,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 +1724,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"
@@ -1962,10 +1974,9 @@
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true,
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -1990,6 +2001,19 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dotenv-expand/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2178,6 +2202,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 +2846,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 +3168,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 +3235,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 +3244,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 +3573,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 +4227,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 +4697,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",

View File

@@ -1,6 +1,6 @@
{
"name": "hytale-f2p-launcher",
"version": "2.0.2b",
"version": "2.0.11",
"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"
@@ -48,6 +48,8 @@
"adm-zip": "^0.5.10",
"axios": "^1.6.0",
"discord-rpc": "^4.0.1",
"dotenv": "^17.2.3",
"electron-updater": "^6.7.3",
"tar": "^6.2.1",
"uuid": "^9.0.1"
},
@@ -66,29 +68,69 @@
"preload.js",
"backend/**/*",
"GUI/**/*",
"package.json"
"package.json",
".env"
],
"win": {
"target": [
{ "target": "nsis", "arch": ["x64", "arm64"] },
{ "target": "portable", "arch": ["x64"] }
{
"target": "nsis",
"arch": [
"x64",
"arm64"
]
}
],
"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 +140,11 @@
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
},
"publish": {
"provider": "github",
"owner": "amiayweb",
"repo": "Hytale-F2P"
}
}
}

View File

@@ -5,6 +5,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
installGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath),
closeWindow: () => ipcRenderer.invoke('window-close'),
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
maximizeWindow: () => ipcRenderer.invoke('window-maximize'),
getVersion: () => ipcRenderer.invoke('get-version'),
saveUsername: (username) => ipcRenderer.invoke('save-username', username),
loadUsername: () => ipcRenderer.invoke('load-username'),
saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername),
@@ -19,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'),
@@ -30,6 +34,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),
@@ -44,6 +49,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
onProgressComplete: (callback) => {
ipcRenderer.on('progress-complete', () => callback());
},
onInstallationStart: (callback) => {
ipcRenderer.on('installation-start', () => callback());
},
onInstallationEnd: (callback) => {
ipcRenderer.on('installation-end', () => callback());
},
getUserId: () => ipcRenderer.invoke('get-user-id'),
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),