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
15 changed files with 602 additions and 1572 deletions

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

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

@@ -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;
@@ -24,57 +23,6 @@ try {
const execAsync = promisify(exec);
/**
* Try to auto-install jemalloc on Linux using pkexec (graphical sudo)
* Returns true if installation was successful
*/
async function tryInstallJemalloc() {
console.log('Linux: Attempting to auto-install jemalloc...');
// Detect package manager and get install command
let installCmd = null;
try {
await execAsync('which pacman');
installCmd = 'pacman -S --noconfirm jemalloc';
} catch (e) {
try {
await execAsync('which apt');
installCmd = 'apt install -y libjemalloc2';
} catch (e2) {
try {
await execAsync('which dnf');
installCmd = 'dnf install -y jemalloc';
} catch (e3) {
console.log('Linux: Could not detect package manager for auto-install');
return false;
}
}
}
// Try pkexec first (graphical sudo), fall back to sudo
const sudoCommands = ['pkexec', 'sudo'];
for (const sudoCmd of sudoCommands) {
try {
await execAsync(`which ${sudoCmd}`);
console.log(`Linux: Installing jemalloc with: ${sudoCmd} ${installCmd}`);
await execAsync(`${sudoCmd} ${installCmd}`, { timeout: 120000 });
console.log('Linux: jemalloc installed successfully');
return true;
} catch (e) {
if (e.killed) {
console.log('Linux: Install timed out');
} else if (e.code === 126 || e.code === 127) {
continue;
} else {
console.log(`Linux: Install failed with ${sudoCmd}: ${e.message}`);
}
}
}
console.log('Linux: Auto-install failed, manual installation required');
return false;
}
// Fetch tokens from the auth server (properly signed with server's Ed25519 key)
async function fetchAuthTokens(uuid, name) {
const authServerUrl = getAuthServerUrl();
@@ -158,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);
@@ -336,64 +282,6 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
const gpuEnv = setupGpuEnvironment(gpuPreference);
Object.assign(env, gpuEnv);
// Linux: Use jemalloc to fix "free(): invalid pointer" crash on glibc 2.41+ (Steam Deck, Ubuntu LTS)
// Root cause: glibc 2.41 has stricter heap validation that catches a pre-existing race condition
if (process.platform === 'linux') {
if (process.env.HYTALE_NO_JEMALLOC !== '1') {
const jemallocPaths = [
'/usr/lib/libjemalloc.so.2', // Arch Linux, Steam Deck
'/usr/lib/x86_64-linux-gnu/libjemalloc.so.2', // Debian/Ubuntu
'/usr/lib64/libjemalloc.so.2', // Fedora/RHEL
'/usr/lib/libjemalloc.so', // Generic fallback
'/usr/lib/x86_64-linux-gnu/libjemalloc.so', // Debian/Ubuntu fallback
'/usr/lib64/libjemalloc.so' // Fedora/RHEL fallback
];
let jemalloc = null;
for (const p of jemallocPaths) {
if (fs.existsSync(p)) {
jemalloc = p;
break;
}
}
if (jemalloc) {
env.LD_PRELOAD = jemalloc + (env.LD_PRELOAD ? ':' + env.LD_PRELOAD : '');
console.log(`Linux: Using jemalloc allocator for stability (${jemalloc})`);
} else {
// Try auto-install
if (process.env.HYTALE_AUTO_INSTALL_JEMALLOC !== '0') {
const installed = await tryInstallJemalloc();
if (installed) {
for (const p of jemallocPaths) {
if (fs.existsSync(p)) {
jemalloc = p;
break;
}
}
if (jemalloc) {
env.LD_PRELOAD = jemalloc + (env.LD_PRELOAD ? ':' + env.LD_PRELOAD : '');
console.log(`Linux: Using jemalloc after auto-install (${jemalloc})`);
}
}
}
if (!jemalloc) {
env.MALLOC_CHECK_ = '0';
console.log('Linux: jemalloc not found - install with: sudo pacman -S jemalloc (Arch) or sudo apt install libjemalloc2 (Debian/Ubuntu)');
console.log('Linux: Using fallback MALLOC_CHECK_=0 (may still crash on glibc 2.41+)');
}
}
} else {
console.log('Linux: jemalloc disabled by HYTALE_NO_JEMALLOC=1');
}
}
// Debug: log LD_PRELOAD before spawn
if (process.platform === 'linux') {
console.log(`Linux: LD_PRELOAD = ${env.LD_PRELOAD || '(not set)'}`);
}
try {
let spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'],
@@ -406,19 +294,7 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
spawnOptions.windowsHide = true;
}
let child;
// Linux: Use shell with inline LD_PRELOAD for maximum compatibility
if (process.platform === 'linux' && env.LD_PRELOAD) {
const quotedArgs = args.map(a => `"${a.replace(/"/g, '\\"')}"`).join(' ');
const shellCmd = `LD_PRELOAD="${env.LD_PRELOAD}" "${clientPath}" ${quotedArgs}`;
console.log(`Linux: Launching via shell with LD_PRELOAD`);
spawnOptions.shell = '/bin/bash';
child = spawn(shellCmd, [], spawnOptions);
} else {
child = spawn(clientPath, args, spawnOptions);
}
const child = spawn(clientPath, args, spawnOptions);
console.log(`Game process started with PID: ${child.pid}`);

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
if (progressCallback) {
progressCallback('Backing up user data...', 5, null, null, null);
}
// Backup UserData AVANT de télécharger/installer (critical for same-branch updates)
try {
console.log('[UpdateGameFiles] 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');
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 (migrationError) {
console.warn('[UpdateGameFiles] UserData migration warning:', migrationError.message);
} 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');
// 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);
}
// 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');
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 (migrationError) {
console.warn('[InstallGame] UserData migration warning:', migrationError.message);
} 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

@@ -65,13 +65,15 @@ class ClientPatcher {
*/
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 {
@@ -86,16 +88,20 @@ 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);
@@ -142,7 +148,7 @@ class ClientPatcher {
/**
* Replace bytes in buffer - only overwrites the length of new bytes
* Does NOT null-pad to avoid corrupting adjacent data
* Prevents offset corruption by not expanding the replacement
*/
replaceBytes(buffer, oldBytes, newBytes) {
let count = 0;
@@ -156,7 +162,7 @@ class ClientPatcher {
const positions = this.findAllOccurrences(result, oldBytes);
for (const pos of positions) {
// Only overwrite the length of the new bytes - don't null-fill!
// Only overwrite the length of the new bytes
newBytes.copy(result, pos);
count++;
}
@@ -165,7 +171,8 @@ class ClientPatcher {
}
/**
* UTF-8 domain replacement for Java JAR files
* 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;
@@ -179,23 +186,22 @@ class ClientPatcher {
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 for .NET AOT binaries
* 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;
const result = Buffer.from(data);
if (newDomain.length > oldDomain.length) {
console.warn(` Warning: New domain longer than old, skipping smart replacement`);
return { buffer: result, count: 0 };
}
const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1));
const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1));
@@ -211,9 +217,18 @@ class ClientPatcher {
const lastCharFirstByte = result[lastCharPos];
if (lastCharFirstByte === oldLastCharByte) {
// Only overwrite, don't null-fill
newUtf16NoLast.copy(result, pos);
result[lastCharPos] = newLastCharByte;
if (lastCharPos + 1 < result.length) {
const secondByte = result[lastCharPos + 1];
if (secondByte === 0x00) {
console.log(` Patched UTF-16LE occurrence at offset 0x${pos.toString(16)}`);
} else {
console.log(` Patched length-prefixed occurrence at offset 0x${pos.toString(16)} (metadata: 0x${secondByte.toString(16)})`);
}
}
count++;
}
}
@@ -223,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);
@@ -231,34 +247,100 @@ class ClientPatcher {
console.log(` Patching strategy: ${strategy.description}`);
// ULTRA-MINIMAL PATCHING - only domain, no subdomain patches
console.log(` Ultra-minimal mode: only patching main domain`);
// 1. Patch telemetry/sentry URL
const oldSentry = 'https://ca900df42fcf57d4dd8401a86ddd7da2@sentry.hytale.com/2';
const newSentry = `${protocol}t@${domain}/2`;
// Only patch main domain (hytale.com -> mainDomain)
console.log(` Patching sentry: ${oldSentry.slice(0, 30)}... -> ${newSentry}`);
const sentryResult = this.replaceBytes(
result,
this.stringToLengthPrefixed(oldSentry),
this.stringToLengthPrefixed(newSentry)
);
result = sentryResult.buffer;
if (sentryResult.count > 0) {
console.log(` Replaced ${sentryResult.count} sentry occurrence(s)`);
totalCount += sentryResult.count;
}
// 2. Patch main domain (hytale.com -> mainDomain)
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
const domainResult = this.replaceBytes(
result,
this.stringToLengthPrefixed(ORIGINAL_DOMAIN),
this.stringToLengthPrefixed(strategy.mainDomain)
result,
this.stringToLengthPrefixed(ORIGINAL_DOMAIN),
this.stringToLengthPrefixed(strategy.mainDomain)
);
result = domainResult.buffer;
if (domainResult.count > 0) {
console.log(` Patched ${domainResult.count} domain occurrence(s)`);
console.log(` Replaced ${domainResult.count} domain occurrence(s)`);
totalCount += domainResult.count;
}
// Skip ALL subdomain patches - let them stay as sessions.hytale.com etc
console.log(` Skipping all subdomain patches (ultra-minimal mode)`);
// 3. Patch subdomain prefixes
const subdomains = ['https://tools.', 'https://sessions.', 'https://account-data.', 'https://telemetry.'];
const newSubdomainPrefix = protocol + strategy.subdomainPrefix;
for (const sub of subdomains) {
console.log(` Patching subdomain: ${sub} -> ${newSubdomainPrefix}`);
const subResult = this.replaceBytes(
result,
this.stringToLengthPrefixed(sub),
this.stringToLengthPrefixed(newSubdomainPrefix)
);
result = subResult.buffer;
if (subResult.count > 0) {
console.log(` Replaced ${subResult.count} occurrence(s)`);
totalCount += subResult.count;
}
}
return { buffer: result, count: totalCount };
}
/**
* Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7
*/
patchDiscordUrl(data) {
let count = 0;
const result = Buffer.from(data);
const oldUrl = '.gg/hytale';
const newUrl = '.gg/MHkEjepMQ7';
// Try length-prefixed format first
const lpResult = this.replaceBytes(
result,
this.stringToLengthPrefixed(oldUrl),
this.stringToLengthPrefixed(newUrl)
);
if (lpResult.count > 0) {
return { buffer: lpResult.buffer, count: lpResult.count };
}
// Fallback to UTF-16LE
const oldUtf16 = this.stringToUtf16LE(oldUrl);
const newUtf16 = this.stringToUtf16LE(newUrl);
const positions = this.findAllOccurrences(result, oldUtf16);
for (const pos of positions) {
newUtf16.copy(result, pos);
count++;
}
return { buffer: result, count };
}
/**
* Check if the client binary has already been patched
* Also verifies the binary actually contains the patched domain
*/
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'));
@@ -276,7 +358,7 @@ class ClientPatcher {
}
}
} catch (e) {
// Flag file corrupt
// Flag file corrupt or unreadable
}
}
return false;
@@ -296,7 +378,8 @@ 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));
}
@@ -312,10 +395,12 @@ class ClientPatcher {
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)}`);
@@ -348,15 +433,22 @@ 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}`);
if (strategy.mode === 'split') {
console.log(` Subdomain prefix: ${strategy.subdomainPrefix}`);
console.log(` Main domain: ${strategy.mainDomain}`);
}
if (!fs.existsSync(clientPath)) {
const error = `Client binary not found: ${clientPath}`;
@@ -366,29 +458,41 @@ class ClientPatcher {
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 (progressCallback) progressCallback('Preparing to patch client...', 10);
if (progressCallback) {
progressCallback('Preparing to patch client...', 10);
}
console.log('Creating 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...');
console.log('Applying domain patches (length-prefixed format)...');
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
if (count === 0) {
// Try legacy UTF-16LE format
console.log('Patching Discord URLs...');
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
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`);
@@ -397,31 +501,40 @@ class ClientPatcher {
return { success: true, patchCount: legacyResult.count, format: 'legacy' };
}
console.log('No occurrences found - binary may already be modified');
console.log('No occurrences found - binary may already be modified or has different format');
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, patchedData);
fs.writeFileSync(clientPath, finalData);
this.markAsPatched(clientPath);
if (progressCallback) progressCallback('Patching complete', 100);
if (progressCallback) {
progressCallback('Patching complete', 100);
}
console.log(`Successfully patched ${count} occurrences`);
console.log(`Successfully patched ${count} domain occurrences and ${discordCount} Discord URLs`);
console.log('=== Patching Complete ===');
return { success: true, patchCount: count };
return { success: true, patchCount: count + discordCount };
}
/**
* 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
*/
async patchServer(serverPath, progressCallback, javaPath = null) {
const newDomain = this.getNewDomain();
console.log('=== Server Patcher ===');
console.log('=== Server Patcher TEMP SYSTEM NEED TO BE FIXED ===');
console.log(`Target: ${serverPath}`);
console.log(`Domain: ${newDomain}`);
@@ -431,6 +544,7 @@ class ClientPatcher {
return { success: false, error };
}
// Check if already patched
const patchFlagFile = serverPath + '.dualauth_patched';
if (fs.existsSync(patchFlagFile)) {
try {
@@ -441,14 +555,16 @@ class ClientPatcher {
return { success: true, alreadyPatched: true };
}
} catch (e) {
// Re-patch
// Flag file corrupt, re-patch
}
}
// Create backup
if (progressCallback) progressCallback('Creating backup...', 10);
console.log('Creating backup...');
this.backupClient(serverPath);
// Download pre-patched JAR
if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
console.log('Downloading pre-patched HytaleServer.jar');
@@ -457,37 +573,55 @@ class ClientPatcher {
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);
return;
// Follow redirect
https.get(response.headers.location, (redirectResponse) => {
if (redirectResponse.statusCode !== 200) {
reject(new Error(`Failed to download: HTTP ${redirectResponse.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;
response.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);
}
});
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
} else {
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
}
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}`));
return;
}
const file = fs.createWriteStream(serverPath);
const totalSize = parseInt(response.headers['content-length'], 10);
let downloaded = 0;
response.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);
}
});
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
};
https.get(url, handleResponse).on('error', (err) => {
}).on('error', (err) => {
fs.unlink(serverPath, () => {});
reject(err);
});
@@ -495,11 +629,12 @@ class ClientPatcher {
console.log(' Download successful');
// Mark as patched
fs.writeFileSync(patchFlagFile, JSON.stringify({
domain: newDomain,
patchedAt: new Date().toISOString(),
patcher: 'PrePatchedDownload',
source: url
source: 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar'
}));
if (progressCallback) progressCallback('Server patching complete', 100);
@@ -508,7 +643,8 @@ class ClientPatcher {
} catch (downloadError) {
console.error(`Failed to download patched JAR: ${downloadError.message}`);
// Restore backup on failure
const backupPath = serverPath + '.original';
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, serverPath);
@@ -520,38 +656,288 @@ class ClientPatcher {
}
/**
* Find Java executable
* 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) {}
} 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) {}
} catch (e) {
// Not available
}
// 3. Check JAVA_HOME
if (process.env.JAVA_HOME) {
const javaBin = path.join(process.env.JAVA_HOME, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
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) {}
} 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
*/
@@ -575,6 +961,7 @@ class ClientPatcher {
return null;
}
findServerPath(gameDir) {
const candidates = [
path.join(gameDir, 'Server', 'HytaleServer.jar'),
@@ -591,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 = {
@@ -601,9 +991,13 @@ 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);
if (progressCallback) {
progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
}
});
} else {
console.warn('Could not find HytaleClient binary');
@@ -612,9 +1006,13 @@ 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);
if (progressCallback) {
progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
}
}, javaPath);
} else {
console.warn('Could not find HytaleServer.jar');
@@ -625,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,231 +0,0 @@
# Steam Deck / Ubuntu LTS Crash Investigation
## Status: UNSOLVED
**Last updated:** 2026-01-27
No stable solution found. jemalloc helps occasionally but crashes still occur randomly.
---
## 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)
**Critical Finding:** The UNPATCHED original binary works fine on Steam Deck. The crash is caused by ANY binary patching.
---
## What Was Tried (All Failed)
### Memory Allocators
| Approach | Result |
|----------|--------|
| `LD_PRELOAD=/usr/lib/libjemalloc.so.2` | Works randomly (3/10 times), not stable |
| `MALLOC_CHECK_=0` | No effect |
| `MALLOC_PERTURB_=255` | No effect |
| `GLIBC_TUNABLES=glibc.malloc.tcache_count=0` | No effect |
### Process/Scheduling
| Approach | Result |
|----------|--------|
| `taskset -c 0` (single core) | Game too slow, stuck at connecting |
| `taskset -c 0,1` or `0-3` | Still crashes |
| `nice -n 19` | No effect |
| `chrt --idle 0` | No effect |
| `strace -f` | No effect |
### Linker/Loading
| Approach | Result |
|----------|--------|
| `LD_BIND_NOW=1` | No effect |
| Wrapper script with LD_PRELOAD | No effect |
| Shell spawn with inline LD_PRELOAD | No effect |
### Patching Variations
| Approach | Result |
|----------|--------|
| Null-padding after replacement | Crashes (made it worse) |
| No null-padding (develop behavior) | Still crashes |
| Minimal patches (3 instead of 6) | Still crashes |
| Ultra-minimal (1 patch - domain only) | Still crashes |
| Skip sentry patch | Still crashes |
| Skip subdomain patches | Still crashes |
**Key Finding:** Even patching just 1 string (main domain only) causes the crash.
---
## String Occurrences Found
### Length-Prefixed Format
Found by default patcher mode:
| Offset | Content | Notes |
|--------|---------|-------|
| 0x1bc5d63 | `hytale.com` | **Surrounded by x86 code!** |
### UTF-16LE Format (3 occurrences)
| Offset | Content |
|--------|---------|
| 0x1bc5ad7 | `sentry.hytale.com/...` |
| 0x1bc5b3f | `https://hytale.com/help...` |
| 0x1bc5bc9 | `store.hytale.com/?...` |
---
## Binary Analysis
When patching with length-prefixed mode:
```
< 01bc5d60: 5933 b80a 0000 0068 0079 0074 0061 006c Y3.....h.y.t.a.l
< 01bc5d70: 0065 002e 0063 006f 006d 8933 8807 0000 .e...c.o.m.3....
---
> 01bc5d60: 5933 b80a 0000 0073 0061 006e 0061 0073 Y3.....s.a.n.a.s
> 01bc5d70: 006f 006c 002e 0077 0073 8933 8807 0000 .o.l...w.s.3....
```
**Structure:**
```
5933 b8 | 0a000000 | h.y.t.a.l.e...c.o.m | 8933 8807 0000
???????? | len=10 | string content | mov [rbx],esi?
```
- `5933 b8` before string - could be code or metadata
- `0a 00 00 00` - .NET length prefix (10 characters)
- String content in UTF-16LE
- `89 33` after - this is `mov [rbx], esi` in x86-64!
**The string is embedded near executable code, not in a clean data section.**
---
## GDB Stack Trace
```
#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
#7-#24 HytaleClient code (asset decompression)
```
Crash occurs in `libzstd.so` during `free()` after "Finished handling RequiredAssets".
---
## Hypotheses
### 1. .NET AOT String Metadata (Most Likely)
.NET AOT may have precomputed hashes, checksums, or relocation info for strings. Modifying string content breaks internal consistency, causing memory corruption when the runtime tries to use related data structures.
### 2. Code/Data Interleaving
The strings are embedded near x86 code (`89 33` = `mov [rbx], esi`). .NET AOT may use relative offsets that get invalidated when we modify nearby bytes.
### 3. Binary Checksums
The binary may have integrity checks for certain sections that we're invalidating by patching.
### 4. Timing-Dependent Race Condition
The fact that it works randomly (~30% of the time with jemalloc) suggests a race condition that's affected by:
- Memory layout changes from patching
- Allocator behavior differences
- CPU scheduling
---
## Valgrind Results (Misleading)
- Valgrind showed NO memory corruption errors
- Game ran successfully under Valgrind (slower execution)
- This suggested jemalloc would fix it, but it doesn't consistently work
The slowdown from Valgrind likely masks the race condition timing.
---
## Current Launcher Implementation
The launcher attempts:
1. Auto-detect jemalloc at common paths
2. Auto-install jemalloc via pkexec if not found
3. Launch game with `LD_PRELOAD` via shell command
But this doesn't provide stable results.
---
## Potential Alternative Approaches (Not Yet Tried)
### 1. LD_PRELOAD Network Hooking
Instead of patching the binary, hook `getaddrinfo()` / `connect()` to redirect network calls at runtime. No binary modification needed.
### 2. Local Proxy + Certificate
Run a local HTTPS proxy that intercepts hytale.com traffic and redirects to custom server. Requires installing a custom CA certificate.
### 3. DNS + iptables Redirect
Use local DNS to resolve hytale.com to localhost, then iptables to redirect to actual custom server. Requires root/sudo.
### 4. Container with Older glibc
Run the game in a container with glibc < 2.41 where the stricter validation doesn't exist.
### 5. Different Patching Location
Find strings in a pure data section rather than code-adjacent areas.
---
## Files Reference
**Binary:** `HytaleClient` (ELF 64-bit, ~39.9 MB)
**Branch:** `fix/steamdeck-jemalloc-crash`
---
## Install jemalloc (Partial Mitigation)
jemalloc may help in some cases (~30% success rate):
```bash
# Steam Deck / Arch Linux
sudo pacman -S jemalloc
# Ubuntu / Debian
sudo apt install libjemalloc2
# Fedora / RHEL
sudo dnf install jemalloc
```
The launcher automatically uses jemalloc if found. To disable:
```bash
HYTALE_NO_JEMALLOC=1 npm start
```
---
## Conclusion
**No stable solution found.** The binary patching approach may be fundamentally incompatible with glibc 2.41's stricter heap validation when modifying .NET AOT compiled binaries.
Alternative approaches (network hooking, proxy, container) may be required for reliable Steam Deck / Ubuntu LTS support.

