Compare commits

..

3 Commits

Author SHA1 Message Date
AMIAY
71974e031f Merge pull request #105 from gaurav-null/readme-tailscale-multiplayer
updated SERVER.md to add platform specification on radmin and add Tai…
2026-01-22 22:45:48 +01:00
guarav-null
5bd52f09db updated SERVER.md to add platform specification on radmin and add Tailscale guide for max compability 2026-01-23 03:05:29 +05:30
AMIAY
828d05ca33 Update README.md 2026-01-22 10:46:09 +01:00
32 changed files with 5381 additions and 7622 deletions

View File

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

18
.github/README1.md vendored
View File

@@ -22,25 +22,13 @@ All builds run in parallel:
### Creating a Release
**⚠️ 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`)
1. Update version in `package.json`
2. Commit and push to `main`
3. Create and push a version tag:
```bash
git tag v2.0.11
git push origin v2.0.11
git tag v2.0.1
git push origin v2.0.1
```
The workflow will:

View File

@@ -33,11 +33,10 @@ jobs:
name: linux-builds
path: |
dist/*.AppImage
dist/*.AppImage.blockmap
dist/*.deb
dist/*.rpm
dist/*.pacman
dist/latest-linux.yml
dist/latest.yml
build-windows:
runs-on: windows-latest
@@ -54,7 +53,6 @@ jobs:
name: windows-builds
path: |
dist/*.exe
dist/*.exe.blockmap
dist/latest.yml
build-macos:
@@ -117,4 +115,4 @@ jobs:
generate_release_notes: true
draft: true
# DYNAMIC FLAGS: Mark as pre-release ONLY IF it's NOT a tag (meaning it's a branch push)
prerelease: ${{ github.ref_type != 'tag' }}
prerelease: ${{ github.ref_type != 'tag' }}

5
.gitignore vendored
View File

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

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://assets.authbp.xyz/bg.png" alt="Background" class="w-full h-full object-cover" />
<img src="https://i.imgur.com/Visrk66.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,7 +51,11 @@
<i class="fas fa-cog"></i>
<span class="nav-tooltip" data-i18n="nav.settings">Settings</span>
</div>
<div class="nav-item logs-nav-item" data-page="logs" id="openLogsBtn" onclick="openLogs()">
<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()">
<i class="fas fa-terminal"></i>
<span class="nav-tooltip">Logs</span>
</div>
@@ -90,9 +94,6 @@
<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>
@@ -103,6 +104,9 @@
<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">
@@ -110,7 +114,7 @@
<div class="install-content">
<div class="install-header">
<h1 class="install-title">
HY<span class="title-accent">TALE</span>
HYTA<span class="title-accent">LE</span>
</h1>
<p class="install-subtitle" data-i18n="install.title">FREE TO PLAY LAUNCHER</p>
</div>
@@ -118,26 +122,22 @@
<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,8 +163,7 @@
<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()">
@@ -181,8 +180,7 @@
<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>
@@ -193,8 +191,7 @@
<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">
@@ -213,8 +210,7 @@
<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>
@@ -295,14 +291,12 @@
<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>
@@ -313,11 +307,8 @@
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>
@@ -329,10 +320,8 @@
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>
@@ -340,25 +329,18 @@
<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>
@@ -373,8 +355,7 @@
<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" />
@@ -388,8 +369,7 @@
</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>
@@ -399,10 +379,8 @@
<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>
@@ -420,38 +398,14 @@
<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>
@@ -463,10 +417,8 @@
<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>
@@ -474,8 +426,7 @@
<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 />
@@ -486,13 +437,12 @@
</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>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-language"></i>
@@ -501,8 +451,7 @@
<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>
@@ -513,6 +462,14 @@
</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">
@@ -525,18 +482,15 @@
<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>
@@ -578,20 +532,6 @@
</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">
@@ -605,12 +545,10 @@
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>
@@ -675,7 +613,8 @@
<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>
@@ -683,8 +622,7 @@
</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>
@@ -708,8 +646,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>
@@ -718,11 +656,6 @@
</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"
@@ -795,15 +728,12 @@
<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,19 +39,6 @@ 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() {
@@ -91,19 +78,12 @@ 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 });
// Don't hide progress bar, just update the message
// User can see the error and close it manually
setTimeout(() => {
window.LauncherUI.hideProgress();
resetInstallButton();
}, 3000);
}
}
}

View File

@@ -1,5 +1,5 @@
let API_KEY = null;
const API_KEY = '$2a$10$bqk254NMZOWVTzLVJCcxEOmhcyUujKxA5xk.kQCN9q0KNYFJd5b32';
const CURSEFORGE_API = 'https://api.curseforge.com/v1';
const HYTALE_GAME_ID = 70216;
@@ -11,15 +11,6 @@ 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();
@@ -426,10 +417,10 @@ async function deleteMod(modId) {
const mod = installedMods.find(m => m.id === modId);
if (!mod) return;
const confirmMsg = window.i18n ?
const confirmMsg = window.i18n ?
window.i18n.t('mods.confirmDelete').replace('{name}', mod.name) + ' ' + window.i18n.t('mods.confirmDeleteDesc') :
`Are you sure you want to delete "${mod.name}"? This action cannot be undone.`;
showConfirmModal(
confirmMsg,
async () => {

View File

@@ -3,11 +3,9 @@ let customJavaCheck;
let customJavaOptions;
let customJavaPath;
let browseJavaBtn;
let settingsPlayerName;
let discordRPCCheck;
let closeLauncherCheck;
let gpuPreferenceRadios;
let settingsPlayerName;
let discordRPCCheck;
let gpuPreferenceRadios;
// UUID Management elements
let currentUuidDisplay;
@@ -161,11 +159,9 @@ function setupSettingsElements() {
customJavaOptions = document.getElementById('customJavaOptions');
customJavaPath = document.getElementById('customJavaPath');
browseJavaBtn = document.getElementById('browseJavaBtn');
settingsPlayerName = document.getElementById('settingsPlayerName');
discordRPCCheck = document.getElementById('discordRPCCheck');
closeLauncherCheck = document.getElementById('closeLauncherCheck');
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
settingsPlayerName = document.getElementById('settingsPlayerName');
discordRPCCheck = document.getElementById('discordRPCCheck');
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
// UUID Management elements
currentUuidDisplay = document.getElementById('currentUuid');
@@ -194,14 +190,9 @@ function setupSettingsElements() {
settingsPlayerName.addEventListener('change', savePlayerName);
}
if (discordRPCCheck) {
discordRPCCheck.addEventListener('change', saveDiscordRPC);
}
if (closeLauncherCheck) {
closeLauncherCheck.addEventListener('change', saveCloseLauncher);
}
if (discordRPCCheck) {
discordRPCCheck.addEventListener('change', saveDiscordRPC);
}
// UUID event listeners
if (copyUuidBtn) {
@@ -344,43 +335,18 @@ async function saveDiscordRPC() {
}
}
async function loadDiscordRPC() {
try {
if (window.electronAPI && window.electronAPI.loadDiscordRPC) {
const enabled = await window.electronAPI.loadDiscordRPC();
if (discordRPCCheck) {
discordRPCCheck.checked = enabled;
}
}
} catch (error) {
console.error('Error loading Discord RPC setting:', error);
}
}
async function 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 loadDiscordRPC() {
try {
if (window.electronAPI && window.electronAPI.loadDiscordRPC) {
const enabled = await window.electronAPI.loadDiscordRPC();
if (discordRPCCheck) {
discordRPCCheck.checked = enabled;
}
}
} catch (error) {
console.error('Error loading Discord RPC setting:', error);
}
}
async function savePlayerName() {
try {
@@ -491,15 +457,13 @@ async function loadGpuPreference() {
}
}
async function loadAllSettings() {
await loadCustomJavaPath();
await loadPlayerName();
await loadCurrentUuid();
await loadDiscordRPC();
await loadCloseLauncher();
await loadGpuPreference();
}
async function loadAllSettings() {
await loadCustomJavaPath();
await loadPlayerName();
await loadCurrentUuid();
await loadDiscordRPC();
await loadGpuPreference();
}
async function openGameLocation() {
try {

View File

@@ -479,9 +479,6 @@ function setupUI() {
progressSpeed = document.getElementById('progressSpeed');
progressSize = document.getElementById('progressSize');
// Setup draggable progress bar
setupProgressDrag();
lockPlayButton(true);
setTimeout(() => {
@@ -500,26 +497,10 @@ 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,
@@ -529,91 +510,4 @@ 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,23 +10,6 @@ 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();
}
@@ -50,46 +33,23 @@ class ClientUpdateManager {
<div class="update-popup-versions">
<div class="version-row">
<span class="version-label">Current Version:</span>
<span class="version-current">${updateInfo.currentVersion || updateInfo.version || 'Unknown'}</span>
<span class="version-current">${updateInfo.currentVersion}</span>
</div>
<div class="version-row">
<span class="version-label">New Version:</span>
<span class="version-new">${updateInfo.newVersion || updateInfo.version || 'Unknown'}</span>
<span class="version-new">${updateInfo.newVersion}</span>
</div>
</div>
<div class="update-popup-message">
A new version of Hytale F2P Launcher is available.<br>
<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>
Please download the latest version to continue using the launcher.
</div>
<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>
<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 class="update-popup-footer">
This popup cannot be closed until you update the launcher
@@ -102,31 +62,6 @@ 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) => {
@@ -145,7 +80,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>Manually Download';
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Update';
}
});
}
@@ -164,134 +99,6 @@ 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) {
@@ -337,12 +144,7 @@ class ClientUpdateManager {
async checkForUpdatesOnDemand() {
try {
const updateInfo = await window.electronAPI.checkForUpdates();
// Double-check that versions are actually different before showing popup
if (updateInfo.updateAvailable &&
updateInfo.newVersion &&
updateInfo.currentVersion &&
updateInfo.newVersion !== updateInfo.currentVersion) {
if (updateInfo.updateAvailable) {
this.showUpdatePopup(updateInfo);
}
return updateInfo;

View File

@@ -4,12 +4,14 @@
"mods": "Mods",
"news": "News",
"chat": "Players Chat",
"settings": "Settings"
"settings": "Settings",
"skins": "Skins"
},
"header": {
"playersLabel": "Players:",
"manageProfiles": "Manage Profiles",
"defaultProfile": "Default"
"defaultProfile": "Default",
"f2p": "FREE TO PLAY"
},
"install": {
"title": "FREE TO PLAY LAUNCHER",
@@ -122,10 +124,7 @@
"logsCopy": "Copy",
"logsRefresh": "Refresh",
"logsFolder": "Open Folder",
"logsLoading": "Loading logs...",
"closeLauncher": "Launcher Behavior",
"closeOnStart": "Close Launcher on game start",
"closeOnStartDescription": "Automatically close the launcher after Hytale has launched"
"logsLoading": "Loading logs..."
},
"uuid": {
"modalTitle": "UUID Management",
@@ -149,6 +148,10 @@
"notificationText": "Join our Discord community!",
"joinButton": "Join Discord"
},
"skins": {
"title": "Skins",
"comingSoon": "Skin customization coming soon..."
},
"common": {
"confirm": "Confirm",
"cancel": "Cancel",

View File

@@ -4,12 +4,14 @@
"mods": "Mods",
"news": "Noticias",
"chat": "Chat de Jugadores",
"settings": "Configuración"
"settings": "Configuración",
"skins": "Aspectos"
},
"header": {
"playersLabel": "Jugadores:",
"manageProfiles": "Gestionar Perfiles",
"defaultProfile": "Predeterminado"
"defaultProfile": "Predeterminado",
"f2p": "FREE TO PLAY"
},
"install": {
"title": "LAUNCHER GRATUITO",
@@ -122,10 +124,7 @@
"logsCopy": "Copiar",
"logsRefresh": "Actualizar",
"logsFolder": "Abrir Carpeta",
"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"
"logsLoading": "Cargando registros..."
},
"uuid": {
"modalTitle": "Gestión de UUID",
@@ -149,6 +148,10 @@
"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,12 +4,14 @@
"mods": "Mods",
"news": "Notícias",
"chat": "Chat de Jogadores",
"settings": "Configurações"
"settings": "Configurações",
"skins": "Aparências"
},
"header": {
"playersLabel": "Jogadores:",
"manageProfiles": "Gerenciar Perfis",
"defaultProfile": "Padrão"
"defaultProfile": "Padrão",
"f2p": "FREE TO PLAY"
},
"install": {
"title": "LANÇADOR JOGO GRATUITO",
@@ -122,10 +124,7 @@
"logsCopy": "Copiar",
"logsRefresh": "Atualizar",
"logsFolder": "Abrir Pasta",
"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"
"logsLoading": "Carregando registros..."
},
"uuid": {
"modalTitle": "Gerenciamento de UUID",
@@ -149,7 +148,10 @@
"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",

View File

@@ -1,178 +0,0 @@
<!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: 45;
z-index: 20;
}
.sidebar-logo {
@@ -109,12 +109,6 @@ body {
transform: scale(1.1);
}
/* Allow logs navigation during installation */
.logs-nav-item {
z-index: 100;
position: relative;
}
.nav-tooltip {
position: absolute;
left: 100%;
@@ -216,63 +210,6 @@ 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;
@@ -437,10 +374,10 @@ body {
}
.control-btn {
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
width: 20px;
height: 20px;
border-radius: 50%;
border: none;
cursor: pointer !important;
transition: all 0.3s ease;
display: flex !important;
@@ -449,36 +386,24 @@ body {
position: relative;
z-index: 100000 !important;
pointer-events: auto !important;
backdrop-filter: blur(10px);
}
.control-btn i {
font-size: 0.75rem;
opacity: 0.7;
font-size: 0.5rem;
opacity: 0;
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: rgba(251, 191, 36, 0.4);
border-color: rgba(251, 191, 36, 0.5);
background: #fbbf24;
}
.close {
@@ -486,8 +411,7 @@ body {
}
.close:hover {
background: rgba(239, 68, 68, 0.4);
border-color: rgba(239, 68, 68, 0.5);
background: #ef4444;
}
@@ -505,7 +429,7 @@ body {
}
.title-accent {
color: #bf84f7;
color: #9333ea;
text-shadow: 0 0 20px rgba(147, 51, 234, 0.5);
}
@@ -1004,22 +928,15 @@ body {
.news-grid-horizontal {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-auto-rows: minmax(200px, 1fr);
display: flex;
gap: 1rem;
overflow-y: auto;
overflow-x: hidden;
overflow-x: auto;
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;
}
@@ -1037,11 +954,9 @@ body {
}
.news-grid-horizontal .news-item {
width: 100%;
min-width: 0;
max-width: none;
height: auto;
aspect-ratio: 16 / 9;
min-width: 300px;
max-width: 300px;
height: 200px;
flex-shrink: 0;
}
@@ -1082,12 +997,6 @@ 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);
@@ -1591,55 +1500,44 @@ body {
.progress-overlay {
position: fixed;
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;
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;
box-shadow:
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);
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);
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 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);
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);
border-color: rgba(147, 51, 234, 0.3);
}
100% {
box-shadow:
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);
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);
}
}
.progress-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: 1.5rem;
}
.progress-info {
@@ -1650,7 +1548,7 @@ body {
.progress-info span {
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
font-size: 0.875rem;
}
#progressText {
@@ -1674,8 +1572,8 @@ body {
#progressPercent {
color: #9333ea;
font-weight: 700;
font-size: 1.25rem;
text-shadow: 0 0 15px rgba(147, 51, 234, 0.6);
font-size: 2rem;
text-shadow: 0 0 20px rgba(147, 51, 234, 0.8);
animation: percentGlow 1.5s ease-in-out infinite;
}
@@ -1694,15 +1592,15 @@ body {
}
.progress-bar-container {
height: 10px;
height: 16px;
background: linear-gradient(90deg, #1f2937, #374151);
border: 1px solid rgba(147, 51, 234, 0.2);
border-radius: 8px;
border: 2px solid rgba(147, 51, 234, 0.2);
border-radius: 12px;
overflow: hidden;
position: relative;
box-shadow:
inset 0 2px 4px rgba(0, 0, 0, 0.5),
0 0 15px rgba(147, 51, 234, 0.1);
0 0 20px rgba(147, 51, 234, 0.1);
}
.progress-bar-container::before {
@@ -1738,15 +1636,15 @@ body {
#06b6d4 75%,
#10b981 100%);
background-size: 200% 100%;
border-radius: 6px;
border-radius: 10px;
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 20px rgba(147, 51, 234, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
0 0 30px rgba(147, 51, 234, 0.6),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
@keyframes progressFlow {
@@ -1794,71 +1692,6 @@ 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;
@@ -4571,27 +4404,6 @@ 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

@@ -6,7 +6,7 @@
![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey?style=for-the-badge)
![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge)
**A modern, cross-platform offline launcher for Hytale with automatic updates and multiplayer support (all OS supported)**
**A modern, cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)**
[![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/network/members)
@@ -76,25 +76,6 @@ 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)*

View File

@@ -89,8 +89,23 @@ Creates a virtual LAN - all players need to install it:
3. **Host via Online Play** - Use your Radmin VPN IP instead
4. **Friends connect** - They'll see you on the virtual LAN
Both options bypass all NAT/CGNAT issues.
Both options bypass all NAT/CGNAT issues. But for **Windows machines only!**
#### Option 3: Tailscale
It creates mesh VPN service that streamlines connecting devices and services securely across different networks. And **works crossplatform!!**
1. All member's are required to download [Tailscale](https://tailscale.com/download) on your device.
[Once installed, Tailwind starts and live inside your hidden icon section in Windows, Mac and Linux]
2. Create a **common tailscale** account which will shared among your friends to log in.
3. Ask your **host to login in to thier tailscale client first**, then the other members.
##### Host
1. Open your singleplayer world
2. Go to Online Play settings
3. Re-save your settings to generate a new share code
##### Friends
1. Ensure Tailscale is connected
2. Use the new share code to connect
[To test your connection, ping the host's ipv4 mentioned in tailwind]
---
## Part 2: Dedicated Server (Advanced)

View File

@@ -1,379 +0,0 @@
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,15 +156,6 @@ 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();
@@ -340,8 +331,5 @@ module.exports = {
resetCurrentUserUuid,
// GPU Preference exports
saveGpuPreference,
loadGpuPreference,
// Close Launcher export
saveCloseLauncherOnStart,
loadCloseLauncherOnStart
loadGpuPreference
};

View File

@@ -162,18 +162,13 @@ 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) {
@@ -182,34 +177,6 @@ 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,
@@ -224,6 +191,5 @@ module.exports = {
findClientPath,
findUserDataPath,
findUserDataRecursive,
getModsPath,
getProfilesDir
getModsPath
};

View File

@@ -17,8 +17,6 @@ const {
loadDiscordRPC,
saveLanguage,
loadLanguage,
saveCloseLauncherOnStart,
loadCloseLauncherOnStart,
saveModsToConfig,
loadModsFromConfig,
getUuidForUser,
@@ -126,10 +124,6 @@ module.exports = {
saveLanguage,
loadLanguage,
// Close Launcher functions
saveCloseLauncherOnStart,
loadCloseLauncherOnStart,
// GPU Preference functions
saveGpuPreference,
loadGpuPreference,

View File

@@ -2,30 +2,10 @@ const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const axios = require('axios');
const { getModsPath, getProfilesDir } = require('../core/paths');
const { getModsPath } = 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);
}
@@ -55,33 +35,30 @@ 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 || [];
// Use profile-specific paths
const profileModsPath = getProfileModsPath(activeProfile.id);
const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
if (!fs.existsSync(profileModsPath)) fs.mkdirSync(profileModsPath, { recursive: true });
if (!fs.existsSync(profileDisabledModsPath)) fs.mkdirSync(profileDisabledModsPath, { recursive: true });
const profileModFiles = new Set(profileMods.map(m => m.fileName));
// We only return mods that are explicitly in the profile
// Check which ones are physically present (either in mods/ or DisabledMods/)
const physicalModsPath = modsPath; // .../mods
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
const validMods = [];
for (const modConfig of profileMods) {
// Check if file exists in either location
const inEnabled = fs.existsSync(path.join(profileModsPath, modConfig.fileName));
const inDisabled = fs.existsSync(path.join(profileDisabledModsPath, modConfig.fileName));
const inEnabled = fs.existsSync(path.join(physicalModsPath, modConfig.fileName));
const inDisabled = fs.existsSync(path.join(disabledModsPath, modConfig.fileName));
if (inEnabled || inDisabled) {
validMods.push({
...modConfig,
// Set filePath based on physical location
filePath: inEnabled ? path.join(profileModsPath, modConfig.fileName) : path.join(profileDisabledModsPath, modConfig.fileName),
filePath: inEnabled ? path.join(physicalModsPath, modConfig.fileName) : path.join(disabledModsPath, modConfig.fileName),
enabled: modConfig.enabled !== false // Default true
});
} else {
@@ -105,11 +82,7 @@ async function loadInstalledMods(modsPath) {
async function downloadMod(modInfo) {
try {
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');
const modsPath = await getModsPath();
if (!modInfo.downloadUrl && !modInfo.fileId) {
throw new Error('No download URL or file ID provided');
@@ -118,9 +91,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 || modInfo.curseForgeId}/files/${modInfo.fileId || modInfo.curseForgeFileId}`, {
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId}/files/${modInfo.fileId}`, {
headers: {
'x-api-key': modInfo.apiKey || API_KEY,
'x-api-key': modInfo.apiKey,
'Accept': 'application/json'
}
});
@@ -146,30 +119,35 @@ async function downloadMod(modInfo) {
return new Promise((resolve, reject) => {
writer.on('finish', () => {
// 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
};
// 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
};
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
});
resolve({
success: true,
filePath: filePath,
fileName: fileName,
modInfo: newMod
});
} else {
reject(new Error('No active profile to save mod to'));
}
});
writer.on('error', reject);
});
@@ -195,11 +173,8 @@ async function uninstallMod(modId, modsPath) {
throw new Error('Mod not found in profile');
}
// Use profile paths
const profileModsPath = getProfileModsPath(activeProfile.id);
const disabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
const enabledPath = path.join(profileModsPath, mod.fileName);
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
const enabledPath = path.join(modsPath, mod.fileName);
const disabledPath = path.join(disabledModsPath, mod.fileName);
let fileRemoved = false;
@@ -251,25 +226,31 @@ async function toggleMod(modId, modsPath) {
updatedMods[modIndex] = { ...mod, enabled: newEnabled };
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
// Move file between Profile/Mods and Profile/DisabledMods
const profileModsPath = getProfileModsPath(activeProfile.id);
const disabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
// Manually move the file to reflect the new state
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
if (!fs.existsSync(disabledModsPath)) fs.mkdirSync(disabledModsPath, { recursive: true });
const currentPath = mod.enabled ? path.join(profileModsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName);
const targetDir = newEnabled ? profileModsPath : disabledModsPath;
const currentPath = mod.enabled ? path.join(modsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName);
// Determine target paths
const targetDir = newEnabled ? modsPath : 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(profileModsPath, mod.fileName);
const altPath = mod.enabled ? path.join(disabledModsPath, mod.fileName) : path.join(modsPath, mod.fileName);
if (fs.existsSync(altPath)) fs.renameSync(altPath, targetPath);
}
}
@@ -292,166 +273,35 @@ async function syncModsForCurrentProfile() {
return;
}
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name} (${activeProfile.id})`);
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name}`);
// 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');
const modsPath = await getModsPath();
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
if (!fs.existsSync(profileDisabledModsPath)) {
fs.mkdirSync(profileDisabledModsPath, { recursive: true });
if (!fs.existsSync(disabledModsPath)) {
fs.mkdirSync(disabledModsPath, { recursive: true });
}
// 2. Symlink / Migration Logic
let needsLink = false;
// 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')) : [];
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;
const shouldBeEnabled = modConfig && modConfig.enabled !== false; // Default to true if in list, unless explicitly false
const currentPath = enabledFiles.includes(fileName) ? path.join(profileModsPath, fileName) : path.join(profileDisabledModsPath, fileName);
const targetDir = shouldBeEnabled ? profileModsPath : profileDisabledModsPath;
// 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 targetPath = path.join(targetDir, fileName);
if (path.dirname(currentPath) !== targetDir) {

View File

@@ -1,5 +1,4 @@
const { execSync } = require('child_process');
const fs = require('fs');
function getOS() {
if (process.platform === 'win32') return 'windows';
@@ -218,17 +217,12 @@ 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';
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';
envVars.__GL_SHADER_DISK_CACHE = '1';
envVars.__GL_SHADER_DISK_CACHE_PATH = '/tmp';
}
console.log('GPU environment variables:', envVars);
} else {

View File

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

View File

@@ -1,284 +0,0 @@
# 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

@@ -1,78 +0,0 @@
# 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');
}
}
```

