Compare commits

..

1 Commits

Author SHA1 Message Date
Fazri Gading
ae375f9b6e Release Stable Build v2.1.1 (#197)
* fix: resolve cross-platform EPERM permissions errors

modManager.js:
- Switch from hardcoded 'junction' to dynamic symlink type based on OS (fixing Linux EPERM).
- Add retry logic for directory removal to handle file locking race conditions.
- Improve broken symlink detection during profile sync.

gameManager.js:
- Implement retry loop (3 attempts) for game directory removal in updateGameFiles to prevent EBUSY/EPERM errors on Windows.

paths.js:
- Prevent fs.mkdirSync failure in getModsPath by pre-checking for broken symbolic links.

* fix: missing pacman builds

* Update README.md Windows Prequisites for ARM64 builds

* fix: remove broken symlink after detected

* fix: add pathexists for paths.js to check symlink

* fix: isbrokenlink should be true to remove the symlink

* add arch package .pkg.tar.zst for release

* fix: release workflow for build-arch and build-linux

* build-arch job now only build arch .pkg.tar.zst package instead of the whole generic linux.
* build-linux job now exclude .pacman package since its deprecated and should not be used.

* fix: removes pacman build as it replaced by tar.zst and adds build:arch shortcut for pkgbuild

* aur: add proper VCS (-git) PKGBUILD

created clean VCS-based PKGBUILD following arch packaging conventions.

this explicitly marked as a rolling (-git) build and derives its version dynamically from git tags and commit history via pkgver(). previous hybrid approach has been changed.

key changes:
- use -git suffix to clearly indicate rolling source builds
- set pkgver=0 and compute the actual version via pkgver()
- build only a directory layout using electron-builder (--dir)
- avoid generating AppImage, deb, rpm, or pacman installers
- align build and package steps with Arch packaging guidelines

note: this PKGBUILD is intended for development and AUR use only and is not suitable for binary redistribution or release artifacts.

* ci: add fixed-version PKGBUILD for Arch Linux releases

this PKGBUILD intended for CI and GitHub release artifacts. targets tagged releases only and uses a fixed pkgver that matches the corresponding git tag. all of the VCS logic has been removed to PKGBUILD-git to ensure reproducible builds and stable versioning suitable for binary distribution.

the build process relies on electron-builder directory output (--dir) and packages only the unpacked application into a standard Arch Linux package (.pkg.tar.zst). other distro format are excluded from this path and handled separately.

this change establishes a clear separation between:
- rolling AUR development builds (-git)
- CI-generated, versioned Arch Linux release packages

the result is predictable artifact naming, correct version alignment, and Arch-compliant packaging for downstream users.

* Update README.md

adds information for Arch build

* Update README.md

BUILD.md location was changed and now this link is poiting to nothing

* Update PKGBUILD

* Update PKGBUILD-git

* chore: fix ubuntu/debian part in README.md

* Polish language support (#195)

* add hardware specification in support_request.yml

* Add logs text field in bug_report.yml

* chore: add changelog in README.md

* fix screenshot input in feature_request.yml

* add hardware spec input in bug_report.yml

---------

Co-authored-by: TalesAmaral <57869141+TalesAmaral@users.noreply.github.com>
Co-authored-by: walti0 <95646872+walti0@users.noreply.github.com>
2026-01-27 03:44:24 +08:00
21 changed files with 693 additions and 1855 deletions

View File

@@ -1,22 +1,8 @@
name: Support Request
description: Request help or support
title: "[SUPPORT] <ADD YOUR TITLE HERE>"
title: "[SUPPORT] "
labels: ["support"]
body:
- type: dropdown
id: acknowledge
attributes:
label: Checklist
options:
- label: I have read the README.md before asking Support Request.
required: true
- label: I have read the TROUBLESHOOTING.md before asking Support Request.
required: true
- label: I have added title before submitting this Support Request.
required: true
- label: I acknowledge that my Support Request will not be responded as quick as in Discord Open-A-Ticket, I prefer this way.
required: true
- type: markdown
attributes:
value: |
@@ -38,7 +24,7 @@ body:
attributes:
label: Context
description: Provide any relevant context or background information.
placeholder: "I've tried these steps, but got..."
placeholder: "I've tried..., but got..."
validations:
required: true
@@ -51,17 +37,12 @@ body:
validations:
required: true
- type: dropdown
- type: input
id: version
attributes:
label: Version
description: What version are you using?
options:
- v2.1.2
- v2.1.1
- v2.1.0
- v2.0.11
- v2.0.2
placeholder: "e.g. v2.0.11 stable/pre-release"
validations:
required: true
@@ -71,12 +52,13 @@ body:
label: Platform
description: What platform are you using?
options:
- Windows 11 x64
- Windows 10 x64
- macOS ARM64 (Apple Silicon)
- Linux x64 Ubuntu/Debian-based
- Linux x64 Fedora/RHEL-based
- Linux x64 Arch-based
- Windows 10
- Windows 11
- macOS (Apple Silicon)
- macOS (Intel)
- Linux Ubuntu/Debian-based
- Linux Fedora/RHEL-based
- Linux Arch-based
validations:
required: true

View File

@@ -882,7 +882,7 @@
<script src="js/i18n.js"></script>
<script type="module" src="js/settings.js"></script>
<script type="module" src="js/update.js"></script>
<!-- updater.js disabled - using update.js instead which has skip button and macOS handling -->
<script src="js/updater.js"></script>
</body>

View File

@@ -4,9 +4,6 @@ const i18n = (() => {
let translations = {};
const availableLanguages = [
{ code: 'en', name: 'English' },
{ code: 'fr', name: 'Français' },
{ code: 'de', name: 'Deutsch' },
{ code: 'sv', name: 'Svenska' },
{ code: 'es-ES', name: 'Español (España)' },
{ code: 'pt-BR', name: 'Portuguese (Brazil)' },
{ code: 'tr-TR', name: 'Turkish (Turkey)' },

View File

@@ -72,11 +72,8 @@ export async function installGame() {
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true);
// Sync player name to both launcher and settings inputs
const playerNameInput = document.getElementById('playerName');
if (playerNameInput) playerNameInput.value = playerName;
const settingsPlayerName = document.getElementById('settingsPlayerName');
if (settingsPlayerName) settingsPlayerName.value = playerName;
resetInstallButton();
}, 2000);
}
@@ -128,11 +125,8 @@ function simulateInstallation(playerName) {
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true);
// Sync player name to both launcher and settings inputs
const playerNameInput = document.getElementById('playerName');
if (playerNameInput) playerNameInput.value = playerName;
const settingsPlayerName = document.getElementById('settingsPlayerName');
if (settingsPlayerName) settingsPlayerName.value = playerName;
resetInstallButton();
}, 2000);
}
@@ -252,3 +246,9 @@ document.addEventListener('DOMContentLoaded', async () => {
setupInstallation();
await checkGameStatusAndShowInterface();
});
window.browseInstallPath = browseInstallPath;
document.addEventListener('DOMContentLoaded', async () => {
setupInstallation();
await checkGameStatusAndShowInterface();
});

View File