View File

@@ -1,95 +0,0 @@
# Steam Deck / Linux Crash Fix
## SOLUTION: Use jemalloc ✓
The crash is caused by glibc 2.41's stricter heap validation. Using jemalloc as the memory allocator fixes the issue.
### Install jemalloc
```bash
# Steam Deck / Arch Linux
sudo pacman -S jemalloc
# Ubuntu / Debian
sudo apt install libjemalloc2
# Fedora / RHEL
sudo dnf install jemalloc
```
### Launcher Auto-Detection
The launcher automatically uses jemalloc when installed. No manual configuration needed.
To disable (for testing):
```bash
HYTALE_NO_JEMALLOC=1 npm start
```
### Manual Launch with jemalloc
```bash
cd ~/.hytalef2p/release/package/game/latest
LD_PRELOAD=/usr/lib/libjemalloc.so.2 ./Client/HytaleClient --app-dir /home/deck/.hytalef2p/release/package/game/latest --java-exec /home/deck/.hytalef2p/release/package/jre/latest/bin/java --auth-mode authenticated --uuid YOUR_UUID --name Player --identity-token YOUR_TOKEN --session-token YOUR_TOKEN --user-dir /home/deck/.hytalesaves
```
---
## Debug Commands (for troubleshooting)
### Base Command
```bash
cd ~/.hytalef2p/release/package/game/latest
```
### GDB Stack Trace (for crash analysis)
```bash
gdb -ex "run --app-dir ..." ./Client/HytaleClient
# After crash:
bt
bt full
info registers
quit
```
### Test glibc tunables (alternative fixes that didn't work reliably)
**Disable tcache:**
```bash
GLIBC_TUNABLES=glibc.malloc.tcache_count=0 ./Client/HytaleClient ...
```
**Disable heap validation:**
```bash
MALLOC_CHECK_=0 ./Client/HytaleClient ...
```
### Binary Validation
```bash
file ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
ldd ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
```
### Hex Dump Commands
```bash
# Search for hytale.com UTF-16LE
xxd ~/.hytalef2p/release/package/game/latest/Client/HytaleClient.original | grep "6800 7900 7400 6100 6c00 6500 2e00 6300 6f00 6d00"
```
---
## Test Different Patch Modes
```bash
# Restore original
cp ~/.hytalef2p/release/package/game/latest/Client/HytaleClient.original ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
rm ~/.hytalef2p/release/package/game/latest/Client/HytaleClient.patched_custom
# Test UTF-16LE mode
HYTALE_PATCH_MODE=utf16le HYTALE_AUTH_DOMAIN=sanasol.ws npm start
# Test length-prefixed mode (default)
HYTALE_AUTH_DOMAIN=sanasol.ws npm start
```

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 {
@@ -299,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 () => {

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