View File

@@ -1,196 +0,0 @@
# 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

188
main.js
View File

@@ -1,36 +1,19 @@
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '.env') });
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const path = require('path');
const fs = require('fs');
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, 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, 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 = process.env.DISCORD_CLIENT_ID;
const DISCORD_CLIENT_ID = '1462244937868513373';
function initDiscordRPC() {
try {
@@ -97,47 +80,19 @@ function toggleDiscordRPC(enabled) {
console.log('Discord RPC disconnected successfully');
} catch (error) {
console.error('Error disconnecting Discord RPC:', error.message);
discordRPC = null;
discordRPC = null; // Force null même en cas d'erreur
}
}
}
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: true,
resizable: false,
alwaysOnTop: false,
backgroundColor: '#090909',
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
@@ -149,10 +104,6 @@ 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...');
@@ -187,21 +138,10 @@ function createWindow() {
if (input.key === 'F12') {
event.preventDefault();
}
if (input.key === 'F5') {
event.preventDefault();
}
// Close application shortcuts
const isMac = process.platform === 'darwin';
const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') ||
(!isMac && input.control && input.key.toLowerCase() === 'q') ||
(!isMac && input.alt && input.key === 'F4');
if (quitShortcut) {
app.quit();
}
});
if (input.key === 'F5') {
event.preventDefault();
}
});
mainWindow.webContents.on('context-menu', (e) => {
@@ -212,9 +152,7 @@ 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);
@@ -239,7 +177,7 @@ app.whenReady().then(async () => {
// Initialize Profile Manager (runs migration if needed)
profileManager.init();
createSplashScreen();
createWindow();
setTimeout(async () => {
let timeoutReached = false;
@@ -345,14 +283,15 @@ app.on('before-quit', () => {
cleanupDiscordRPC();
});
app.on('window-all-closed', () => {
console.log('=== LAUNCHER CLOSING ===');
cleanupDiscordRPC();
app.quit();
});
app.on('window-all-closed', () => {
console.log('=== LAUNCHER CLOSING ===');
cleanupDiscordRPC();
if (process.platform !== 'darwin') {
app.quit();
}
});
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
try {
@@ -369,20 +308,9 @@ 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;
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference);
return result;
} catch (error) {
console.error('Launch error:', error);
const errorMessage = error.message || error.toString();
@@ -399,11 +327,6 @@ 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 = {
@@ -419,21 +342,11 @@ 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 };
}
});
@@ -497,21 +410,11 @@ ipcMain.handle('save-language', (event, language) => {
return { success: true };
});
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 () => {
ipcMain.handle('load-language', () => {
return loadLanguage();
});
ipcMain.handle('select-install-path', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'],
title: 'Select Installation Folder'
@@ -723,10 +626,6 @@ 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');
@@ -836,10 +735,11 @@ ipcMain.handle('open-download-page', async () => {
try {
await shell.openExternal(updateManager.getDownloadUrl());
setTimeout(() => {
app.quit();
}, 1000);
setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
}, 1000);
return { success: true };
} catch (error) {
@@ -881,10 +781,11 @@ ipcMain.handle('get-detected-gpu', () => {
return global.detectedGpu;
});
ipcMain.handle('window-close', () => {
app.quit();
});
ipcMain.handle('window-close', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
});
ipcMain.handle('window-minimize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
@@ -892,21 +793,6 @@ ipcMain.handle('window-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();
});

10011
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,150 +1,103 @@
{
"name": "hytale-f2p-launcher",
"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",
"scripts": {
"start": "electron .",
"dev": "electron . --dev",
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:linux": "electron-builder --linux",
"build:mac": "electron-builder --mac",
"build:all": "electron-builder --win --linux --mac"
},
"keywords": [
"hytale",
"launcher",
"game",
"client",
"cross-platform",
"electron",
"auto-update",
"mod-manager",
"chat"
],
"maintainers": [
{
"name": "Terromur",
"url": "https://github.com/Terromur"
},
{
"name": "Fari Gading",
"email": "fazrigading@gmail.com",
"url": "https://github.com/fazrigading"
}
],
"author": {
"name": "AMIAY",
"email": "support@amiay.dev"
},
"license": "MIT",
"devDependencies": {
"electron": "^40.0.0",
"electron-builder": "^26.4.0"
},
"dependencies": {
"adm-zip": "^0.5.10",
"axios": "^1.6.0",
"discord-rpc": "^4.0.1",
"dotenv": "^17.2.3",
"electron-updater": "^6.7.3",
"tar": "^6.2.1",
"uuid": "^9.0.1"
},
"overrides": {
"tar": "$tar"
},
"build": {
"appId": "com.hytalef2p.launcher",
"productName": "Hytale F2P Launcher",
"artifactName": "${name}_${version}_${arch}.${ext}",
"directories": {
"output": "dist"
},
"files": [
"main.js",
"preload.js",
"backend/**/*",
"GUI/**/*",
"package.json",
".env"
],
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64",
"arm64"
]
}
],
"icon": "icon.ico"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64",
"arm64"
]
},
{
"target": "deb",
"arch": [
"x64",
"arm64"
]
},
{
"target": "rpm",
"arch": [
"x64",
"arm64"
]
},
{
"target": "pacman",
"arch": [
"x64",
"arm64"
]
}
],
"icon": "build/icon.png",
"category": "Game"
},
"mac": {
"target": [
{
"target": "dmg",
"arch": [
"universal"
]
},
{
"target": "zip",
"arch": [
"universal"
]
}
],
"icon": "build/icon.icns",
"category": "public.app-category.games"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
},
"publish": {
"provider": "github",
"owner": "amiayweb",
"repo": "Hytale-F2P"
}
}
}
{
"name": "hytale-f2p-launcher",
"version": "2.0.2b",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
"homepage": "https://github.com/amiayweb/Hytale-F2P",
"main": "main.js",
"scripts": {
"start": "electron .",
"dev": "electron . --dev",
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:linux": "electron-builder --linux",
"build:mac": "electron-builder --mac",
"build:all": "electron-builder --win --linux --mac"
},
"keywords": [
"hytale",
"launcher",
"game",
"client",
"cross-platform",
"electron",
"auto-update",
"mod-manager",
"chat"
],
"maintainers": [
{
"name": "Terromur",
"url": "https://github.com/Terromur"
},
{
"name": "Fari Gading",
"email": "fazrigading@gmail.com",
"url": "https://github.com/fazrigading"
}
],
"author": {
"name": "AMIAY",
"email": "support@amiay.dev"
},
"license": "MIT",
"devDependencies": {
"electron": "^40.0.0",
"electron-builder": "^26.4.0"
},
"dependencies": {
"adm-zip": "^0.5.10",
"axios": "^1.6.0",
"discord-rpc": "^4.0.1",
"tar": "^6.2.1",
"uuid": "^9.0.1"
},
"overrides": {
"tar": "$tar"
},
"build": {
"appId": "com.hytalef2p.launcher",
"productName": "Hytale F2P Launcher",
"artifactName": "${name}_${version}_${arch}.${ext}",
"directories": {
"output": "dist"
},
"files": [
"main.js",
"preload.js",
"backend/**/*",
"GUI/**/*",
"package.json"
],
"win": {
"target": [
{ "target": "nsis", "arch": ["x64", "arm64"] },
{ "target": "portable", "arch": ["x64"] }
],
"icon": "icon.ico"
},
"linux": {
"target": [
{ "target": "AppImage", "arch": ["x64", "arm64"] },
{ "target": "deb", "arch": ["x64", "arm64"] },
{ "target": "rpm", "arch": ["x64", "arm64"] },
{ "target": "pacman", "arch": ["x64", "arm64"] }
],
"icon": "build/icon.png",
"category": "Game"
},
"mac": {
"target": [
{ "target": "dmg", "arch": ["universal"] },
{ "target": "zip", "arch": ["universal"] }
],
"icon": "build/icon.icns",
"category": "public.app-category.games"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
}

View File

@@ -5,8 +5,6 @@ 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),
@@ -21,8 +19,6 @@ 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'),
@@ -34,7 +30,6 @@ 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),
@@ -49,12 +44,6 @@ 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'),
@@ -62,7 +51,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
onUpdatePopup: (callback) => {
ipcRenderer.on('show-update-popup', (event, data) => callback(data));
},
getGpuInfo: () => ipcRenderer.invoke('get-gpu-info'),
saveGpuPreference: (gpuPreference) => ipcRenderer.invoke('save-gpu-preference', gpuPreference),
loadGpuPreference: () => ipcRenderer.invoke('load-gpu-preference'),