@@ -6,12 +6,12 @@ class ClientUpdateManager {
}
init() {
console.log('🔧 ClientUpdateManager initializing...');
window.electronAPI.onUpdatePopup((updateInfo) => {
this.showUpdatePopup(updateInfo);
});
// Listen for electron-updater events from main.js
// This is the primary update trigger - main.js checks for updates on startup
// Listen for electron-updater events
window.electronAPI.onUpdateAvailable((updateInfo) => {
console.log('📥 update-available event received:', updateInfo);
this.showUpdatePopup(updateInfo);
});
@@ -20,30 +20,18 @@ class ClientUpdateManager {
});
window.electronAPI.onUpdateDownloaded((updateInfo) => {
console.log('📦 update-downloaded event received:', updateInfo);
this.showUpdateDownloaded(updateInfo);
});
window.electronAPI.onUpdateError((errorInfo) => {
console.log('❌ update-error event received:', errorInfo);
this.handleUpdateError(errorInfo);
});
console.log('✅ ClientUpdateManager initialized');
// Note: Don't call checkForUpdatesOnDemand() here - main.js already checks
// for updates after 3 seconds and sends 'update-available' event.
// Calling it here would cause duplicate popups.
this.checkForUpdatesOnDemand();
}
showUpdatePopup(updateInfo) {
console.log('🔔 showUpdatePopup called, updatePopupVisible:', this.updatePopupVisible);
// Check if popup already exists in DOM (extra safety)
if (this.updatePopupVisible || document.getElementById('update-popup-overlay')) {
console.log('⚠️ Update popup already visible, skipping');
return;
}
if (this.updatePopupVisible) return;
this.updatePopupVisible = true;
@@ -104,10 +92,7 @@ class ClientUpdateManager {
</div>
<div class="update-popup-footer">
<span id="update-footer-text">Downloading update...</span>
<button id="update-skip-btn" class="update-skip-btn" style="display: none; margin-top: 0.5rem; background: transparent; border: 1px solid rgba(255,255,255,0.2); color: #9ca3af; padding: 0.5rem 1rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.75rem;">
Skip for now (not recommended)
</button>
This popup cannot be closed until you update the launcher
</div>
</div>
</div>
@@ -134,37 +119,10 @@ class ClientUpdateManager {
try {
await window.electronAPI.quitAndInstallUpdate();
// If we're still here after 5 seconds, the install probably failed
setTimeout(() => {
console.log('⚠️ Install may have failed - showing skip option');
installBtn.disabled = false;
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Try Again';
// Show skip button
const skipBtn = document.getElementById('update-skip-btn');
const footerText = document.getElementById('update-footer-text');
if (skipBtn) {
skipBtn.style.display = 'inline-block';
if (footerText) {
footerText.textContent = 'Install not working? Skip for now:';
}
}
}, 5000);
} catch (error) {
console.error('❌ Error installing update:', error);
installBtn.disabled = false;
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Install & Restart';
// Show skip button on error
const skipBtn = document.getElementById('update-skip-btn');
const footerText = document.getElementById('update-footer-text');
if (skipBtn) {
skipBtn.style.display = 'inline-block';
if (footerText) {
footerText.textContent = 'Install failed. Skip for now:';
}
}
}
});
}
@@ -180,14 +138,9 @@ class ClientUpdateManager {
try {
await window.electronAPI.openDownloadPage();
console.log('✅ Download page opened');
console.log('✅ Download page opened, launcher will close...');
downloadBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Opened in browser';
// Close the popup after opening download page
setTimeout(() => {
this.closeUpdatePopup();
}, 1500);
downloadBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Launcher closing...';
} catch (error) {
console.error('❌ Error opening download page:', error);
@@ -208,39 +161,9 @@ class ClientUpdateManager {
});
}
// Show skip button after 30 seconds as fallback (in case update is stuck)
setTimeout(() => {
const skipBtn = document.getElementById('update-skip-btn');
const footerText = document.getElementById('update-footer-text');
if (skipBtn) {
skipBtn.style.display = 'inline-block';
if (footerText) {
footerText.textContent = 'Update taking too long?';
}
}
}, 30000);
const skipBtn = document.getElementById('update-skip-btn');
if (skipBtn) {
skipBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.closeUpdatePopup();
});
}
console.log('🔔 Update popup displayed with new style');
}
closeUpdatePopup() {
const overlay = document.getElementById('update-popup-overlay');
if (overlay) {
overlay.remove();
}
this.updatePopupVisible = false;
this.unblockInterface();
}
updateDownloadProgress(progress) {
const progressBar = document.getElementById('update-progress-bar');
const progressPercent = document.getElementById('update-progress-percent');
@@ -274,86 +197,25 @@ class ClientUpdateManager {
const statusText = document.getElementById('update-status-text');
const progressContainer = document.getElementById('update-progress-container');
const buttonsContainer = document.getElementById('update-buttons-container');
const installBtn = document.getElementById('update-install-btn');
const downloadBtn = document.getElementById('update-download-btn');
const skipBtn = document.getElementById('update-skip-btn');
const footerText = document.getElementById('update-footer-text');
const popupContainer = document.querySelector('.update-popup-container');
// Remove breathing/pulse animation when download is complete
if (popupContainer) {
popupContainer.classList.remove('update-popup-pulse');
if (statusText) {
statusText.textContent = 'Update downloaded! Ready to install.';
}
if (progressContainer) {
progressContainer.style.display = 'none';
}
// Use platform info from main process if available, fallback to browser detection
const autoInstallSupported = updateInfo.autoInstallSupported !== undefined
? updateInfo.autoInstallSupported
: navigator.platform.toUpperCase().indexOf('MAC') < 0;
if (!autoInstallSupported) {
// macOS: Show manual download as primary since auto-update doesn't work
if (statusText) {
statusText.textContent = 'Update downloaded but auto-install may not work on macOS.';
}
if (installBtn) {
// Still show install button but as secondary option
installBtn.classList.add('update-download-btn-secondary');
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Try Install & Restart';
}
if (downloadBtn) {
// Make manual download primary
downloadBtn.classList.remove('update-download-btn-secondary');
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Manually (Recommended)';
}
if (footerText) {
footerText.textContent = 'Auto-install often fails on macOS:';
}
} else {
// Windows/Linux: Auto-install should work
if (statusText) {
statusText.textContent = 'Update downloaded! Ready to install.';
}
if (footerText) {
footerText.textContent = 'Click to install the update:';
}
}
if (buttonsContainer) {
buttonsContainer.style.display = 'block';
}
// Always show skip button in downloaded state
if (skipBtn) {
skipBtn.style.display = 'inline-block';
console.log('✅ Skip button made visible');
} else {
console.error('❌ Skip button not found in DOM!');
}
console.log('✅ Update downloaded, ready to install. autoInstallSupported:', autoInstallSupported);
console.log('✅ Update downloaded, ready to install');
}
handleUpdateError(errorInfo) {
console.error('Update error:', errorInfo);
// Show skip button immediately on any error
const skipBtn = document.getElementById('update-skip-btn');
const footerText = document.getElementById('update-footer-text');
if (skipBtn) {
skipBtn.style.display = 'inline-block';
if (footerText) {
footerText.textContent = 'Update failed. You can skip for now.';
}
}
// If manual download is required, update the UI (this will handle status text)
if (errorInfo.requiresManualDownload) {
this.showManualDownloadRequired(errorInfo);
@@ -427,16 +289,6 @@ class ClientUpdateManager {
buttonsContainer.style.display = 'block';
}
// Show skip button for manual download errors
const skipBtn = document.getElementById('update-skip-btn');
const footerText = document.getElementById('update-footer-text');
if (skipBtn) {
skipBtn.style.display = 'inline-block';
if (footerText) {
footerText.textContent = 'Or continue without updating:';
}
}
console.log('⚠️ Manual download required due to update error');
}
@@ -448,35 +300,13 @@ class ClientUpdateManager {
document.body.classList.add('no-select');
// Store bound functions so we can remove them later
this._boundBlockKeyEvents = this.blockKeyEvents.bind(this);
this._boundBlockContextMenu = this.blockContextMenu.bind(this);
document.addEventListener('keydown', this.blockKeyEvents.bind(this), true);
document.addEventListener('keydown', this._boundBlockKeyEvents, true);
document.addEventListener('contextmenu', this._boundBlockContextMenu, true);
document.addEventListener('contextmenu', this.blockContextMenu.bind(this), true);
console.log('🚫 Interface blocked for update');
}
unblockInterface() {
const mainContent = document.querySelector('.flex.w-full.h-screen');
if (mainContent) {
mainContent.classList.remove('interface-blocked');
}
document.body.classList.remove('no-select');
// Remove event listeners
if (this._boundBlockKeyEvents) {
document.removeEventListener('keydown', this._boundBlockKeyEvents, true);
}
if (this._boundBlockContextMenu) {
document.removeEventListener('contextmenu', this._boundBlockContextMenu, true);
}
console.log('✅ Interface unblocked');
}
blockKeyEvents(event) {
if (event.target.closest('#update-popup-overlay')) {
if ((event.key === 'Enter' || event.key === ' ') &&

View File

@@ -1,283 +0,0 @@
{
"nav": {
"play": "Spielen",
"mods": "Mods",
"news": "Neuigkeiten",
"chat": "Spieler-Chat",
"settings": "Einstellungen"
},
"header": {
"playersLabel": "Spieler:",
"manageProfiles": "Profile verwalten",
"defaultProfile": "Standard"
},
"install": {
"title": "KOSTENLOSER LAUNCHER",
"playerName": "Spielername",
"playerNamePlaceholder": "Namen eingeben",
"gameBranch": "Spielversion",
"releaseVersion": "Release (Stabil)",
"preReleaseVersion": "Pre-Release (Experimentell)",
"customInstallation": "Benutzerdefinierte Installation",
"installationFolder": "Installationsordner",
"pathPlaceholder": "Standardspeicherort",
"browse": "Durchsuchen",
"installButton": "HYTALE INSTALLIEREN",
"installing": "INSTALLIERE..."
},
"play": {
"ready": "BEREIT ZUM SPIELEN",
"subtitle": "Starte Hytale und beginne das Abenteuer",
"playButton": "HYTALE SPIELEN",
"latestNews": "NEUESTE NACHRICHTEN",
"viewAll": "ALLE ANZEIGEN",
"checking": "ÜBERPRÜFE...",
"play": "SPIELEN"
},
"mods": {
"searchPlaceholder": "Mods suchen...",
"myMods": "MEINE MODS",
"previous": "ZURÜCK",
"next": "WEITER",
"page": "Seite",
"of": "von",
"modalTitle": "MEINE MODS",
"noModsFound": "Keine Mods gefunden",
"noModsFoundDesc": "Versuche deine Suche anzupassen",
"noModsInstalled": "Keine Mods installiert",
"noModsInstalledDesc": "Füge Mods von CurseForge hinzu oder importiere lokale Dateien",
"view": "ANZEIGEN",
"install": "INSTALLIEREN",
"installed": "INSTALLIERT",
"enable": "AKTIVIEREN",
"disable": "DEAKTIVIEREN",
"active": "AKTIV",
"disabled": "DEAKTIVIERT",
"delete": "Mod löschen",
"noDescription": "Keine Beschreibung verfügbar",
"confirmDelete": "Möchtest du \"{name}\" wirklich löschen?",
"confirmDeleteDesc": "Diese Aktion kann nicht rückgängig gemacht werden.",
"confirmDeletion": "Löschung bestätigen",
"apiKeyRequired": "API-Schlüssel erforderlich",
"apiKeyRequiredDesc": "CurseForge API-Schlüssel wird benötigt, um Mods zu durchsuchen"
},
"news": {
"title": "ALLE NACHRICHTEN",
"readMore": "Mehr lesen"
},
"chat": {
"title": "SPIELER-CHAT",
"pickColor": "Farbe",
"inputPlaceholder": "Nachricht eingeben...",
"send": "Senden",
"online": "online",
"charCounter": "{current}/{max}",
"secureChat": "Sicherer Chat - Links werden zensiert",
"joinChat": "Chat beitreten",
"chooseUsername": "Wähle einen Benutzernamen, um dem Spieler-Chat beizutreten",
"username": "Benutzername",
"usernamePlaceholder": "Benutzernamen eingeben...",
"usernameHint": "3-20 Zeichen, nur Buchstaben, Zahlen, - und _",
"joinButton": "Chat beitreten",
"colorModal": {
"title": "Benutzernamenfarbe anpassen",
"chooseSolid": "Wähle eine einfarbige Farbe:",
"customColor": "Benutzerdefinierte Farbe:",
"preview": "Vorschau:",
"previewUsername": "Benutzername",
"apply": "Farbe anwenden"
}
},
"settings": {
"title": "EINSTELLUNGEN",
"java": "Java Runtime",
"useCustomJava": "Benutzerdefinierten Java-Pfad verwenden",
"javaDescription": "Ersetze die mitgelieferte Java-Installation durch deine eigene",
"javaPath": "Java-Ausführungsdatei-Pfad",
"javaPathPlaceholder": "Java-Pfad auswählen...",
"javaBrowse": "Durchsuchen",
"javaHint": "Wähle den Java-Installationsordner (unterstützt Windows, Mac, Linux)",
"discord": "Discord-Integration",
"enableRPC": "Discord Rich Presence aktivieren",
"discordDescription": "Zeige deine Launcher-Aktivität auf Discord",
"game": "Spieloptionen",
"playerName": "Spielername",
"playerNamePlaceholder": "Spielernamen eingeben",
"playerNameHint": "Dieser Name wird im Spiel verwendet (1-16 Zeichen)",
"openGameLocation": "Spielordner öffnen",
"openGameLocationDesc": "Öffne den Spielinstallationsordner",
"account": "Spieler-UUID-Verwaltung",
"currentUUID": "Aktuelle UUID",
"uuidPlaceholder": "UUID wird geladen...",
"copyUUID": "UUID kopieren",
"regenerateUUID": "UUID neu generieren",
"uuidHint": "Deine eindeutige Spielerkennung für diesen Benutzernamen",
"manageUUIDs": "Alle UUIDs verwalten",
"manageUUIDsDesc": "Alle Spieler-UUIDs anzeigen und verwalten",
"language": "Sprache",
"selectLanguage": "Sprache auswählen",
"repairGame": "Spiel reparieren",
"reinstallGame": "Spieldateien neu installieren (behält Daten)",
"gpuPreference": "GPU-Präferenz",
"gpuHint": "Wähle deine bevorzugte GPU (Linux: betrifft DRI_PRIME)",
"gpuAuto": "Auto",
"gpuIntegrated": "Integriert",
"gpuDedicated": "Dediziert",
"logs": "SYSTEMPROTOKOLLE",
"logsCopy": "Kopieren",
"logsRefresh": "Aktualisieren",
"logsFolder": "Ordner öffnen",
"logsLoading": "Protokolle werden geladen...",
"closeLauncher": "Launcher-Verhalten",
"closeOnStart": "Launcher beim Spielstart schließen",
"closeOnStartDescription": "Schließe den Launcher automatisch, nachdem Hytale gestartet wurde",
"hwAccel": "Hardware-Beschleunigung",
"hwAccelDescription": "Hardware-Beschleunigung für den Launcher aktivieren",
"gameBranch": "Spiel-Branch",
"branchRelease": "Release",
"branchPreRelease": "Pre-Release",
"branchHint": "Wechsel zwischen stabiler Release- und experimenteller Pre-Release-Version",
"branchWarning": "Das Ändern des Branches lädt eine andere Spielversion herunter und installiert sie",
"branchSwitching": "Wechsle zu {branch}...",
"branchSwitched": "Erfolgreich zu {branch} gewechselt!",
"installRequired": "Installation erforderlich",
"branchInstallConfirm": "Das Spiel wird für den {branch}-Branch installiert. Fortfahren?"
},
"uuid": {
"modalTitle": "UUID-Verwaltung",
"currentUserUUID": "Aktuelle Benutzer-UUID",
"allPlayerUUIDs": "Alle Spieler-UUIDs",
"generateNew": "Neue UUID generieren",
"loadingUUIDs": "UUIDs werden geladen...",
"setCustomUUID": "Benutzerdefinierte UUID festlegen",
"customPlaceholder": "Benutzerdefinierte UUID eingeben (Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "UUID festlegen",
"warning": "Warnung: Das Festlegen einer benutzerdefinierten UUID ändert deine aktuelle Spieleridentität",
"copyTooltip": "UUID kopieren",
"regenerateTooltip": "Neue UUID generieren"
},
"profiles": {
"modalTitle": "Profile verwalten",
"newProfilePlaceholder": "Neuer Profilname",
"createProfile": "Profil erstellen"
},
"discord": {
"notificationText": "Tritt unserer Discord-Community bei!",
"joinButton": "Discord beitreten"
},
"common": {
"confirm": "Bestätigen",
"cancel": "Abbrechen",
"save": "Speichern",
"close": "Schließen",
"delete": "Löschen",
"edit": "Bearbeiten",
"loading": "Lädt...",
"apply": "Anwenden",
"install": "Installieren"
},
"notifications": {
"gameDataNotFound": "Fehler: Spieldaten nicht gefunden",
"gameUpdatedSuccess": "Spiel erfolgreich aktualisiert! 🎉",
"updateFailed": "Update fehlgeschlagen: {error}",
"updateError": "Update-Fehler: {error}",
"discordEnabled": "Discord Rich Presence aktiviert",
"discordDisabled": "Discord Rich Presence deaktiviert",
"discordSaveFailed": "Discord-Einstellung konnte nicht gespeichert werden",
"playerNameRequired": "Bitte gib einen gültigen Spielernamen ein",
"playerNameSaved": "Spielername erfolgreich gespeichert",
"playerNameSaveFailed": "Spielername konnte nicht gespeichert werden",
"uuidCopied": "UUID in die Zwischenablage kopiert!",
"uuidCopyFailed": "UUID konnte nicht kopiert werden",
"uuidRegenNotAvailable": "UUID-Neugenerierung nicht verfügbar",
"uuidRegenFailed": "UUID konnte nicht neu generiert werden",
"uuidGenerated": "Neue UUID erfolgreich generiert!",
"uuidGeneratedShort": "Neue UUID generiert!",
"uuidGenerateFailed": "Neue UUID konnte nicht generiert werden",
"uuidRequired": "Bitte gib eine UUID ein",
"uuidInvalidFormat": "Ungültiges UUID-Format",
"uuidSetFailed": "Benutzerdefinierte UUID konnte nicht festgelegt werden",
"uuidSetSuccess": "Benutzerdefinierte UUID erfolgreich festgelegt!",
"uuidDeleteFailed": "UUID konnte nicht gelöscht werden",
"uuidDeleteSuccess": "UUID erfolgreich gelöscht!",
"modsDownloading": "{name} wird heruntergeladen...",
"modsTogglingMod": "Mod wird umgeschaltet...",
"modsDeletingMod": "Mod wird gelöscht...",
"modsLoadingMods": "Mods von CurseForge werden geladen...",
"modsInstalledSuccess": "{name} erfolgreich installiert! 🎉",
"modsDeletedSuccess": "{name} erfolgreich gelöscht",
"modsDownloadFailed": "Mod konnte nicht heruntergeladen werden: {error}",
"modsToggleFailed": "Mod konnte nicht umgeschaltet werden: {error}",
"modsDeleteFailed": "Mod konnte nicht gelöscht werden: {error}",
"modsModNotFound": "Mod-Informationen nicht gefunden",
"hwAccelSaved": "Hardware-Beschleunigungseinstellung gespeichert",
"hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden",
"javaPathCopied": "Java-Pfad in die Zwischenablage kopiert!",
"javaPathCopyFailed": "Java-Pfad konnte nicht kopiert werden",
"javaPathSaved": "Java-Pfad erfolgreich gespeichert!",
"javaPathSaveFailed": "Java-Pfad konnte nicht gespeichert werden",
"javaPathInvalid": "Ungültiger Java-Pfad",
"javaPathReset": "Java-Pfad auf Standardwerte zurückgesetzt",
"gameLocationError": "Spielordner konnte nicht geöffnet werden",
"launcherRestartRequired": "Launcher-Neustart erforderlich, um Änderungen anzuwenden",
"gameRepairConfirm": "Möchtest du das Spiel wirklich reparieren? Dies wird alle Spieldateien neu installieren.",
"gameRepairInProgress": "Spiel wird repariert...",
"gameRepairSuccess": "Spiel erfolgreich repariert!",
"gameRepairFailed": "Spielreparatur fehlgeschlagen: {error}",
"invalidUsername": "Ungültiger Benutzername",
"usernameInUse": "Benutzername bereits vergeben",
"chatJoinSuccess": "Du bist dem Chat beigetreten!",
"chatJoinFailed": "Chat-Beitritt fehlgeschlagen",
"messageTooLong": "Nachricht zu lang",
"messageSent": "Nachricht gesendet",
"messageSendFailed": "Nachricht konnte nicht gesendet werden",
"colorUpdated": "Farbe aktualisiert!",
"colorUpdateFailed": "Farbe konnte nicht aktualisiert werden",
"profileCreated": "Profil erfolgreich erstellt!",
"profileCreateFailed": "Profil konnte nicht erstellt werden",
"profileDeleted": "Profil gelöscht",
"profileDeleteFailed": "Profil konnte nicht gelöscht werden",
"profileSwitched": "Profil gewechselt zu: {name}",
"profileSwitchFailed": "Profilwechsel fehlgeschlagen",
"invalidProfileName": "Ungültiger Profilname",
"profileNameExists": "Ein Profil mit diesem Namen existiert bereits",
"noInternet": "Keine Internetverbindung",
"checkInternetConnection": "Überprüfe deine Internetverbindung",
"serverError": "Serverfehler. Bitte versuche es später erneut.",
"unknownError": "Ein unbekannter Fehler ist aufgetreten"
},
"confirm": {
"defaultTitle": "Aktion bestätigen",
"regenerateUuidTitle": "Neue UUID generieren",
"regenerateUuidMessage": "Möchtest du wirklich eine neue UUID generieren? Dies ändert deine Spieleridentität.",
"regenerateUuidButton": "Generieren",
"setCustomUuidTitle": "Benutzerdefinierte UUID festlegen",
"setCustomUuidMessage": "Möchtest du wirklich diese benutzerdefinierte UUID festlegen? Dies ändert deine Spieleridentität.",
"setCustomUuidButton": "UUID festlegen",
"deleteUuidTitle": "UUID löschen",
"deleteUuidMessage": "Möchtest du wirklich die UUID für \"{username}\" löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"deleteUuidButton": "Löschen",
"uninstallGameTitle": "Spiel deinstallieren",
"uninstallGameMessage": "Möchtest du Hytale wirklich deinstallieren? Alle Spieldateien werden gelöscht.",
"uninstallGameButton": "Deinstallieren"
},
"progress": {
"initializing": "Initialisiere...",
"downloading": "Lädt herunter...",
"installing": "Installiere...",
"extracting": "Entpacke...",
"verifying": "Überprüfe...",
"switchingProfile": "Profil wird gewechselt...",
"profileSwitched": "Profil gewechselt!",
"startingGame": "Spiel wird gestartet...",
"launching": "STARTET...",
"uninstallingGame": "Spiel wird deinstalliert...",
"gameUninstalled": "Spiel erfolgreich deinstalliert!",
"uninstallFailed": "Deinstallation fehlgeschlagen: {error}",
"startingUpdate": "Obligatorisches Spiel-Update wird gestartet...",
"installationComplete": "Installation erfolgreich abgeschlossen!",
"installationFailed": "Installation fehlgeschlagen: {error}",
"installingGameFiles": "Spieldateien werden installiert...",
"installComplete": "Installation abgeschlossen!"
}
}

View File

@@ -1,235 +0,0 @@
{
"nav": {
"play": "Jouer",
"mods": "Mods",
"news": "Actualités",
"chat": "Chat Joueurs",
"settings": "Paramètres"
},
"header": {
"playersLabel": "Joueurs:",
"manageProfiles": "Gérer les Profils",
"defaultProfile": "Par défaut"
},
"install": {
"title": "LAUNCHER GRATUIT",
"playerName": "Nom du Joueur",
"playerNamePlaceholder": "Entrez votre nom",
"gameBranch": "Version du Jeu",
"releaseVersion": "Release (Stable)",
"preReleaseVersion": "Pré-Release (Expérimental)",
"customInstallation": "Installation Personnalisée",
"installationFolder": "Dossier d'Installation",
"pathPlaceholder": "Emplacement par défaut",
"browse": "Parcourir",
"installButton": "INSTALLER HYTALE",
"installing": "INSTALLATION..."
},
"play": {
"ready": "PRÊT À JOUER",
"subtitle": "Lancez Hytale et entrez dans l'aventure",
"playButton": "JOUER À HYTALE",
"latestNews": "DERNIÈRES ACTUALITÉS",
"viewAll": "VOIR TOUT",
"checking": "VÉRIFICATION...",
"play": "JOUER"
},
"mods": {
"searchPlaceholder": "Rechercher des mods...",
"myMods": "MES MODS",
"previous": "PRÉCÉDENT",
"next": "SUIVANT",
"page": "Page",
"of": "sur",
"modalTitle": "MES MODS",
"noModsFound": "Aucun Mod Trouvé",
"noModsFoundDesc": "Essayez d'ajuster votre recherche",
"noModsInstalled": "Aucun Mod Installé",
"noModsInstalledDesc": "Ajoutez des mods depuis CurseForge ou importez des fichiers locaux",
"view": "VOIR",
"install": "INSTALLER",
"installed": "INSTALLÉ",
"enable": "ACTIVER",
"disable": "DÉSACTIVER",
"active": "ACTIF",
"disabled": "DÉSACTIVÉ",
"delete": "Supprimer le mod",
"noDescription": "Aucune description disponible",
"confirmDelete": "Êtes-vous sûr de vouloir supprimer \"{name}\" ?",
"confirmDeleteDesc": "Cette action est irréversible.",
"confirmDeletion": "Confirmer la Suppression",
"apiKeyRequired": "Clé API Requise",
"apiKeyRequiredDesc": "Une clé API CurseForge est nécessaire pour parcourir les mods"
},
"news": {
"title": "TOUTES LES ACTUALITÉS",
"readMore": "Lire Plus"
},
"chat": {
"title": "CHAT JOUEURS",
"pickColor": "Couleur",
"inputPlaceholder": "Tapez votre message...",
"send": "Envoyer",
"online": "en ligne",
"charCounter": "{current}/{max}",
"secureChat": "Chat sécurisé - Les liens sont censurés",
"joinChat": "Rejoindre le Chat",
"chooseUsername": "Choisissez un nom d'utilisateur pour rejoindre le Chat Joueurs",
"username": "Nom d'utilisateur",
"usernamePlaceholder": "Entrez votre nom d'utilisateur...",
"usernameHint": "3-20 caractères, lettres, chiffres, - et _ uniquement",
"joinButton": "Rejoindre le Chat",
"colorModal": {
"title": "Personnaliser la Couleur du Nom",
"chooseSolid": "Choisissez une couleur unie:",
"customColor": "Couleur personnalisée:",
"preview": "Aperçu:",
"previewUsername": "Nom d'utilisateur",
"apply": "Appliquer la Couleur"
}
},
"settings": {
"title": "PARAMÈTRES",
"java": "Java Runtime",
"useCustomJava": "Utiliser un Chemin Java Personnalisé",
"javaDescription": "Remplacer le Java intégré par votre propre installation",
"javaPath": "Chemin de l'Exécutable Java",
"javaPathPlaceholder": "Sélectionnez le chemin Java...",
"javaBrowse": "Parcourir",
"javaHint": "Sélectionnez le dossier d'installation de Java (compatible Windows, Mac, Linux)",
"discord": "Intégration Discord",
"enableRPC": "Activer Discord Rich Presence",
"discordDescription": "Afficher votre activité du launcher sur Discord",
"game": "Options de Jeu",
"playerName": "Nom du Joueur",
"playerNamePlaceholder": "Entrez le nom du joueur",
"playerNameHint": "Ce nom sera utilisé en jeu (1-16 caractères)",
"openGameLocation": "Ouvrir l'Emplacement du Jeu",
"openGameLocationDesc": "Ouvrir le dossier d'installation du jeu",
"account": "Gestion UUID Joueur",
"currentUUID": "UUID Actuel",
"uuidPlaceholder": "Chargement UUID...",
"copyUUID": "Copier UUID",
"regenerateUUID": "Régénérer UUID",
"uuidHint": "Votre identifiant unique de joueur pour ce nom d'utilisateur",
"manageUUIDs": "Gérer Tous les UUIDs",
"manageUUIDsDesc": "Voir et gérer tous les UUIDs de joueurs",
"language": "Langue",
"selectLanguage": "Sélectionner la Langue",
"repairGame": "Réparer le Jeu",
"reinstallGame": "Réinstaller les fichiers du jeu (préserve les données)",
"gpuPreference": "Préférence GPU",
"gpuHint": "Sélectionnez votre GPU préféré (Linux: affecte DRI_PRIME)",
"gpuAuto": "Auto",
"gpuIntegrated": "Intégré",
"gpuDedicated": "Dédié",
"logs": "JOURNAUX SYSTÈME",
"logsCopy": "Copier",
"logsRefresh": "Actualiser",
"logsFolder": "Ouvrir le Dossier",
"logsLoading": "Chargement des journaux...",
"closeLauncher": "Comportement du Launcher",
"closeOnStart": "Fermer le Launcher au démarrage du jeu",
"closeOnStartDescription": "Fermer automatiquement le launcher après le lancement d'Hytale",
"hwAccel": "Accélération Matérielle",
"hwAccelDescription": "Activer l'accélération matérielle pour le launcher",
"gameBranch": "Branche du Jeu",
"branchRelease": "Release",
"branchPreRelease": "Pré-Release",
"branchHint": "Basculer entre la version stable release et la pré-release expérimentale",
"branchWarning": "Changer de branche téléchargera et installera une version différente du jeu",
"branchSwitching": "Passage à {branch}...",
"branchSwitched": "Passage à {branch} réussi!",
"installRequired": "Installation Requise",
"branchInstallConfirm": "Le jeu sera installé pour la branche {branch}. Continuer?"
},
"uuid": {
"modalTitle": "Gestion UUID",
"currentUserUUID": "UUID Utilisateur Actuel",
"allPlayerUUIDs": "Tous les UUIDs Joueurs",
"generateNew": "Générer Nouvel UUID",
"loadingUUIDs": "Chargement des UUIDs...",
"setCustomUUID": "Définir UUID Personnalisé",
"customPlaceholder": "Entrez UUID personnalisé (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "Définir UUID",
"warning": "Attention: Définir un UUID personnalisé changera votre identité de joueur actuelle",
"copyTooltip": "Copier UUID",
"regenerateTooltip": "Générer Nouvel UUID"
},
"profiles": {
"modalTitle": "Gérer les Profils",
"newProfilePlaceholder": "Nom du Nouveau Profil",
"createProfile": "Créer un Profil"
},
"discord": {
"notificationText": "Rejoignez notre communauté Discord!",
"joinButton": "Rejoindre Discord"
},
"common": {
"confirm": "Confirmer",
"cancel": "Annuler",
"save": "Sauvegarder",
"close": "Fermer",
"delete": "Supprimer",
"edit": "Modifier",
"loading": "Chargement...",
"apply": "Appliquer",
"install": "Installer"
},
"notifications": {
"gameDataNotFound": "Erreur: Données du jeu introuvables",
"gameUpdatedSuccess": "Jeu mis à jour avec succès! 🎉",
"updateFailed": "Mise à jour échouée: {error}",
"updateError": "Erreur de mise à jour: {error}",
"discordEnabled": "Discord Rich Presence activé",
"discordDisabled": "Discord Rich Presence désactivé",
"discordSaveFailed": "Échec de la sauvegarde des paramètres Discord",
"playerNameRequired": "Veuillez entrer un nom de joueur valide",
"playerNameSaved": "Nom du joueur sauvegardé avec succès",
"playerNameSaveFailed": "Échec de la sauvegarde du nom du joueur",
"uuidCopied": "UUID copié dans le presse-papiers!",
"uuidCopyFailed": "Échec de la copie de l'UUID",
"uuidRegenNotAvailable": "Régénération UUID non disponible",
"uuidRegenFailed": "Échec de la régénération de l'UUID",
"uuidGenerated": "Nouvel UUID généré avec succès!",
"uuidGeneratedShort": "Nouvel UUID généré!",
"uuidGenerateFailed": "Échec de la génération du nouvel UUID",
"uuidRequired": "Veuillez entrer un UUID",
"uuidInvalidFormat": "Format UUID invalide",
"uuidSetFailed": "Échec de la définition de l'UUID personnalisé",
"uuidSetSuccess": "UUID personnalisé défini avec succès!",
"javaPathCopied": "Chemin Java copié dans le presse-papiers!",
"javaPathCopyFailed": "Échec de la copie du chemin Java",
"javaPathSaved": "Chemin Java sauvegardé avec succès!",
"javaPathSaveFailed": "Échec de la sauvegarde du chemin Java",
"javaPathInvalid": "Chemin Java invalide",
"javaPathReset": "Chemin Java réinitialisé aux valeurs par défaut",
"gameLocationError": "Impossible d'ouvrir l'emplacement du jeu",
"launcherRestartRequired": "Redémarrage du launcher requis pour appliquer les modifications",
"gameRepairConfirm": "Êtes-vous sûr de vouloir réparer le jeu? Cela réinstallera tous les fichiers du jeu.",
"gameRepairInProgress": "Réparation du jeu en cours...",
"gameRepairSuccess": "Jeu réparé avec succès!",
"gameRepairFailed": "Échec de la réparation du jeu: {error}",
"invalidUsername": "Nom d'utilisateur invalide",
"usernameInUse": "Nom d'utilisateur déjà utilisé",
"chatJoinSuccess": "Vous avez rejoint le chat!",
"chatJoinFailed": "Échec de la connexion au chat",
"messageTooLong": "Message trop long",
"messageSent": "Message envoyé",
"messageSendFailed": "Échec de l'envoi du message",
"colorUpdated": "Couleur mise à jour!",
"colorUpdateFailed": "Échec de la mise à jour de la couleur",
"profileCreated": "Profil créé avec succès!",
"profileCreateFailed": "Échec de la création du profil",
"profileDeleted": "Profil supprimé",
"profileDeleteFailed": "Échec de la suppression du profil",
"profileSwitched": "Profil changé vers: {name}",
"profileSwitchFailed": "Échec du changement de profil",
"invalidProfileName": "Nom de profil invalide",
"profileNameExists": "Un profil avec ce nom existe déjà",
"noInternet": "Pas de connexion Internet",
"checkInternetConnection": "Vérifiez votre connexion Internet",
"serverError": "Erreur serveur. Veuillez réessayer plus tard.",
"unknownError": "Une erreur inconnue s'est produite"
}
}

View File

@@ -1,283 +0,0 @@
{
"nav": {
"play": "Spela",
"mods": "Moddar",
"news": "Nyheter",
"chat": "Spelarchatt",
"settings": "Inställningar"
},
"header": {
"playersLabel": "Spelare:",
"manageProfiles": "Hantera profiler",
"defaultProfile": "Standard"
},
"install": {
"title": "GRATIS LAUNCHER",
"playerName": "Spelarnamn",
"playerNamePlaceholder": "Ange ditt namn",
"gameBranch": "Spelversion",
"releaseVersion": "Release (Stabil)",
"preReleaseVersion": "Pre-Release (Experimentell)",
"customInstallation": "Anpassad installation",
"installationFolder": "Installationsmapp",
"pathPlaceholder": "Standardplats",
"browse": "Bläddra",
"installButton": "INSTALLERA HYTALE",
"installing": "INSTALLERAR..."
},
"play": {
"ready": "REDO ATT SPELA",
"subtitle": "Starta Hytale och börja äventyret",
"playButton": "SPELA HYTALE",
"latestNews": "SENASTE NYHETERNA",
"viewAll": "VISA ALLA",
"checking": "KONTROLLERAR...",
"play": "SPELA"
},
"mods": {
"searchPlaceholder": "Sök moddar...",
"myMods": "MINA MODDAR",
"previous": "FÖREGÅENDE",
"next": "NÄSTA",
"page": "Sida",
"of": "av",
"modalTitle": "MINA MODDAR",
"noModsFound": "Inga moddar hittades",
"noModsFoundDesc": "Försök justera din sökning",
"noModsInstalled": "Inga moddar installerade",
"noModsInstalledDesc": "Lägg till moddar från CurseForge eller importera lokala filer",
"view": "VISA",
"install": "INSTALLERA",
"installed": "INSTALLERAD",
"enable": "AKTIVERA",
"disable": "INAKTIVERA",
"active": "AKTIV",
"disabled": "INAKTIVERAD",
"delete": "Ta bort modd",
"noDescription": "Ingen beskrivning tillgänglig",
"confirmDelete": "Är du säker på att du vill ta bort \"{name}\"?",
"confirmDeleteDesc": "Denna åtgärd kan inte ångras.",
"confirmDeletion": "Bekräfta borttagning",
"apiKeyRequired": "API-nyckel krävs",
"apiKeyRequiredDesc": "CurseForge API-nyckel behövs för att bläddra bland moddar"
},
"news": {
"title": "ALLA NYHETER",
"readMore": "Läs mer"
},
"chat": {
"title": "SPELARCHATT",
"pickColor": "Färg",
"inputPlaceholder": "Skriv ditt meddelande...",
"send": "Skicka",
"online": "online",
"charCounter": "{current}/{max}",
"secureChat": "Säker chatt - Länkar är censurerade",
"joinChat": "Gå med i chatten",
"chooseUsername": "Välj ett användarnamn för att gå med i spelarchartten",
"username": "Användarnamn",
"usernamePlaceholder": "Ange ditt användarnamn...",
"usernameHint": "3-20 tecken, endast bokstäver, siffror, - och _",
"joinButton": "Gå med i chatten",
"colorModal": {
"title": "Anpassa användarnamnsfargen",
"chooseSolid": "Välj en enfärgad färg:",
"customColor": "Anpassad färg:",
"preview": "Förhandsvisning:",
"previewUsername": "Användarnamn",
"apply": "Använd färg"
}
},
"settings": {
"title": "INSTÄLLNINGAR",
"java": "Java Runtime",
"useCustomJava": "Använd anpassad Java-sökväg",
"javaDescription": "Ersätt den medföljande Java-installationen med din egen",
"javaPath": "Java-körbar fil-sökväg",
"javaPathPlaceholder": "Välj Java-sökväg...",
"javaBrowse": "Bläddra",
"javaHint": "Välj Java-installationsmappen (stöder Windows, Mac, Linux)",
"discord": "Discord-integration",
"enableRPC": "Aktivera Discord Rich Presence",
"discordDescription": "Visa din launcher-aktivitet på Discord",
"game": "Spelalternativ",
"playerName": "Spelarnamn",
"playerNamePlaceholder": "Ange spelarnamn",
"playerNameHint": "Detta namn kommer att användas i spelet (1-16 tecken)",
"openGameLocation": "Öppna spelplats",
"openGameLocationDesc": "Öppna spelinstallationsmappen",
"account": "Spelare UUID-hantering",
"currentUUID": "Nuvarande UUID",
"uuidPlaceholder": "Laddar UUID...",
"copyUUID": "Kopiera UUID",
"regenerateUUID": "Återskapa UUID",
"uuidHint": "Din unika spelaridentifierare för detta användarnamn",
"manageUUIDs": "Hantera alla UUID:er",
"manageUUIDsDesc": "Visa och hantera alla spelare-UUID:er",
"language": "Språk",
"selectLanguage": "Välj språk",
"repairGame": "Reparera spel",
"reinstallGame": "Ominstallera spelfiler (bevarar data)",
"gpuPreference": "GPU-preferens",
"gpuHint": "Välj din föredragna GPU (Linux: påverkar DRI_PRIME)",
"gpuAuto": "Auto",
"gpuIntegrated": "Integrerad",
"gpuDedicated": "Dedikerad",
"logs": "SYSTEMLOGGAR",
"logsCopy": "Kopiera",
"logsRefresh": "Uppdatera",
"logsFolder": "Öppna mapp",
"logsLoading": "Laddar loggar...",
"closeLauncher": "Launcher-beteende",
"closeOnStart": "Stäng launcher vid spelstart",
"closeOnStartDescription": "Stäng automatiskt launcher efter att Hytale har startats",
"hwAccel": "Hårdvaruacceleration",
"hwAccelDescription": "Aktivera hårdvaruacceleration för launchern",
"gameBranch": "Spelgren",
"branchRelease": "Release",
"branchPreRelease": "Pre-Release",
"branchHint": "Växla mellan stabil release- och experimentell pre-release-version",
"branchWarning": "Att byta gren kommer att ladda ner och installera en annan spelversion",
"branchSwitching": "Byter till {branch}...",
"branchSwitched": "Bytte framgångsrikt till {branch}!",
"installRequired": "Installation krävs",
"branchInstallConfirm": "Spelet kommer att installeras för {branch}-grenen. Fortsätt?"
},
"uuid": {
"modalTitle": "UUID-hantering",
"currentUserUUID": "Nuvarande användar-UUID",
"allPlayerUUIDs": "Alla spelare-UUID:er",
"generateNew": "Generera ny UUID",
"loadingUUIDs": "Laddar UUID:er...",
"setCustomUUID": "Ange anpassad UUID",
"customPlaceholder": "Ange anpassad UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "Ange UUID",
"warning": "Varning: Att ange en anpassad UUID kommer att ändra din nuvarande spelaridentitet",
"copyTooltip": "Kopiera UUID",
"regenerateTooltip": "Generera ny UUID"
},
"profiles": {
"modalTitle": "Hantera profiler",
"newProfilePlaceholder": "Nytt profilnamn",
"createProfile": "Skapa profil"
},
"discord": {
"notificationText": "Gå med i vår Discord-gemenskap!",
"joinButton": "Gå med i Discord"
},
"common": {
"confirm": "Bekräfta",
"cancel": "Avbryt",
"save": "Spara",
"close": "Stäng",
"delete": "Ta bort",
"edit": "Redigera",
"loading": "Laddar...",
"apply": "Verkställ",
"install": "Installera"
},
"notifications": {
"gameDataNotFound": "Fel: Speldata hittades inte",
"gameUpdatedSuccess": "Spelet uppdaterades framgångsrikt! 🎉",
"updateFailed": "Uppdatering misslyckades: {error}",
"updateError": "Uppdateringsfel: {error}",
"discordEnabled": "Discord Rich Presence aktiverad",
"discordDisabled": "Discord Rich Presence inaktiverad",
"discordSaveFailed": "Misslyckades med att spara Discord-inställning",
"playerNameRequired": "Ange ett giltigt spelarnamn",
"playerNameSaved": "Spelarnamn sparat framgångsrikt",
"playerNameSaveFailed": "Misslyckades med att spara spelarnamn",
"uuidCopied": "UUID kopierad till urklipp!",
"uuidCopyFailed": "Misslyckades med att kopiera UUID",
"uuidRegenNotAvailable": "UUID-återgenerering ej tillgänglig",
"uuidRegenFailed": "Misslyckades med att återgenerera UUID",
"uuidGenerated": "Ny UUID genererad framgångsrikt!",
"uuidGeneratedShort": "Ny UUID genererad!",
"uuidGenerateFailed": "Misslyckades med att generera ny UUID",
"uuidRequired": "Ange en UUID",
"uuidInvalidFormat": "Ogiltigt UUID-format",
"uuidSetFailed": "Misslyckades med att ange anpassad UUID",
"uuidSetSuccess": "Anpassad UUID angiven framgångsrikt!",
"uuidDeleteFailed": "Misslyckades med att ta bort UUID",
"uuidDeleteSuccess": "UUID borttagen framgångsrikt!",
"modsDownloading": "Laddar ner {name}...",
"modsTogglingMod": "Växlar modd...",
"modsDeletingMod": "Tar bort modd...",
"modsLoadingMods": "Laddar moddar från CurseForge...",
"modsInstalledSuccess": "{name} installerad framgångsrikt! 🎉",
"modsDeletedSuccess": "{name} borttagen framgångsrikt",
"modsDownloadFailed": "Misslyckades med att ladda ner modd: {error}",
"modsToggleFailed": "Misslyckades med att växla modd: {error}",
"modsDeleteFailed": "Misslyckades med att ta bort modd: {error}",
"modsModNotFound": "Moddinformation hittades inte",
"hwAccelSaved": "Hårdvaruaccelerationsinställning sparad",
"hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning",
"javaPathCopied": "Java-sökväg kopierad till urklipp!",
"javaPathCopyFailed": "Misslyckades med att kopiera Java-sökväg",
"javaPathSaved": "Java-sökväg sparad framgångsrikt!",
"javaPathSaveFailed": "Misslyckades med att spara Java-sökväg",
"javaPathInvalid": "Ogiltig Java-sökväg",
"javaPathReset": "Java-sökväg återställd till standardvärden",
"gameLocationError": "Kunde inte öppna spelplats",
"launcherRestartRequired": "Launcher-omstart krävs för att tillämpa ändringar",
"gameRepairConfirm": "Är du säker på att du vill reparera spelet? Detta kommer att ominstallera alla spelfiler.",
"gameRepairInProgress": "Reparerar spel...",
"gameRepairSuccess": "Spel reparerat framgångsrikt!",
"gameRepairFailed": "Spelreparation misslyckades: {error}",
"invalidUsername": "Ogiltigt användarnamn",
"usernameInUse": "Användarnamn upptaget",
"chatJoinSuccess": "Du har gått med i chatten!",
"chatJoinFailed": "Misslyckades med att gå med i chatten",
"messageTooLong": "Meddelande för långt",
"messageSent": "Meddelande skickat",
"messageSendFailed": "Misslyckades med att skicka meddelande",
"colorUpdated": "Färg uppdaterad!",
"colorUpdateFailed": "Misslyckades med att uppdatera färg",
"profileCreated": "Profil skapad framgångsrikt!",
"profileCreateFailed": "Misslyckades med att skapa profil",
"profileDeleted": "Profil borttagen",
"profileDeleteFailed": "Misslyckades med att ta bort profil",
"profileSwitched": "Bytte profil till: {name}",
"profileSwitchFailed": "Profilbyte misslyckades",
"invalidProfileName": "Ogiltigt profilnamn",
"profileNameExists": "En profil med detta namn finns redan",
"noInternet": "Ingen internetanslutning",
"checkInternetConnection": "Kontrollera din internetanslutning",
"serverError": "Serverfel. Försök igen senare.",
"unknownError": "Ett okänt fel inträffade"
},
"confirm": {
"defaultTitle": "Bekräfta åtgärd",
"regenerateUuidTitle": "Generera ny UUID",
"regenerateUuidMessage": "Är du säker på att du vill generera en ny UUID? Detta kommer att ändra din spelaridentitet.",
"regenerateUuidButton": "Generera",
"setCustomUuidTitle": "Ange anpassad UUID",
"setCustomUuidMessage": "Är du säker på att du vill ange denna anpassade UUID? Detta kommer att ändra din spelaridentitet.",
"setCustomUuidButton": "Ange UUID",
"deleteUuidTitle": "Ta bort UUID",
"deleteUuidMessage": "Är du säker på att du vill ta bort UUID:n för \"{username}\"? Denna åtgärd kan inte ångras.",
"deleteUuidButton": "Ta bort",
"uninstallGameTitle": "Avinstallera spel",
"uninstallGameMessage": "Är du säker på att du vill avinstallera Hytale? Alla spelfiler kommer att tas bort.",
"uninstallGameButton": "Avinstallera"
},
"progress": {
"initializing": "Initierar...",
"downloading": "Laddar ner...",
"installing": "Installerar...",
"extracting": "Extraherar...",
"verifying": "Verifierar...",
"switchingProfile": "Byter profil...",
"profileSwitched": "Profil bytt!",
"startingGame": "Startar spel...",
"launching": "STARTAR...",
"uninstallingGame": "Avinstallerar spel...",
"gameUninstalled": "Spel avinstallerat framgångsrikt!",
"uninstallFailed": "Avinstallation misslyckades: {error}",
"startingUpdate": "Startar obligatorisk speluppdatering...",
"installationComplete": "Installation slutförd framgångsrikt!",
"installationFailed": "Installation misslyckades: {error}",
"installingGameFiles": "Installerar spelfiler...",
"installComplete": "Installation slutförd!"
}
}

View File

@@ -14,15 +14,15 @@ source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop")
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
build() {
cd "$pkgname-$pkgver"
cd "$_pkgname-$pkgver"
npm ci
npm run build:arch
}
package() {
cd "$pkgname-$pkgver"
install -d "$pkgdir/opt/$pkgname"
cp -r dist/linux-unpacked/* "$pkgdir/opt/$pkgname"
install -Dm644 "$srcdir/$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop"
install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$pkgname.png"
cd "$_pkgname-$pkgver"
install -d "$pkgdir/opt/$_pkgname"
cp -r dist/linux-unpacked/* "$pkgdir/opt/$_pkgname"
install -Dm644 "$srcdir/$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png"
}

View File

@@ -4,10 +4,9 @@
<h1>🎮 Hytale F2P Launcher 🚀</h1>
<h2>💻 Cross-Platform Multiplayer 🖥️</h2>
<h3>Available for Windows 🪟, macOS 🍎, and Linux 🐧</h3>
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support!</small></p>
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)</small></p>
</header>
![GitHub Downloads](https://img.shields.io/github/downloads/amiayweb/Hytale-F2P/total?style=for-the-badge)
![Version](https://img.shields.io/badge/Version-2.1.1-green?style=for-the-badge)
![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-orange?style=for-the-badge)
![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge)
@@ -119,9 +118,9 @@
<tr>
<td><b>🖥️ OS</b></td>
<td colspan="3" align="center">
Windows 10/11 (64-bit X64) | Linux (x64) | macOS (ARM64/Apple Silicon)
Windows 10/11 (64-bit; X64/ARM64) | Linux (x64/ARM64) | macOS (Apple Silicon only)
<br />
<small><i>⚠️ Note: ARM64 (Windows & Linux), macOS (x86/Intel) <b>are not supported!</b> ⚠️</i></small>
<small><i>⚠️ Note: macOS Intel (x86) is not yet supported <sup><a href="#fn1" id="ref1">1</a></sup></i></small>
</td>
</tr>
<tr>
@@ -132,7 +131,7 @@
</tr>
<tr>
<td><b>🧠 RAM</b></td>
<td>8GB (dGPU) / 12GB (iGPU)<sup><a href="#fn1" id="ref1">1</a></sup></td>
<td>8GB (dGPU)<sup><a href="#fn1" id="ref2">2</a></sup> /<br>12GB (iGPU)<sup><a href="#fn1" id="ref3">3</a></sup></td>
<td>16 GB</td>
<td>32 GB</td>
</tr>
@@ -157,19 +156,19 @@
</tbody>
</table>
</div>
<p id="fn1"><sup>Note 1</sup> Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum, while using Integrated GPU (iGPU) must have 12 GB RAM.</p>
<p id="fn1"><sup>Note 1</sup> Hytale did not provide game files for macOS Intel, yet.</p>
<p id="fn2"><sup>Note 2</sup> Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum.</p>
<p id="fn3"><sup>Note 3</sup> Using Integrated GPU (dGPU) must have 12 GB RAM minimum.</p>
> [!WARNING]
> Our launcher has **not yet** supported Offline Mode (playing Hytale without internet).
> We will surely add the feature as soon as possible. Kindly wait for the update.
---
### 🪟 Windows Prequisites
* **
* **Java JDK 25:**
* [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows)
* [Adoptium](https://adoptium.net/temurin/releases/?version=25)
* [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows), **no** support for Windows ARM64 in both version 25 and 21.
* [Adoptium](https://adoptium.net/temurin/releases/?version=25), has Windows ARM64 support in version 21 only.
* [Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download), has Windows ARM64 support in version 25.
* Download from any vendor if your OS is not Windows with ARM64 architecture.
* **Latest Visual Studio Redist:**
* Download via [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe)
* Or [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/)
@@ -179,9 +178,9 @@
> [!WARNING]
> Ubuntu-based Distro like ZorinOS or Pop!_OS or Linux Mint would encounter issues due to UbuntuLTS environment, [check this Discord post](https://discord.com/channels/1462260103951421493/1463662398501027973).
* Make sure you have already installed newest **GPU driver** especially proprietary NVIDIA, consult your distro docs or wiki.
* Also make sure that your GPU can be connected to EGL, try checking it first (again, consult your distro docs or wiki) before installing Hytale game via our launcher.
* Install `libpng` package to avoid `SDL3_Image` error:
* Make sure you have already installed newest **GPU driver**, consult your distro docs or wiki.
* Install `libpng` package to avoid SDL3_Image error:
* `libpng16-16 libpng-dev` for Ubuntu/Debian-based Distro
* `libpng libpng-devel` for Fedora/RHEL-based Distro
* `libpng` for Arch-based Distro
@@ -195,12 +194,11 @@
1. **Prerequisites:** Ensure you have installed all [**Windows Prerequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-windows-prequisites) listed above.
2. **Download:** Get the latest `Hytale-F2P-Launcher.exe` from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page.
3. **SmartScreen Note:** Since the executable is currently unsigned, Windows may show a "Windows protected your PC" popup.
* Click **More info**, then click **Run anyway**.
* Click **More info**.
* Click **Run anyway**.
4. **Launch:** Once installed, you can launch the app directly from your Desktop or the Start menu.
5. **Whitelist in Windows Firewall** [#192](https://github.com/amiayweb/Hytale-F2P/issues/192#issuecomment-3803042908)
* Open the Windows Start Menu and search for `Allow an app through Windows Firewall`
* Click "Change settings" (you may need Admin privileges) and Locate `HytaleClient.exe` in the list.
* Ensure both the Private and Public checkboxes are checked. Click OK to save.
---
### 🐧 Linux Installation
@@ -245,6 +243,8 @@
* **Desktop Entry:** After installing via `.rpm`, `.deb`, or `.pkg.tar.zst`, the launcher should automatically appear in your App Library/Grid.
* Missing libxcrypt.so.1: Install `libxcrypt-compat` using your package manager
---
### 🍎 macOS Installation
> [!NOTE]
@@ -272,9 +272,9 @@ The `.zip` version is useful for users who prefer a portable installation or nee
---
# 📢 How to Host a Server
# How to Host a Server
## 🌐 Host your Singleplayer Server (Online-Play Feature)
## Host your Singleplayer Server (Online-Play Feature)
> [!NOTE]
> You have to play the game to host the server. See Dedicated Server section below if you want to host it without you playing as the host.
@@ -283,29 +283,23 @@ The `.zip` version is useful for users who prefer a portable installation or nee
2. Pause the game (Esc) > select Online Play > Turn on `Allow Other Players to Join` > Set password if needed > Press `Save`.
3. Check the status `Connected via STUN` or `Connected via UPnP`.
## 🖧 Host a Dedicated Server
## Dedicated Server
> [!NOTE]
> If you already have the patched `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server.
> If you have already `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server.
> [!TIP]
> Use services like Playit.gg, Tailscale, Radmin VPN to share UDP connection if setting up router as an admin is not possible.
> [!WARNING]
> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting). Linux ARM64 is supported for server only.
> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting).
> [!IMPORTANT]
> See detailed information of setting up a server here: [SERVER.md](SERVER.md). Download the latest patched JAR, the patched RAR, or the SH/BAT scripts from channel `#open-public-server` in our Discord Server.
> See detailed information of setting up a server here: [SERVER.md](SERVER.md)
---
## 🔧 Troubleshooting
See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed Troubleshooting guide.
---
## 🔨 Building from Source
## 🛠️ Building from Source
See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.

View File

@@ -14,21 +14,6 @@ function getAppDir() {
}
}
/**
* Get centralized UserData saves directory (NEW in 2.1.2)
* UserData is now stored separately from game installation
*/
function getHytaleSavesDir() {
const home = os.homedir();
if (process.platform === 'win32') {
return path.join(home, 'AppData', 'Local', 'HytaleSaves');
} else if (process.platform === 'darwin') {
return path.join(home, 'Library', 'Application Support', 'HytaleSaves');
} else {
return path.join(home, '.hytalesaves');
}
}
const DEFAULT_APP_DIR = getAppDir();
function getResolvedAppDir(customPath) {
@@ -233,8 +218,20 @@ async function getModsPath(customInstallPath = null) {
function getProfilesDir(customInstallPath = null) {
try {
// NEW 2.1.2: Use centralized UserData location
const userDataPath = getHytaleSavesDir();
// get UserData path
let installPath = customInstallPath;
if (!installPath) {
const configFile = path.join(DEFAULT_APP_DIR, 'config.json');
if (fs.existsSync(configFile)) {
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
installPath = config.installPath || '';
}
}
if (!installPath) installPath = getAppDir();
const branch = loadVersionBranch();
const gameLatest = path.join(installPath, branch, 'package', 'game', 'latest');
const userDataPath = findUserDataPath(gameLatest);
const profilesDir = path.join(userDataPath, 'Profiles');
if (!fs.existsSync(profilesDir)) {
@@ -250,7 +247,6 @@ function getProfilesDir(customInstallPath = null) {
module.exports = {
getAppDir,
getHytaleSavesDir,
getResolvedAppDir,
expandHome,
APP_DIR,

View File

@@ -12,7 +12,6 @@ const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA
const { getLatestClientVersion } = require('../services/versionManager');
const { updateGameFiles } = require('./gameManager');
const { syncModsForCurrentProfile } = require('./modManager');
const { getUserDataPath } = require('../utils/userDataMigration');
// Client patcher for custom auth server (sanasol.ws)
let clientPatcher = null;
@@ -107,9 +106,7 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
const customAppDir = getResolvedAppDir(installPathOverride);
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
// NEW 2.1.2: Use centralized UserData location
const userDataDir = getUserDataPath();
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
const gameLatest = customGameDir;
let clientPath = findClientPath(gameLatest);
@@ -285,55 +282,6 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
const gpuEnv = setupGpuEnvironment(gpuPreference);
Object.assign(env, gpuEnv);
// Linux: Replace bundled libzstd.so with system version to fix glibc 2.41+ crash
// The bundled libzstd causes "free(): invalid pointer" on Steam Deck / Ubuntu LTS
if (process.platform === 'linux' && process.env.HYTALE_NO_LIBZSTD_FIX !== '1') {
const clientDir = path.dirname(clientPath);
const bundledLibzstd = path.join(clientDir, 'libzstd.so');
const backupLibzstd = path.join(clientDir, 'libzstd.so.bundled');
// Common system libzstd paths
const systemLibzstdPaths = [
'/usr/lib/libzstd.so.1', // Arch Linux, Steam Deck
'/usr/lib/x86_64-linux-gnu/libzstd.so.1', // Debian/Ubuntu
'/usr/lib64/libzstd.so.1' // Fedora/RHEL
];
let systemLibzstd = null;
for (const p of systemLibzstdPaths) {
if (fs.existsSync(p)) {
systemLibzstd = p;
break;
}
}
if (systemLibzstd && fs.existsSync(bundledLibzstd)) {
try {
const stats = fs.lstatSync(bundledLibzstd);
// Only replace if it's not already a symlink to system version
if (!stats.isSymbolicLink()) {
// Backup bundled version
if (!fs.existsSync(backupLibzstd)) {
fs.renameSync(bundledLibzstd, backupLibzstd);
console.log(`Linux: Backed up bundled libzstd.so`);
} else {
fs.unlinkSync(bundledLibzstd);
}
// Create symlink to system version
fs.symlinkSync(systemLibzstd, bundledLibzstd);
console.log(`Linux: Linked libzstd.so to system version (${systemLibzstd}) for glibc 2.41+ compatibility`);
} else {
const linkTarget = fs.readlinkSync(bundledLibzstd);
console.log(`Linux: libzstd.so already linked to ${linkTarget}`);
}
} catch (libzstdError) {
console.warn(`Linux: Could not replace libzstd.so: ${libzstdError.message}`);
}
}
}
try {
let spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'],

View File

@@ -9,7 +9,7 @@ const { installButler } = require('./butlerManager');
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config');
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
const { getUserDataPath, migrateUserDataToCentralized } = require('../utils/userDataMigration');
const userDataBackup = require('../utils/userDataBackup');
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
const osName = getOS();
@@ -308,25 +308,31 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, cacheDir = CACHE_DIR, branchOverride = null) {
let tempUpdateDir;
let backupPath = null;
const branch = branchOverride || loadVersionBranch();
const installPath = path.dirname(path.dirname(path.dirname(path.dirname(gameDir))));
// Vérifier si on a version_client et version_branch dans config.json
const config = loadConfig();
const oldBranch = config.version_branch || 'release';
const hasVersionConfig = !!(config.version_client && config.version_branch);
const oldBranch = config.version_branch || 'release'; // L'ancienne branche pour le backup
console.log(`[UpdateGameFiles] hasVersionConfig: ${hasVersionConfig}`);
console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`);
try {
// NEW 2.1.2: Ensure UserData migration to centralized location
try {
console.log('[UpdateGameFiles] Ensuring UserData migration...');
const migrationResult = await migrateUserDataToCentralized();
if (migrationResult.migrated) {
console.log('[UpdateGameFiles] ✓ UserData migrated to centralized location');
} else if (migrationResult.alreadyMigrated) {
console.log('[UpdateGameFiles] ✓ UserData already in centralized location');
if (progressCallback) {
progressCallback('Backing up user data...', 5, null, null, null);
}
} catch (migrationError) {
console.warn('[UpdateGameFiles] UserData migration warning:', migrationError.message);
// Backup UserData AVANT de télécharger/installer (critical for same-branch updates)
try {
console.log(`[UpdateGameFiles] Attempting to backup UserData from old branch: ${oldBranch}`);
backupPath = await userDataBackup.backupUserData(installPath, oldBranch, hasVersionConfig);
if (backupPath) {
console.log(`[UpdateGameFiles] ✓ UserData backed up from ${oldBranch}: ${backupPath}`);
}
} catch (backupError) {
console.warn('[UpdateGameFiles] ✗ UserData backup failed:', backupError.message);
}
if (progressCallback) {
@@ -384,9 +390,31 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
console.log('Logo@2x.png update result after update:', logoResult);
// NEW 2.1.2: No longer create UserData in game installation
// UserData is now in centralized location (getUserDataPath())
console.log('[UpdateGameFiles] UserData is managed in centralized location');
// Ensure UserData directory exists
const userDataDir = path.join(gameDir, 'Client', 'UserData');
if (!fs.existsSync(userDataDir)) {
console.log(`[UpdateGameFiles] Creating UserData directory at: ${userDataDir}`);
fs.mkdirSync(userDataDir, { recursive: true });
}
if (progressCallback) {
progressCallback('Restoring user data...', 90, null, null, null);
}
// Restore UserData using new system
if (backupPath) {
try {
console.log(`[UpdateGameFiles] Restoring UserData from ${oldBranch} to ${branch}`);
console.log(`[UpdateGameFiles] Source backup: ${backupPath}`);
await userDataBackup.restoreUserData(backupPath, installPath, branch);
await userDataBackup.cleanupBackup(backupPath);
console.log(`[UpdateGameFiles] ✓ UserData migrated successfully from ${oldBranch} to ${branch}`);
} catch (restoreError) {
console.warn('[UpdateGameFiles] ✗ UserData restore failed:', restoreError.message);
}
} else {
console.log('[UpdateGameFiles] No backup to restore, empty UserData folder created');
}
console.log(`Game files updated successfully to version: ${newVersion}`);
@@ -406,6 +434,15 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
} catch (error) {
console.error('Error updating game files:', error);
if (backupPath) {
try {
await userDataBackup.cleanupBackup(backupPath);
console.log('UserData backup cleaned up after error');
} catch (cleanupError) {
console.warn('Could not clean up UserData backup:', cleanupError.message);
}
}
if (tempUpdateDir && fs.existsSync(tempUpdateDir)) {
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
}
@@ -433,18 +470,28 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
const customToolsDir = path.join(customAppDir, 'butler');
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
// NEW 2.1.2: Ensure UserData migration to centralized location
try {
console.log('[InstallGame] Ensuring UserData migration...');
const migrationResult = await migrateUserDataToCentralized();
if (migrationResult.migrated) {
console.log('[InstallGame] ✓ UserData migrated to centralized location');
} else if (migrationResult.alreadyMigrated) {
console.log('[InstallGame] ✓ UserData already in centralized location');
// Vérifier si on a version_client et version_branch dans config.json
const config = loadConfig();
const hasVersionConfig = !!(config.version_client && config.version_branch);
console.log(`[InstallGame] Configuration detected - version_client: ${config.version_client}, version_branch: ${config.version_branch}`);
console.log(`[InstallGame] hasVersionConfig: ${hasVersionConfig}`);
// Backup UserData AVANT l'installation si nécessaire
let backupPath = null;
if (progressCallback) {
progressCallback('Checking for existing UserData...', 5, null, null, null);
}
} catch (migrationError) {
console.warn('[InstallGame] UserData migration warning:', migrationError.message);
try {
console.log(`[InstallGame] Attempting UserData backup (hasVersionConfig: ${hasVersionConfig})...`);
backupPath = await userDataBackup.backupUserData(customAppDir, branch, hasVersionConfig);
if (backupPath) {
console.log(`[InstallGame] ✓ UserData backed up to: ${backupPath}`);
}
} catch (backupError) {
console.warn('[InstallGame] ✗ UserData backup failed:', backupError.message);
}
[customAppDir, customCacheDir, customToolsDir].forEach(dir => {
@@ -453,6 +500,10 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
}
});
if (!fs.existsSync(userDataDir)) {
fs.mkdirSync(userDataDir, { recursive: true });
}
saveUsername(playerName);
if (installPathOverride) {
saveInstallPath(installPathOverride);
@@ -544,9 +595,29 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
console.log('Logo@2x.png update result after installation:', logoResult);
// NEW 2.1.2: No longer create UserData in game installation
// UserData is managed in centralized location (getUserDataPath())
console.log('[InstallGame] UserData is managed in centralized location');
// Ensure UserData directory exists
if (!fs.existsSync(userDataDir)) {
console.log(`[InstallGame] Creating UserData directory at: ${userDataDir}`);
fs.mkdirSync(userDataDir, { recursive: true });
}
// Restore UserData from backup if exists
if (backupPath) {
if (progressCallback) {
progressCallback('Restoring UserData...', 95, null, null, null);
}
try {
console.log(`[InstallGame] Restoring UserData from: ${backupPath}`);
await userDataBackup.restoreUserData(backupPath, customAppDir, branch);
await userDataBackup.cleanupBackup(backupPath);
console.log('[InstallGame] ✓ UserData restored successfully');
} catch (restoreError) {
console.warn('[InstallGame] ✗ UserData restore failed:', restoreError.message);
}
} else {
console.log('[InstallGame] No backup to restore, empty UserData folder created');
}
if (progressCallback) {
progressCallback('Installation complete', 100, null, null, null);

View File

@@ -1,5 +1,10 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const AdmZip = require('adm-zip');
const { execSync, spawn } = require('child_process');
const { getJavaExec, getBundledJavaPath } = require('../managers/javaManager');
const { JRE_DIR } = require('../core/paths');
// Domain configuration
const ORIGINAL_DOMAIN = 'hytale.com';
@@ -21,13 +26,15 @@ function getTargetDomain() {
const DEFAULT_NEW_DOMAIN = 'auth.sanasol.ws';
/**
* Patches HytaleClient binary to replace hytale.com with custom domain
* Server patching is done via pre-patched JAR download from CDN
* Patches HytaleClient and HytaleServer binaries to replace hytale.com with custom domain
* This allows the game to connect to a custom authentication server
*
* Supports domains from 4 to 16 characters:
* - All F2P traffic routes to single endpoint: https://{domain} (no subdomains)
* - Domains <= 10 chars: Direct replacement, subdomains stripped
* - Domains 11-16 chars: Split mode - first 6 chars replace subdomain prefix, rest replaces domain
*
* Official hytale.com keeps original subdomain behavior (sessions., account-data., etc.)
*/
class ClientPatcher {
constructor() {
@@ -54,16 +61,19 @@ class ClientPatcher {
/**
* Calculate the domain patching strategy based on length
* @returns {object} Strategy with mainDomain and subdomainPrefix
*/
getDomainStrategy(domain) {
if (domain.length <= 10) {
// Direct replacement - subdomains will be stripped
return {
mode: 'direct',
mainDomain: domain,
subdomainPrefix: '',
subdomainPrefix: '', // Empty = subdomains stripped
description: `Direct replacement: hytale.com -> ${domain}`
};
} else {
// Split mode: first 6 chars become subdomain prefix, rest replaces hytale.com
const prefix = domain.slice(0, 6);
const suffix = domain.slice(6);
return {
@@ -77,15 +87,21 @@ class ClientPatcher {
/**
* Convert a string to the length-prefixed byte format used by the client
* Format: [length byte] [00 00 00 padding] [char1] [00] [char2] [00] ... [lastChar]
* Note: No null byte after the last character
*/
stringToLengthPrefixed(str) {
const length = str.length;
const result = Buffer.alloc(4 + length + (length - 1));
const result = Buffer.alloc(4 + length + (length - 1)); // length byte + padding + chars + separators
// Length byte
result[0] = length;
// Padding: 00 00 00
result[1] = 0x00;
result[2] = 0x00;
result[3] = 0x00;
// Characters with null separators (no separator after last char)
let pos = 4;
for (let i = 0; i < length; i++) {
result[pos++] = str.charCodeAt(i);
@@ -93,6 +109,7 @@ class ClientPatcher {
result[pos++] = 0x00;
}
}
return result;
}
@@ -107,6 +124,13 @@ class ClientPatcher {
return buf;
}
/**
* Convert a string to UTF-8 bytes (how Java stores strings)
*/
stringToUtf8(str) {
return Buffer.from(str, 'utf8');
}
/**
* Find all occurrences of a pattern in a buffer
*/
@@ -124,6 +148,7 @@ class ClientPatcher {
/**
* Replace bytes in buffer - only overwrites the length of new bytes
* Prevents offset corruption by not expanding the replacement
*/
replaceBytes(buffer, oldBytes, newBytes) {
let count = 0;
@@ -135,7 +160,9 @@ class ClientPatcher {
}
const positions = this.findAllOccurrences(result, oldBytes);
for (const pos of positions) {
// Only overwrite the length of the new bytes
newBytes.copy(result, pos);
count++;
}
@@ -144,7 +171,32 @@ class ClientPatcher {
}
/**
* Smart domain replacement that handles both null-terminated and non-null-terminated strings
* UTF-8 domain replacement for Java JAR files.
* Java stores strings in UTF-8 format in the constant pool.
*/
findAndReplaceDomainUtf8(data, oldDomain, newDomain) {
let count = 0;
const result = Buffer.from(data);
const oldUtf8 = this.stringToUtf8(oldDomain);
const newUtf8 = this.stringToUtf8(newDomain);
const positions = this.findAllOccurrences(result, oldUtf8);
for (const pos of positions) {
newUtf8.copy(result, pos);
count++;
console.log(` Patched UTF-8 occurrence at offset 0x${pos.toString(16)}`);
}
return { buffer: result, count };
}
/**
* Smart domain replacement that handles both null-terminated and non-null-terminated strings.
* .NET AOT stores some strings in various formats:
* - Standard UTF-16LE (each char is 2 bytes with \x00 high byte)
* - Length-prefixed where last char may have metadata byte instead of \x00
*/
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
let count = 0;
@@ -166,6 +218,7 @@ class ClientPatcher {
if (lastCharFirstByte === oldLastCharByte) {
newUtf16NoLast.copy(result, pos);
result[lastCharPos] = newLastCharByte;
if (lastCharPos + 1 < result.length) {
@@ -185,6 +238,7 @@ class ClientPatcher {
/**
* Apply all domain patches using length-prefixed format
* This is the main patching method for variable-length domains
*/
applyDomainPatches(data, domain, protocol = 'https://') {
let result = Buffer.from(data);
@@ -209,7 +263,7 @@ class ClientPatcher {
totalCount += sentryResult.count;
}
// 2. Patch main domain
// 2. Patch main domain (hytale.com -> mainDomain)
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
const domainResult = this.replaceBytes(
result,
@@ -244,7 +298,7 @@ class ClientPatcher {
}
/**
* Patch Discord invite URLs
* Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7
*/
patchDiscordUrl(data) {
let count = 0;
@@ -253,6 +307,7 @@ class ClientPatcher {
const oldUrl = '.gg/hytale';
const newUrl = '.gg/MHkEjepMQ7';
// Try length-prefixed format first
const lpResult = this.replaceBytes(
result,
this.stringToLengthPrefixed(oldUrl),
@@ -268,6 +323,7 @@ class ClientPatcher {
const newUtf16 = this.stringToUtf16LE(newUrl);
const positions = this.findAllOccurrences(result, oldUtf16);
for (const pos of positions) {
newUtf16.copy(result, pos);
count++;
@@ -277,66 +333,39 @@ class ClientPatcher {
}
/**
* Check patch status of client binary
* Check if the client binary has already been patched
* Also verifies the binary actually contains the patched domain
*/
getPatchStatus(clientPath) {
isPatchedAlready(clientPath) {
const newDomain = this.getNewDomain();
const patchFlagFile = clientPath + this.patchedFlag;
// First check flag file
if (fs.existsSync(patchFlagFile)) {
try {
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
const currentDomain = flagData.targetDomain;
if (currentDomain === newDomain) {
if (flagData.targetDomain === newDomain) {
// Verify the binary actually contains the patched domain
const data = fs.readFileSync(clientPath);
const strategy = this.getDomainStrategy(newDomain);
const domainPattern = this.stringToLengthPrefixed(strategy.mainDomain);
if (data.includes(domainPattern)) {
return { patched: true, currentDomain, needsRestore: false };
return true;
} else {
console.log(' Flag exists but binary not patched (was updated?), needs re-patching...');
return { patched: false, currentDomain: null, needsRestore: false };
console.log(' Flag exists but binary not patched (was updated?), re-patching...');
return false;
}
} else {
console.log(` Currently patched for "${currentDomain}", need to change to "${newDomain}"`);
return { patched: false, currentDomain, needsRestore: true };
}
} catch (e) {
// Flag file corrupt
// Flag file corrupt or unreadable
}
}
return { patched: false, currentDomain: null, needsRestore: false };
}
/**
* Check if client is already patched (backward compat)
*/
isPatchedAlready(clientPath) {
return this.getPatchStatus(clientPath).patched;
}
/**
* Restore client from backup
*/
restoreFromBackup(clientPath) {
const backupPath = clientPath + '.original';
if (fs.existsSync(backupPath)) {
console.log(' Restoring original binary from backup for re-patching...');
fs.copyFileSync(backupPath, clientPath);
const patchFlagFile = clientPath + this.patchedFlag;
if (fs.existsSync(patchFlagFile)) {
fs.unlinkSync(patchFlagFile);
}
return true;
}
console.warn(' No backup found to restore - will try patching anyway');
return false;
}
/**
* Mark client as patched
* Mark the client as patched
*/
markAsPatched(clientPath) {
const newDomain = this.getNewDomain();
@@ -349,28 +378,29 @@ class ClientPatcher {
patchMode: strategy.mode,
mainDomain: strategy.mainDomain,
subdomainPrefix: strategy.subdomainPrefix,
patcherVersion: '2.1.0',
patcherVersion: '2.0.0',
verified: 'binary_contents'
};
fs.writeFileSync(patchFlagFile, JSON.stringify(flagData, null, 2));
}
/**
* Create backup of original client binary
* Create a backup of the original client binary
*/
backupClient(clientPath) {
const backupPath = clientPath + '.original';
try {
if (!fs.existsSync(backupPath)) {
console.log(` Creating backup at ${path.basename(backupPath)}`);
fs.copyFileSync(clientPath, backupPath);
return backupPath;
}
// Check if current file differs from backup (might have been updated)
const currentSize = fs.statSync(clientPath).size;
const backupSize = fs.statSync(backupPath).size;
if (currentSize !== backupSize) {
// File was updated, create timestamped backup of old backup
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const oldBackupPath = `${clientPath}.original.${timestamp}`;
console.log(` File updated, archiving old backup to ${path.basename(oldBackupPath)}`);
@@ -381,14 +411,10 @@ class ClientPatcher {
console.log(' Backup already exists');
return backupPath;
} catch (e) {
console.error(` Failed to create backup: ${e.message}`);
return null;
}
}
/**
* Restore original client binary
* Restore the original client binary from backup
*/
restoreClient(clientPath) {
const backupPath = clientPath + '.original';
@@ -407,12 +433,15 @@ class ClientPatcher {
/**
* Patch the client binary to use the custom domain
* @param {string} clientPath - Path to the HytaleClient binary
* @param {function} progressCallback - Optional callback for progress updates
* @returns {object} Result object with success status and details
*/
async patchClient(clientPath, progressCallback) {
const newDomain = this.getNewDomain();
const strategy = this.getDomainStrategy(newDomain);
console.log('=== Client Patcher v2.1 ===');
console.log('=== Client Patcher v2.0 ===');
console.log(`Target: ${clientPath}`);
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
console.log(`Mode: ${strategy.mode}`);
@@ -427,34 +456,32 @@ class ClientPatcher {
return { success: false, error };
}
const patchStatus = this.getPatchStatus(clientPath);
if (patchStatus.patched) {
if (this.isPatchedAlready(clientPath)) {
console.log(`Client already patched for ${newDomain}, skipping`);
if (progressCallback) progressCallback('Client already patched', 100);
if (progressCallback) {
progressCallback('Client already patched', 100);
}
return { success: true, alreadyPatched: true, patchCount: 0 };
}
if (patchStatus.needsRestore) {
if (progressCallback) progressCallback('Restoring original for domain change...', 5);
this.restoreFromBackup(clientPath);
if (progressCallback) {
progressCallback('Preparing to patch client...', 10);
}
if (progressCallback) progressCallback('Preparing to patch client...', 10);
console.log('Creating backup...');
const backupResult = this.backupClient(clientPath);
if (!backupResult) {
console.warn(' Could not create backup - proceeding without backup');
}
this.backupClient(clientPath);
if (progressCallback) progressCallback('Reading client binary...', 20);
if (progressCallback) {
progressCallback('Reading client binary...', 20);
}
console.log('Reading client binary...');
const data = fs.readFileSync(clientPath);
console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`);
if (progressCallback) progressCallback('Patching domain references...', 50);
if (progressCallback) {
progressCallback('Patching domain references...', 50);
}
console.log('Applying domain patches (length-prefixed format)...');
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
@@ -465,6 +492,7 @@ class ClientPatcher {
if (count === 0 && discordCount === 0) {
console.log('No occurrences found - trying legacy UTF-16LE format...');
// Fallback to legacy patching for older binary formats
const legacyResult = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, strategy.mainDomain);
if (legacyResult.count > 0) {
console.log(`Found ${legacyResult.count} occurrences with legacy format`);
@@ -477,14 +505,18 @@ class ClientPatcher {
return { success: true, patchCount: 0, warning: 'No occurrences found' };
}
if (progressCallback) progressCallback('Writing patched binary...', 80);
if (progressCallback) {
progressCallback('Writing patched binary...', 80);
}
console.log('Writing patched binary...');
fs.writeFileSync(clientPath, finalData);
this.markAsPatched(clientPath);
if (progressCallback) progressCallback('Patching complete', 100);
if (progressCallback) {
progressCallback('Patching complete', 100);
}
console.log(`Successfully patched ${count} domain occurrences and ${discordCount} Discord URLs`);
console.log('=== Patching Complete ===');
@@ -493,45 +525,16 @@ class ClientPatcher {
}
/**
* Check if server JAR contains DualAuth classes (was patched)
* Patch the server JAR by downloading pre-patched version
* @param {string} serverPath - Path to the HytaleServer.jar
* @param {function} progressCallback - Optional callback for progress updates
* @param {string} javaPath - Path to Java executable (unused, kept for compatibility)
* @returns {object} Result object with success status and details
*/
serverJarContainsDualAuth(serverPath) {
try {
const data = fs.readFileSync(serverPath);
// Check for DualAuthContext class signature in JAR
const signature = Buffer.from('DualAuthContext', 'utf8');
return data.includes(signature);
} catch (e) {
return false;
}
}
/**
* Validate downloaded file is not corrupt/partial
* Server JAR should be at least 50MB
*/
validateServerJarSize(serverPath) {
try {
const stats = fs.statSync(serverPath);
const minSize = 50 * 1024 * 1024; // 50MB minimum
if (stats.size < minSize) {
console.error(` Downloaded JAR too small: ${(stats.size / 1024 / 1024).toFixed(2)} MB (expected >50MB)`);
return false;
}
console.log(` Downloaded size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
return true;
} catch (e) {
return false;
}
}
/**
* Patch server JAR by downloading pre-patched version from CDN
*/
async patchServer(serverPath, progressCallback) {
async patchServer(serverPath, progressCallback, javaPath = null) {
const newDomain = this.getNewDomain();
console.log('=== Server Patcher (Pre-patched Download) ===');
console.log('=== Server Patcher TEMP SYSTEM NEED TO BE FIXED ===');
console.log(`Target: ${serverPath}`);
console.log(`Domain: ${newDomain}`);
@@ -543,82 +546,61 @@ class ClientPatcher {
// Check if already patched
const patchFlagFile = serverPath + '.dualauth_patched';
let needsRestore = false;
if (fs.existsSync(patchFlagFile)) {
try {
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
if (flagData.domain === newDomain) {
// Verify JAR actually contains DualAuth classes (game may have auto-updated)
if (this.serverJarContainsDualAuth(serverPath)) {
console.log(`Server already patched for ${newDomain}, skipping`);
if (progressCallback) progressCallback('Server already patched', 100);
return { success: true, alreadyPatched: true };
} else {
console.log(' Flag exists but JAR not patched (was auto-updated?), will re-download...');
// Delete stale flag file
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
}
} else {
console.log(`Server patched for "${flagData.domain}", need to change to "${newDomain}"`);
needsRestore = true;
}
} catch (e) {
// Flag file corrupt, re-patch
console.log(' Flag file corrupt, will re-download');
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
}
}
// Restore backup if patched for different domain
if (needsRestore) {
const backupPath = serverPath + '.original';
if (fs.existsSync(backupPath)) {
if (progressCallback) progressCallback('Restoring original for domain change...', 5);
console.log('Restoring original JAR from backup for re-patching...');
fs.copyFileSync(backupPath, serverPath);
if (fs.existsSync(patchFlagFile)) {
fs.unlinkSync(patchFlagFile);
}
} else {
console.warn(' No backup found to restore - will download fresh patched JAR');
}
}
// Create backup
if (progressCallback) progressCallback('Creating backup...', 10);
console.log('Creating backup...');
const backupResult = this.backupClient(serverPath);
if (!backupResult) {
console.warn(' Could not create backup - proceeding without backup');
}
// Only support standard domain (auth.sanasol.ws) via pre-patched download
if (newDomain !== 'auth.sanasol.ws' && newDomain !== 'sanasol.ws') {
console.error(`Domain "${newDomain}" requires DualAuthPatcher - only auth.sanasol.ws is supported via pre-patched download`);
return { success: false, error: `Unsupported domain: ${newDomain}. Only auth.sanasol.ws is supported.` };
}
this.backupClient(serverPath);
// Download pre-patched JAR
if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
console.log('Downloading pre-patched HytaleServer.jar...');
console.log('Downloading pre-patched HytaleServer.jar');
try {
const https = require('https');
const url = 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar';
await new Promise((resolve, reject) => {
const handleResponse = (response) => {
https.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
https.get(response.headers.location, handleResponse).on('error', reject);
// Follow redirect
https.get(response.headers.location, (redirectResponse) => {
if (redirectResponse.statusCode !== 200) {
reject(new Error(`Failed to download: HTTP ${redirectResponse.statusCode}`));
return;
}
if (response.statusCode !== 200) {
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
return;
}
const file = fs.createWriteStream(serverPath);
const totalSize = parseInt(redirectResponse.headers['content-length'], 10);
let downloaded = 0;
redirectResponse.on('data', (chunk) => {
downloaded += chunk.length;
if (progressCallback && totalSize) {
const percent = 30 + Math.floor((downloaded / totalSize) * 60);
progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent);
}
});
redirectResponse.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}).on('error', reject);
} else if (response.statusCode === 200) {
const file = fs.createWriteStream(serverPath);
const totalSize = parseInt(response.headers['content-length'], 10);
let downloaded = 0;
@@ -636,9 +618,10 @@ class ClientPatcher {
file.close();
resolve();
});
};
https.get(url, handleResponse).on('error', (err) => {
} else {
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
}
}).on('error', (err) => {
fs.unlink(serverPath, () => {});
reject(err);
});
@@ -646,42 +629,12 @@ class ClientPatcher {
console.log(' Download successful');
// Verify downloaded JAR size and contents
if (progressCallback) progressCallback('Verifying downloaded JAR...', 95);
if (!this.validateServerJarSize(serverPath)) {
console.error('Downloaded JAR appears corrupt or incomplete');
// Restore backup on verification failure
const backupPath = serverPath + '.original';
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, serverPath);
console.log('Restored backup after verification failure');
}
return { success: false, error: 'Downloaded JAR verification failed - file too small (corrupt/partial download)' };
}
if (!this.serverJarContainsDualAuth(serverPath)) {
console.error('Downloaded JAR does not contain DualAuth classes - invalid or corrupt download');
// Restore backup on verification failure
const backupPath = serverPath + '.original';
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, serverPath);
console.log('Restored backup after verification failure');
}
return { success: false, error: 'Downloaded JAR verification failed - missing DualAuth classes' };
}
console.log(' Verification successful - DualAuth classes present');
// Mark as patched
fs.writeFileSync(patchFlagFile, JSON.stringify({
domain: newDomain,
patchedAt: new Date().toISOString(),
patcher: 'PrePatchedDownload',
source: 'https://download.sanasol.ws/download/HytaleServer.jar'
source: 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar'
}));
if (progressCallback) progressCallback('Server patching complete', 100);
@@ -703,7 +656,290 @@ class ClientPatcher {
}
/**
* Find client binary path based on platform
* Find Java executable - uses bundled JRE first (same as game uses)
* Falls back to system Java if bundled not available
*/
findJava() {
// 1. Try bundled JRE first (comes with the game)
try {
const bundled = getBundledJavaPath(JRE_DIR);
if (bundled && fs.existsSync(bundled)) {
console.log(`Using bundled Java: ${bundled}`);
return bundled;
}
} catch (e) {
// Bundled not available
}
// 2. Try javaManager's getJavaExec (handles all fallbacks)
try {
const javaExec = getJavaExec(JRE_DIR);
if (javaExec && fs.existsSync(javaExec)) {
console.log(`Using Java from javaManager: ${javaExec}`);
return javaExec;
}
} catch (e) {
// Not available
}
// 3. Check JAVA_HOME
if (process.env.JAVA_HOME) {
const javaHome = process.env.JAVA_HOME;
const javaBin = path.join(javaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
if (fs.existsSync(javaBin)) {
console.log(`Using Java from JAVA_HOME: ${javaBin}`);
return javaBin;
}
}
// 4. Try 'java' from PATH
try {
execSync('java -version 2>&1', { encoding: 'utf8' });
console.log('Using Java from PATH');
return 'java';
} catch (e) {
// Not in PATH
}
return null;
}
/**
* Download DualAuthPatcher from hytale-auth-server if not present
*/
async ensurePatcherDownloaded(patcherDir) {
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
const patcherUrl = 'https://raw.githubusercontent.com/sanasol/hytale-auth-server/master/patcher/DualAuthPatcher.java';
if (!fs.existsSync(patcherDir)) {
fs.mkdirSync(patcherDir, { recursive: true });
}
if (!fs.existsSync(patcherJava)) {
console.log('Downloading DualAuthPatcher from hytale-auth-server...');
try {
const https = require('https');
await new Promise((resolve, reject) => {
const file = fs.createWriteStream(patcherJava);
https.get(patcherUrl, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
// Follow redirect
https.get(response.headers.location, (redirectResponse) => {
redirectResponse.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}).on('error', reject);
} else {
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}
}).on('error', (err) => {
fs.unlink(patcherJava, () => {});
reject(err);
});
});
console.log(' Downloaded DualAuthPatcher.java');
} catch (e) {
console.error(` Failed to download DualAuthPatcher: ${e.message}`);
throw e;
}
}
}
/**
* Download ASM libraries if not present
*/
async ensureAsmLibraries(libDir) {
if (!fs.existsSync(libDir)) {
fs.mkdirSync(libDir, { recursive: true });
}
const libs = [
{ name: 'asm-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar' },
{ name: 'asm-tree-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar' },
{ name: 'asm-util-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-util/9.6/asm-util-9.6.jar' }
];
for (const lib of libs) {
const libPath = path.join(libDir, lib.name);
if (!fs.existsSync(libPath)) {
console.log(`Downloading ${lib.name}...`);
try {
const https = require('https');
await new Promise((resolve, reject) => {
const file = fs.createWriteStream(libPath);
https.get(lib.url, (response) => {
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}).on('error', (err) => {
fs.unlink(libPath, () => {});
reject(err);
});
});
console.log(` Downloaded ${lib.name}`);
} catch (e) {
console.error(` Failed to download ${lib.name}: ${e.message}`);
throw e;
}
}
}
}
/**
* Compile DualAuthPatcher if needed
*/
async compileDualAuthPatcher(java, patcherDir, libDir) {
const patcherClass = path.join(patcherDir, 'DualAuthPatcher.class');
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
// Check if already compiled and up to date
if (fs.existsSync(patcherClass)) {
const classTime = fs.statSync(patcherClass).mtime;
const javaTime = fs.statSync(patcherJava).mtime;
if (classTime > javaTime) {
console.log('DualAuthPatcher already compiled');
return { success: true };
}
}
console.log('Compiling DualAuthPatcher...');
const javac = java.replace(/java(\.exe)?$/, 'javac$1');
const classpath = [
path.join(libDir, 'asm-9.6.jar'),
path.join(libDir, 'asm-tree-9.6.jar'),
path.join(libDir, 'asm-util-9.6.jar')
].join(process.platform === 'win32' ? ';' : ':');
try {
// Fix PATH for packaged Electron apps on Windows
const execOptions = {
stdio: 'pipe',
cwd: patcherDir,
env: { ...process.env }
};
// Add system32 to PATH for Windows to find cmd.exe
if (process.platform === 'win32') {
const systemRoot = process.env.SystemRoot || 'C:\\WINDOWS';
const systemPath = `${systemRoot}\\system32;${systemRoot};${systemRoot}\\System32\\Wbem`;
execOptions.env.PATH = execOptions.env.PATH
? `${systemPath};${execOptions.env.PATH}`
: systemPath;
execOptions.shell = true;
}
execSync(`"${javac}" -cp "${classpath}" -d "${patcherDir}" "${patcherJava}"`, execOptions);
console.log(' Compilation successful');
return { success: true };
} catch (e) {
const error = `Failed to compile DualAuthPatcher: ${e.message}`;
console.error(error);
if (e.stderr) console.error(e.stderr.toString());
return { success: false, error };
}
}
/**
* Run DualAuthPatcher on the server JAR
*/
async runDualAuthPatcher(java, classpath, serverPath, domain) {
return new Promise((resolve) => {
const args = ['-cp', classpath, 'DualAuthPatcher', serverPath];
const env = { ...process.env, HYTALE_AUTH_DOMAIN: domain };
console.log(`Running: java ${args.join(' ')}`);
console.log(` HYTALE_AUTH_DOMAIN=${domain}`);
const proc = spawn(java, args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data) => {
const str = data.toString();
stdout += str;
console.log(str.trim());
});
proc.stderr.on('data', (data) => {
const str = data.toString();
stderr += str;
console.error(str.trim());
});
proc.on('close', (code) => {
if (code === 0) {
resolve({ success: true, stdout });
} else {
resolve({ success: false, error: `Patcher exited with code ${code}: ${stderr}` });
}
});
proc.on('error', (err) => {
resolve({ success: false, error: `Failed to run patcher: ${err.message}` });
});
});
}
/**
* Legacy server patcher (simple domain replacement, no dual auth)
* Use patchServer() for full dual auth support
*/
async patchServerLegacy(serverPath, progressCallback) {
const newDomain = this.getNewDomain();
const strategy = this.getDomainStrategy(newDomain);
console.log('=== Legacy Server Patcher ===');
console.log(`Target: ${serverPath}`);
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
if (!fs.existsSync(serverPath)) {
return { success: false, error: `Server JAR not found: ${serverPath}` };
}
if (progressCallback) progressCallback('Patching server...', 20);
console.log('Opening server JAR...');
const zip = new AdmZip(serverPath);
const entries = zip.getEntries();
let totalCount = 0;
const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN);
for (const entry of entries) {
const name = entry.entryName;
if (name.endsWith('.class') || name.endsWith('.properties') ||
name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) {
const data = entry.getData();
if (data.includes(oldUtf8)) {
const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, strategy.mainDomain);
if (count > 0) {
zip.updateFile(entry.entryName, patchedData);
totalCount += count;
}
}
}
}
if (totalCount > 0) {
zip.writeZip(serverPath);
}
if (progressCallback) progressCallback('Complete', 100);
return { success: true, patchCount: totalCount };
}
/**
* Find the client binary path based on platform
*/
findClientPath(gameDir) {
const candidates = [];
@@ -725,9 +961,7 @@ class ClientPatcher {
return null;
}
/**
* Find server JAR path
*/
findServerPath(gameDir) {
const candidates = [
path.join(gameDir, 'Server', 'HytaleServer.jar'),
@@ -744,6 +978,9 @@ class ClientPatcher {
/**
* Ensure both client and server are patched before launching
* @param {string} gameDir - Path to the game directory
* @param {function} progressCallback - Optional callback for progress updates
* @param {string} javaPath - Optional path to Java executable for server patching
*/
async ensureClientPatched(gameDir, progressCallback, javaPath = null) {
const results = {
@@ -754,7 +991,9 @@ class ClientPatcher {
const clientPath = this.findClientPath(gameDir);
if (clientPath) {
if (progressCallback) progressCallback('Patching client binary...', 10);
if (progressCallback) {
progressCallback('Patching client binary...', 10);
}
results.client = await this.patchClient(clientPath, (msg, pct) => {
if (progressCallback) {
progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
@@ -767,12 +1006,14 @@ class ClientPatcher {
const serverPath = this.findServerPath(gameDir);
if (serverPath) {
if (progressCallback) progressCallback('Patching server JAR...', 50);
if (progressCallback) {
progressCallback('Patching server JAR...', 50);
}
results.server = await this.patchServer(serverPath, (msg, pct) => {
if (progressCallback) {
progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
}
});
}, javaPath);
} else {
console.warn('Could not find HytaleServer.jar');
results.server = { success: false, error: 'Server JAR not found' };
@@ -782,7 +1023,9 @@ class ClientPatcher {
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched);
results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0);
if (progressCallback) progressCallback('Patching complete', 100);
if (progressCallback) {
progressCallback('Patching complete', 100);
}
return results;
}

View File

@@ -46,8 +46,7 @@ class UserDataBackup {
console.log(`[UserDataBackup] Copying from ${userDataPath} to ${backupPath}...`);
await fs.copy(userDataPath, backupPath, {
overwrite: true,
errorOnExist: false,
dereference: true // Follow symlinks to avoid EPERM errors on Windows
errorOnExist: false
});
console.log('[UserDataBackup] ✓ Backup completed successfully');
return backupPath;
@@ -83,8 +82,7 @@ class UserDataBackup {
await fs.copy(backupPath, userDataPath, {
overwrite: true,
errorOnExist: false,
dereference: true // Follow symlinks to avoid EPERM errors on Windows
errorOnExist: false
});
console.log('UserData restore completed successfully');

View File

@@ -1,172 +0,0 @@
const fs = require('fs-extra');
const path = require('path');
const { getHytaleSavesDir, getResolvedAppDir } = require('../core/paths');
const { loadConfig, saveConfig } = require('../core/config');
/**
* NEW SYSTEM (2.1.2+): UserData Migration to Centralized Location
*
* UserData is now stored in a centralized location instead of inside game installation:
* - Windows: %LOCALAPPDATA%\HytaleSaves\
* - macOS: ~/Library/Application Support/HytaleSaves/
* - Linux: ~/.hytalesaves/
*
* This eliminates the need for backup/restore during updates.
*/
/**
* Check if migration to centralized UserData has been completed
*/
function isMigrationCompleted() {
const config = loadConfig();
return config.userDataMigrated === true;
}
/**
* Mark migration as completed
*/
function markMigrationCompleted() {
saveConfig({ userDataMigrated: true });
console.log('[UserDataMigration] Migration marked as completed in config');
}
/**
* Find old UserData location (pre-2.1.2)
* Searches in: installPath/branch/package/game/latest/Client/UserData
*/
function findOldUserDataPath() {
try {
const config = loadConfig();
const installPath = getResolvedAppDir();
const branch = config.version_branch || 'release';
console.log(`[UserDataMigration] Looking for old UserData...`);
console.log(`[UserDataMigration] Install path: ${installPath}`);
console.log(`[UserDataMigration] Branch: ${branch}`);
// Old location
const oldPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData');
console.log(`[UserDataMigration] Checking: ${oldPath}`);
console.log(`[UserDataMigration] Checking: ${oldPath}`);
if (fs.existsSync(oldPath)) {
console.log(`[UserDataMigration] ✓ Found old UserData at: ${oldPath}`);
return oldPath;
}
console.log(`[UserDataMigration] ✗ Not found at current branch location`);
// Try other branch if current doesn't exist
const otherBranch = branch === 'release' ? 'pre-release' : 'release';
const otherPath = path.join(installPath, otherBranch, 'package', 'game', 'latest', 'Client', 'UserData');
console.log(`[UserDataMigration] Checking other branch: ${otherPath}`);
console.log(`[UserDataMigration] Checking other branch: ${otherPath}`);
if (fs.existsSync(otherPath)) {
console.log(`[UserDataMigration] ✓ Found old UserData in other branch at: ${otherPath}`);
return otherPath;
}
console.log('[UserDataMigration] ✗ No old UserData found in any branch');
return null;
} catch (error) {
console.error('[UserDataMigration] Error finding old UserData:', error);
return null;
}
}
/**
* Migrate UserData from old location to new centralized location
* One-time operation when upgrading to 2.1.2
*/
async function migrateUserDataToCentralized() {
// Check if already migrated
if (isMigrationCompleted()) {
console.log('[UserDataMigration] Migration already completed, skipping');
return { success: true, alreadyMigrated: true };
}
console.log('[UserDataMigration] === Starting UserData Migration to Centralized Location ===');
const newUserDataPath = getHytaleSavesDir();
console.log(`[UserDataMigration] Target location: ${newUserDataPath}`);
// Ensure new directory exists
if (!fs.existsSync(newUserDataPath)) {
fs.mkdirSync(newUserDataPath, { recursive: true });
console.log('[UserDataMigration] Created new HytaleSaves directory');
}
// Find old UserData
const oldUserDataPath = findOldUserDataPath();
if (!oldUserDataPath) {
console.log('[UserDataMigration] No old UserData found - fresh install or already migrated');
// Don't mark as migrated - let it check again next time in case game gets installed later
return { success: true, freshInstall: true };
}
// Check if new location already has data (shouldn't happen, but safety check)
const existingFiles = fs.readdirSync(newUserDataPath);
if (existingFiles.length > 0) {
console.warn('[UserDataMigration] New location already contains files, marking as migrated to avoid re-attempts');
markMigrationCompleted();
return { success: true, skipped: true, reason: 'target_not_empty' };
}
try {
console.log(`[UserDataMigration] Copying from ${oldUserDataPath} to ${newUserDataPath}...`);
// Copy all UserData to new location
await fs.copy(oldUserDataPath, newUserDataPath, {
overwrite: false,
errorOnExist: false,
dereference: true // Follow symlinks to avoid EPERM errors on Windows
});
console.log('[UserDataMigration] ✓ UserData copied successfully');
// Mark migration as completed
markMigrationCompleted();
console.log('[UserDataMigration] === Migration Completed Successfully ===');
return {
success: true,
migrated: true,
from: oldUserDataPath,
to: newUserDataPath
};
} catch (error) {
console.error('[UserDataMigration] ✗ Migration failed:', error);
return {
success: false,
error: error.message,
from: oldUserDataPath,
to: newUserDataPath
};
}
}
/**
* Get the centralized UserData path (always use this in 2.1.2+)
* Ensures directory exists
*/
function getUserDataPath() {
const userDataPath = getHytaleSavesDir();
// Ensure directory exists
if (!fs.existsSync(userDataPath)) {
fs.mkdirSync(userDataPath, { recursive: true });
console.log(`[UserDataMigration] Created UserData directory: ${userDataPath}`);
}
return userDataPath;
}
module.exports = {
migrateUserDataToCentralized,
getUserDataPath,
isMigrationCompleted,
findOldUserDataPath
};

View File

@@ -1,123 +0,0 @@
# Steam Deck / Ubuntu LTS Crash Investigation
## Status: SOLVED
**Last updated:** 2026-01-27
**Solution:** Replace bundled `libzstd.so` with system version.
---
## Problem Summary
The Hytale F2P launcher's client patcher causes crashes on Steam Deck and Ubuntu LTS with the error:
```
free(): invalid pointer
```
or
```
SIGSEGV (Segmentation fault)
```
The crash occurs after successful authentication, specifically right after "Finished handling RequiredAssets".
**Affected Systems:**
- Steam Deck (glibc 2.41)
- Ubuntu LTS
**Working Systems:**
- macOS
- Windows
- Older Arch Linux (glibc < 2.41)
---
## Root Cause
The **bundled `libzstd.so`** in the game client is incompatible with glibc 2.41's stricter heap validation. When the game decompresses assets using this library, it triggers heap corruption detected by glibc 2.41.
The crash occurs in `libzstd.so` during `free()` after "Finished handling RequiredAssets" (asset decompression).
---
## Solution
Replace the bundled `libzstd.so` with the system's `libzstd.so.1`.
### Automatic (Launcher)
The launcher automatically detects and replaces `libzstd.so` on Linux systems. No manual action needed.
### Manual
```bash
cd ~/.hytalef2p/release/package/game/latest/Client
# Backup bundled version
mv libzstd.so libzstd.so.bundled
# Link to system version
# Steam Deck / Arch Linux:
ln -s /usr/lib/libzstd.so.1 libzstd.so
# Debian / Ubuntu:
ln -s /usr/lib/x86_64-linux-gnu/libzstd.so.1 libzstd.so
# Fedora / RHEL:
ln -s /usr/lib64/libzstd.so.1 libzstd.so
```
### Restore Original
```bash
cd ~/.hytalef2p/release/package/game/latest/Client
rm libzstd.so
mv libzstd.so.bundled libzstd.so
```
---
## Why This Works
1. The bundled `libzstd.so` was likely compiled with different allocator settings or an older toolchain
2. glibc 2.41 has stricter heap validation that catches invalid memory operations
3. The system `libzstd.so.1` is compiled with the system's glibc and uses compatible memory allocation patterns
4. By using the system library, we avoid the incompatibility entirely
---
## Previous Investigation (for reference)
### What Was Tried Before Finding Solution
| Approach | Result |
|----------|--------|
| jemalloc allocator | Worked ~30% of time, not stable |
| GLIBC_TUNABLES | No effect |
| taskset (CPU pinning) | Single core too slow |
| nice/chrt (scheduling) | No effect |
| Various patching approaches | All crashed |
### Key Insight
The crash was in `libzstd.so`, not in our patched code. The patching just changed timing enough to expose the libzstd incompatibility more frequently.
---
## GDB Stack Trace (Historical)
```
#0 0x00007ffff7d3f5a4 in ?? () from /usr/lib/libc.so.6
#1 raise () from /usr/lib/libc.so.6
#2 abort () from /usr/lib/libc.so.6
#3-#4 ?? () from /usr/lib/libc.so.6
#5 free () from /usr/lib/libc.so.6
#6 ?? () from libzstd.so <-- CRASH POINT (bundled library)
#7-#24 HytaleClient code (asset decompression)
```
---
## Branch
`fix/steamdeck-libzstd`

View File

@@ -1,65 +0,0 @@
# Steam Deck / Linux Crash Fix
## SOLUTION: Use system libzstd
The crash is caused by the bundled `libzstd.so` being incompatible with glibc 2.41's stricter heap validation.
### Automatic Fix
The launcher automatically replaces `libzstd.so` with the system version. No manual action needed.
### Manual Fix
```bash
cd ~/.hytalef2p/release/package/game/latest/Client
# Backup and replace
mv libzstd.so libzstd.so.bundled
ln -s /usr/lib/libzstd.so.1 libzstd.so
```
### Restore Original
```bash
cd ~/.hytalef2p/release/package/game/latest/Client
rm libzstd.so
mv libzstd.so.bundled libzstd.so
```
---
## Debug Commands (for troubleshooting)
### Check libzstd Status
```bash
# Check if symlinked
ls -la ~/.hytalef2p/release/package/game/latest/Client/libzstd.so
# Find system libzstd
find /usr/lib -name "libzstd.so*"
```
### Binary Validation
```bash
file ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
ldd ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
```
### Restore Client Binary
```bash
cd ~/.hytalef2p/release/package/game/latest/Client
cp HytaleClient.original HytaleClient
rm -f HytaleClient.patched_custom
```
---
## Environment Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `HYTALE_AUTH_DOMAIN` | Custom auth domain | `auth.sanasol.ws` |
| `HYTALE_NO_LIBZSTD_FIX` | Disable libzstd replacement | `1` |

73
main.js
View File

@@ -5,7 +5,6 @@ const { autoUpdater } = require('electron-updater');
const fs = require('fs');
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
const { retryPWRDownload } = require('./backend/managers/gameManager');
const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration');
// Handle Hardware Acceleration
try {
@@ -176,8 +175,7 @@ function createWindow() {
initDiscordRPC();
// Configure and initialize electron-updater
// Enable auto-download so updates start immediately when available
autoUpdater.autoDownload = true;
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.on('checking-for-update', () => {
@@ -202,20 +200,6 @@ function createWindow() {
autoUpdater.on('error', (err) => {
console.error('Error in auto-updater:', err);
// Handle macOS code signing errors - requires manual download
if (mainWindow && !mainWindow.isDestroyed()) {
const isMacSigningError = process.platform === 'darwin' &&
(err.code === 'ERR_UPDATER_INVALID_SIGNATURE' ||
err.message.includes('signature') ||
err.message.includes('code sign'));
mainWindow.webContents.send('update-error', {
message: err.message,
isMacSigningError: isMacSigningError,
requiresManualDownload: isMacSigningError || process.platform === 'darwin'
});
}
});
autoUpdater.on('download-progress', (progressObj) => {
@@ -233,10 +217,7 @@ function createWindow() {
console.log('Update downloaded:', info.version);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-downloaded', {
version: info.version,
platform: process.platform,
// macOS auto-install often fails on unsigned apps
autoInstallSupported: process.platform !== 'darwin'
version: info.version
});
}
});
@@ -317,14 +298,6 @@ app.whenReady().then(async () => {
// Initialize Profile Manager (runs migration if needed)
profileManager.init();
// Migrate UserData to centralized location (v2.1.2+)
console.log('[Startup] Checking UserData migration...');
try {
await migrateUserDataToCentralized();
} catch (error) {
console.error('[Startup] UserData migration failed:', error);
}
createSplashScreen();
setTimeout(async () => {
@@ -877,17 +850,6 @@ ipcMain.handle('open-external', async (event, url) => {
}
});
ipcMain.handle('open-download-page', async () => {
try {
// Open GitHub releases page for manual download
await shell.openExternal('https://github.com/amiayweb/Hytale-F2P/releases/latest');
return { success: true };
} catch (error) {
console.error('Failed to open download page:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('open-game-location', async () => {
try {
const { getResolvedAppDir, loadVersionBranch } = require('./backend/launcher');
@@ -1115,37 +1077,8 @@ ipcMain.handle('download-update', async () => {
}
});
ipcMain.handle('install-update', async () => {
console.log('[AutoUpdater] Installing update...');
// On macOS, quitAndInstall often fails silently
// Use a more aggressive approach
if (process.platform === 'darwin') {
console.log('[AutoUpdater] macOS detected, using force quit approach');
// Give user feedback that something is happening
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-installing');
}
// Small delay to show the "Installing..." state
await new Promise(resolve => setTimeout(resolve, 500));
try {
ipcMain.handle('install-update', () => {
autoUpdater.quitAndInstall(false, true);
} catch (err) {
console.error('[AutoUpdater] quitAndInstall failed:', err);
// Force quit the app - the update should install on next launch
app.exit(0);
}
// If quitAndInstall didn't work, force exit after a delay
setTimeout(() => {
console.log('[AutoUpdater] Force exiting app...');
app.exit(0);
}, 2000);
} else {
autoUpdater.quitAndInstall(false, true);
}
});
ipcMain.handle('get-launcher-version', () => {

View File

@@ -1,6 +1,6 @@
{
"name": "hytale-f2p-launcher",
"version": "2.1.2",
"version": "2.1.1",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
"homepage": "https://github.com/amiayweb/Hytale-F2P",
"main": "main.js",

View File

@@ -24,7 +24,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
saveCloseLauncher: (enabled) => ipcRenderer.invoke('save-close-launcher', enabled),
loadCloseLauncher: () => ipcRenderer.invoke('load-close-launcher'),
// Hardware Acceleration
// Harwadre Acceleration
saveLauncherHardwareAcceleration: (enabled) => ipcRenderer.invoke('save-launcher-hw-accel', enabled),
loadLauncherHardwareAcceleration: () => ipcRenderer.invoke('load-launcher-hw-accel'),
@@ -50,7 +50,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
selectModFiles: () => ipcRenderer.invoke('select-mod-files'),
copyModFile: (sourcePath, modsPath) => ipcRenderer.invoke('copy-mod-file', sourcePath, modsPath),
onProgressUpdate: (callback) => {
ipcRenderer.on('progress-update', (event, data) => callback(data));
ipcRenderer.on('progress-update', (event, data) => {
// Ensure data includes retry state if available
if (data && typeof data === 'object') {
callback(data);
} else {
callback(data);
}
});
},
onProgressComplete: (callback) => {
ipcRenderer.on('progress-complete', () => callback());
@@ -62,6 +69,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.on('installation-end', () => callback());
},
getUserId: () => ipcRenderer.invoke('get-user-id'),
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
getUpdateInfo: () => ipcRenderer.invoke('get-update-info'),
onUpdatePopup: (callback) => {
@@ -118,7 +126,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
downloadUpdate: () => ipcRenderer.invoke('download-update'),
installUpdate: () => ipcRenderer.invoke('install-update'),
quitAndInstallUpdate: () => ipcRenderer.invoke('install-update'), // Alias for update.js compatibility
getLauncherVersion: () => ipcRenderer.invoke('get-launcher-version'),
onUpdateAvailable: (callback) => {
ipcRenderer.on('update-available', (event, data) => callback(data));